chromium/chrome/android/features/tab_ui/java/src/org/chromium/chrome/browser/tasks/tab_management/TabGroupListCoordinator.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.tasks.tab_management;

import static org.chromium.chrome.browser.tasks.tab_management.TabGroupListProperties.ON_IS_SCROLLED_CHANGED;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;

import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.util.Consumer;

import org.chromium.base.Callback;
import org.chromium.chrome.browser.data_sharing.DataSharingServiceFactory;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.hub.PaneManager;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileProvider;
import org.chromium.chrome.browser.signin.services.IdentityServicesProvider;
import org.chromium.chrome.browser.sync.SyncServiceFactory;
import org.chromium.chrome.browser.tab_group_sync.TabGroupSyncFeatures;
import org.chromium.chrome.browser.tab_group_sync.TabGroupSyncServiceFactory;
import org.chromium.chrome.browser.tab_group_sync.TabGroupUiActionHandler;
import org.chromium.chrome.browser.tab_ui.TabListFaviconProvider;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
import org.chromium.chrome.browser.ui.favicon.FaviconHelper;
import org.chromium.chrome.browser.ui.favicon.FaviconHelper.FaviconImageCallback;
import org.chromium.chrome.tab_ui.R;
import org.chromium.components.data_sharing.DataSharingService;
import org.chromium.components.embedder_support.util.UrlUtilities;
import org.chromium.components.signin.identitymanager.IdentityManager;
import org.chromium.components.sync.SyncService;
import org.chromium.components.tab_group_sync.TabGroupSyncService;
import org.chromium.ui.base.ViewUtils;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.ui.modelutil.LayoutViewBuilder;
import org.chromium.ui.modelutil.MVCListAdapter.ModelList;
import org.chromium.ui.modelutil.MVCListAdapter.ViewBuilder;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
import org.chromium.ui.modelutil.SimpleRecyclerViewAdapter;
import org.chromium.url.GURL;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/** Orchestrates the displaying of a list of interactable tab groups. */
public class TabGroupListCoordinator {
    @IntDef({RowType.TAB_GROUP})
    @Retention(RetentionPolicy.SOURCE)
    public @interface RowType {
        int TAB_GROUP = 0;
    }

    private final TabGroupListView mView;

    private final SimpleRecyclerViewAdapter mSimpleRecyclerViewAdapter;
    private TabGroupListMediator mTabGroupListMediator;

