chromium/chrome/browser/tab_group_sync/android/java/src/org/chromium/chrome/browser/tab_group_sync/TabGroupSyncUtils.java

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.tab_group_sync;

import android.text.TextUtils;
import android.util.Pair;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.Token;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.components.embedder_support.util.UrlUtilities;
import org.chromium.components.tab_group_sync.ClosingSource;
import org.chromium.components.tab_group_sync.EventDetails;
import org.chromium.components.tab_group_sync.LocalTabGroupId;
import org.chromium.components.tab_group_sync.SavedTabGroup;
import org.chromium.components.tab_group_sync.TabGroupEvent;
import org.chromium.components.tab_group_sync.TabGroupSyncService;
import org.chromium.content_public.common.ContentUrlConstants;
import org.chromium.url.GURL;

/** Utility methods for tab group sync. */
public final class TabGroupSyncUtils {
    // The URL written to sync when the local URL isn't in a syncable format, i.e. HTTP or HTTPS.
    public static final GURL UNSAVEABLE_URL_OVERRIDE = new GURL(UrlConstants.NTP_NON_NATIVE_URL);
    public static final String UNSAVEABLE_TAB_TITLE = "Unsavable tab";
    public static final GURL NTP_URL = new GURL(UrlConstants.NTP_NON_NATIVE_URL);
    public static final String NEW_TAB_TITLE = "New tab";

    /**
     * Whether the given {@param localId} corresponds to a tab group in the current window
     * corresponding to {@param tabGroupModelFilter}.
     *
     * @param tabGroupModelFilter The tab group model filter in which to find the tab group.
     * @param localId The ID of the tab group.
     */
    public static boolean isInCurrentWindow(
            TabGroupModelFilter tabGroupModelFilter, LocalTabGroupId localId) {
        int rootId = tabGroupModelFilter.getRootIdFromStableId(localId.tabGroupId);
        return rootId != Tab.INVALID_TAB_ID;
    }

    /** Conversion method to get a {@link LocalTabGroupId} from a root ID. */
    public static LocalTabGroupId getLocalTabGroupId(TabGroupModelFilter filter, int rootId) {
        Token tabGroupId = filter.getStableIdFromRootId(rootId);
        return tabGroupId == null ? null : new LocalTabGroupId(tabGroupId);
    }

    /** Conversion method to get a root ID from a {@link LocalTabGroupId}. */
    public static int getRootId(TabGroupModelFilter filter, LocalTabGroupId localTabGroupId) {
        assert localTabGroupId != null;
        return filter.getRootIdFromStableId(localTabGroupId.tabGroupId);
    }

    /** Util method to get a {@link LocalTabGroupId} from a tab. */
    public static LocalTabGroupId getLocalTabGroupId(Tab tab) {
        return new LocalTabGroupId(tab.getTabGroupId());
    }

    /** Utility method to filter out URLs not suitable for tab group sync. */
    public static Pair<GURL, String> getFilteredUrlAndTitle(GURL url, String title) {
        assert url != null;
        if (isSavableUrl(url)) {
            return new Pair<>(url, title);
        } else if (isNtpOrAboutBlankUrl(url)) {
            return new Pair<>(NTP_URL, NEW_TAB_TITLE);
        } else {
            return new Pair<>(UNSAVEABLE_URL_OVERRIDE, UNSAVEABLE_TAB_TITLE);
        }
    }

    /** Utility method to determine if a URL can be synced or not. */
    public static boolean isSavableUrl(GURL url) {
        return UrlUtilities.isHttpOrHttps(url);
    }

    @VisibleForTesting
    static boolean isNtpOrAboutBlankUrl(GURL url) {
        String urlString = url.getValidSpecOrEmpty();
        return UrlUtilities.isNtpUrl(url)
                || TextUtils.equals(urlString, ContentUrlConstants.ABOUT_BLANK_DISPLAY_URL)
                || TextUtils.equals(urlString, ContentUrlConstants.ABOUT_BLANK_URL);
    }

    /**
     * Removes all tab groups mappings found in the {@link TabGroupSyncService} that don't have
     * corresponding local IDs in the {@link TabGroupModelFilter}.
     *
     * @param tabGroupSyncService The {@link TabGroupSyncService} to remove tabs from.
     * @param filter The {@link TabGroupModelFilter} to check for tab groups.
     */
    public static void unmapLocalIdsNotInTabGroupModelFilter(
            TabGroupSyncService tabGroupSyncService, TabGroupModelFilter filter) {
        assert !filter.isIncognito();

        for (String syncGroupId : tabGroupSyncService.getAllGroupIds()) {
            SavedTabGroup savedTabGroup = tabGroupSyncService.getGroup(syncGroupId);
            // If there is no local ID the group is already hidden so this is a no-op.
            if (savedTabGroup.localId == null) continue;

            if (!isInCurrentWindow(filter, savedTabGroup.localId)) {
                tabGroupSyncService.removeLocalTabGroupMapping(savedTabGroup.localId);
                recordTabGroupOpenCloseMetrics(
                        tabGroupSyncService,
                        /* open= */ false,
                        ClosingSource.CLEANED_UP_ON_LAST_INSTANCE_CLOSURE,
                        savedTabGroup.localId);
            }
        }
    }

    /** Helper method to record open close metrics. */
    public static void recordTabGroupOpenCloseMetrics(
            TabGroupSyncService tabGroupSyncService,
            boolean open,
            int source,
            LocalTabGroupId localTabGroupId) {
        int eventType = open ? TabGroupEvent.TAB_GROUP_OPENED : TabGroupEvent.TAB_GROUP_CLOSED;
        EventDetails eventDetails = new EventDetails(eventType);
        eventDetails.localGroupId = localTabGroupId;
        if (open) {
            eventDetails.openingSource = source;
        } else {
            eventDetails.closingSource = source;
        }
        tabGroupSyncService.recordTabGroupEvent(eventDetails);
    }

    /**
     * Tries to get the saved group for a given tab id, returning null if anything goes wrong, such
     * as the tab not being in a group.
     *
     * @param tabId The id of the tab.
     * @param tabModel The tab model to look up the tab in.
     * @param tabGroupSyncService The sync service to get tab group data form.
     * @return The group data object.
     */
    public static SavedTabGroup getSavedTabGroupFromTabId(
            int tabId,
            @NonNull TabModel tabModel,
            @NonNull TabGroupSyncService tabGroupSyncService) {
        @Nullable Tab tab = tabModel.getTabById(tabId);
        if (tab == null || tab.getTabGroupId() == null) return null;
        LocalTabGroupId localTabGroupId = new LocalTabGroupId(tab.getTabGroupId());
        return tabGroupSyncService.getGroup(localTabGroupId);
    }
}