    /**
     * @param context Used to load resources and views.
     * @param filter Used to interact with local tab model and groups.
     * @param profileProvider Used to fetch keyed service.
     * @param paneManager Used to switch to show detailed tab group UI.
     * @param tabGroupUiActionHandler Used to open hidden tab groups.
     * @param modalDialogManager Used to show confirmation dialogs.
     * @param onIsScrolledChanged To be invoked whenever the scrolled state changes.
     */
    public TabGroupListCoordinator(
            Context context,
            TabGroupModelFilter filter,
            ProfileProvider profileProvider,
            PaneManager paneManager,
            TabGroupUiActionHandler tabGroupUiActionHandler,
            ModalDialogManager modalDialogManager,
            Consumer<Boolean> onIsScrolledChanged) {
        ModelList modelList = new ModelList();
        PropertyModel.Builder builder = new PropertyModel.Builder(TabGroupListProperties.ALL_KEYS);
        builder.with(ON_IS_SCROLLED_CHANGED, onIsScrolledChanged);
        PropertyModel propertyModel = builder.build();

        ViewBuilder<TabGroupRowView> layoutBuilder =
                new LayoutViewBuilder<>(R.layout.tab_group_row);
        mSimpleRecyclerViewAdapter = new SimpleRecyclerViewAdapter(modelList);
        mSimpleRecyclerViewAdapter.registerType(
                RowType.TAB_GROUP, layoutBuilder, new TabGroupRowViewBinder());

        mView =
                (TabGroupListView)
                        LayoutInflater.from(context)
                                .inflate(R.layout.tab_group_list, /* root= */ null);
        PropertyModelChangeProcessor.create(propertyModel, mView, TabGroupListViewBinder::bind);
        mView.setRecyclerViewAdapter(mSimpleRecyclerViewAdapter);

        Profile profile = profileProvider.getOriginalProfile();
        FaviconResolver faviconResolver = buildFaviconResolver(context, profile);
        @Nullable TabGroupSyncService tabGroupSyncService = null;
        if (TabGroupSyncFeatures.isTabGroupSyncEnabled(profile)) {
            tabGroupSyncService = TabGroupSyncServiceFactory.getForProfile(profile);
        }
        @Nullable DataSharingService dataSharingService = null;
        if (ChromeFeatureList.isEnabled(ChromeFeatureList.DATA_SHARING)) {
            dataSharingService = DataSharingServiceFactory.getForProfile(profile);
        }
        IdentityManager identityManager =
                IdentityServicesProvider.get().getIdentityManager(profile);
        ActionConfirmationManager actionConfirmationManager =
                new ActionConfirmationManager(profile, context, filter, modalDialogManager);
        SyncService syncService = SyncServiceFactory.getForProfile(profile);
        mTabGroupListMediator =
                new TabGroupListMediator(
                        modelList,
                        propertyModel,
                        filter,
                        faviconResolver,
                        tabGroupSyncService,
                        dataSharingService,
                        identityManager,
                        paneManager,
                        tabGroupUiActionHandler,
                        actionConfirmationManager,
                        syncService);
    }

    @VisibleForTesting
    static FaviconResolver buildFaviconResolver(Context context, Profile profile) {
        TabListFaviconProvider fallbackProvider =
                new TabListFaviconProvider(
                        context, /* isTabStrip= */ false, R.dimen.default_favicon_corner_radius);
        return (GURL url, Callback<Drawable> callback) -> {
            if (UrlUtilities.isInternalScheme(url)) {
                callback.onResult(
                        fallbackProvider
                                .getRoundedChromeFavicon(/* isIncognito= */ false)
                                .getDefaultDrawable());
            } else {
                resolveForeignFavicon(context, profile, fallbackProvider, url, callback);
            }
        };
    }

    private static void resolveForeignFavicon(
            Context context,
            Profile profile,
            TabListFaviconProvider fallbackProvider,
            GURL url,
            Callback<Drawable> callback) {
        Resources resources = context.getResources();
        int faviconSizePixels = resources.getDimensionPixelSize(R.dimen.tab_grid_favicon_size);
        FaviconImageCallback faviconImageCallback =
                (Bitmap bitmap, GURL ignored) ->
                        onForeignFavicon(context, fallbackProvider, callback, bitmap);
        new FaviconHelper()
                .getForeignFaviconImageForURL(
                        profile, url, faviconSizePixels, faviconImageCallback);
    }

    private static void onForeignFavicon(
            Context context,
            TabListFaviconProvider fallbackProvider,
            Callback<Drawable> callback,
            Bitmap bitmap) {
        Resources resources = context.getResources();
        final Drawable drawable;
        if (bitmap == null) {
            drawable =
                    fallbackProvider
                            .getDefaultFavicon(/* isIncognito= */ false)
                            .getDefaultDrawable();
        } else {
            int cornerRadiusPixels =
                    resources.getDimensionPixelSize(R.dimen.default_favicon_corner_radius);
            drawable = ViewUtils.createRoundedBitmapDrawable(resources, bitmap, cornerRadiusPixels);
        }
        callback.onResult(drawable);
    }

    /** Returns the root view of this component, allowing the parent to anchor in the hierarchy. */
    public View getView() {
        return mView;
    }

    /** Permanently cleans up this component. */
    public void destroy() {
        mTabGroupListMediator.destroy();
        mSimpleRecyclerViewAdapter.destroy();
    }
}