chromium/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabTaskDescriptionHelper.java

// Copyright 2016 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.customtabs;

import android.app.Activity;
import android.app.ActivityManager;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Build;
import android.text.TextUtils;

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

import org.chromium.chrome.R;
import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.browserservices.intents.WebappExtras;
import org.chromium.chrome.browser.customtabs.content.CustomTabActivityTabProvider;
import org.chromium.chrome.browser.customtabs.content.TabObserverRegistrar;
import org.chromium.chrome.browser.customtabs.content.TabObserverRegistrar.CustomTabTabObserver;
import org.chromium.chrome.browser.dependency_injection.ActivityScope;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.lifecycle.DestroyObserver;
import org.chromium.chrome.browser.lifecycle.NativeInitObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.theme.TopUiThemeColorProvider;
import org.chromium.chrome.browser.ui.favicon.FaviconHelper;
import org.chromium.components.embedder_support.util.UrlUtilities;
import org.chromium.components.security_state.SecurityStateModel;
import org.chromium.content_public.browser.NavigationHandle;
import org.chromium.ui.util.ColorUtils;
import org.chromium.url.GURL;

import javax.inject.Inject;

/**
 * Helper that updates the Android task description given the state of the current tab.
 *
 * <p>
 * The task description is what is shown in Android's Overview/Recents screen for each entry.
 */
@ActivityScope
public class CustomTabTaskDescriptionHelper implements NativeInitObserver, DestroyObserver {
    private final Activity mActivity;
    private final CustomTabActivityTabProvider mTabProvider;
    private final TabObserverRegistrar mTabObserverRegistrar;
    private final BrowserServicesIntentDataProvider mIntentDataProvider;
    private final TopUiThemeColorProvider mTopUiThemeColorProvider;

    @Nullable private CustomTabTaskDescriptionIconGenerator mIconGenerator;
    @Nullable private FaviconHelper mFaviconHelper;

    @Nullable private CustomTabTabObserver mTabObserver;
    @Nullable private CustomTabTabObserver mIconTabObserver;
    @Nullable private CustomTabActivityTabProvider.Observer mActivityTabObserver;

    private int mDefaultThemeColor;
    @Nullable private String mForceTitle;
    @Nullable private Bitmap mForceIcon;
    private boolean mUseClientIcon;

    @Nullable private Bitmap mLargestFavicon;

    @Inject
    public CustomTabTaskDescriptionHelper(
            Activity activity,
            CustomTabActivityTabProvider tabProvider,
            TabObserverRegistrar tabObserverRegistrar,
            BrowserServicesIntentDataProvider intentDataProvider,
            ActivityLifecycleDispatcher activityLifecycleDispatcher,
            TopUiThemeColorProvider topUiThemeColorProvider) {
        mActivity = activity;
        mTabProvider = tabProvider;
        mTabObserverRegistrar = tabObserverRegistrar;
        mIntentDataProvider = intentDataProvider;
        mTopUiThemeColorProvider = topUiThemeColorProvider;

        activityLifecycleDispatcher.register(this);
    }

    @Override
    public void onFinishNativeInitialization() {
        WebappExtras webappExtras = mIntentDataProvider.getWebappExtras();
        boolean canUpdate = (webappExtras != null || usesSeparateTask());
        if (!canUpdate) return;

        mDefaultThemeColor = mActivity.getColor(R.color.default_primary_color);
        if (webappExtras != null) {
            if (mIntentDataProvider.getColorProvider().hasCustomToolbarColor()) {
                mDefaultThemeColor = mIntentDataProvider.getColorProvider().getToolbarColor();
            }
            mForceIcon = webappExtras.icon.bitmap();
            mForceTitle = webappExtras.shortName;

            // This is a workaround for crbug/1098580. ActivityManager.TaskDescription
            // does not handle adaptive icon when passing a bitmap. So set the task icon to be null
            // to preserve the client app's icon. Only set this flag on O+ because this does not
            // work with old_style_webapk.
            if (mIntentDataProvider.isWebApkActivity()
                    && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                mUseClientIcon = true;
            }
        }

        mIconGenerator = new CustomTabTaskDescriptionIconGenerator(mActivity);
        mFaviconHelper = new FaviconHelper();

        startObserving();
    }

    private void startObserving() {
        mTabObserver =
                new CustomTabTabObserver() {
                    @Override
                    public void onUrlUpdated(Tab tab) {
                        updateTaskDescription();
                    }

                    @Override
                    public void onTitleUpdated(Tab tab) {
                        updateTaskDescription();
                    }

                    @Override
                    public void onDidFinishNavigationInPrimaryMainFrame(
                            Tab tab, NavigationHandle navigation) {
                        if (navigation.hasCommitted() && !navigation.isSameDocument()) {
                            mLargestFavicon = null;
                            updateTaskDescription();
                        }
                    }

                    @Override
                    public void onLoadStopped(Tab tab, boolean toDifferentDocument) {
                        updateTaskDescription();
                    }

                    @Override
                    public void onDidChangeThemeColor(Tab tab, int color) {
                        updateTaskDescription();
                    }

                    @Override
                    public void onAttachedToInitialTab(@NonNull Tab tab) {
                        onActiveTabChanged();
                    }

                    @Override
                    public void onObservingDifferentTab(@NonNull Tab tab) {
                        onActiveTabChanged();
                    }
                };
        mTabObserverRegistrar.registerActivityTabObserver(mTabObserver);

        if (mForceIcon == null && !mUseClientIcon) {
            mIconTabObserver =
                    new CustomTabTabObserver() {
                        @Override
                        public void onWebContentsSwapped(
                                Tab tab, boolean didStartLoad, boolean didFinishLoad) {
                            if (!didStartLoad) return;
                            resetIcon();
                        }

                        @Override
                        public void onFaviconUpdated(Tab tab, Bitmap icon, GURL iconUrl) {
                            if (icon == null) return;
                            updateFavicon(icon);
                        }

                        @Override
                        public void onSSLStateUpdated(Tab tab) {
                            if (hasSecurityWarningOrError(tab)) resetIcon();
                        }

                        private boolean hasSecurityWarningOrError(Tab tab) {
                            boolean isContentDangerous =
                                    SecurityStateModel.isContentDangerous(tab.getWebContents());
                            return isContentDangerous;
                        }
                    };
            mTabObserverRegistrar.registerActivityTabObserver(mIconTabObserver);
        }
    }

    private void stopObserving() {
        mTabObserverRegistrar.unregisterActivityTabObserver(mTabObserver);
        mTabObserverRegistrar.unregisterActivityTabObserver(mIconTabObserver);
    }

    private void onActiveTabChanged() {
        updateTaskDescription();
        if (mForceIcon == null) {
            fetchIcon();
        }
    }

    private void resetIcon() {
        mLargestFavicon = null;
        updateTaskDescription();
    }

    private void updateFavicon(Bitmap favicon) {
        if (favicon == null) return;
        if (mLargestFavicon == null
                || favicon.getWidth() > mLargestFavicon.getWidth()
                || favicon.getHeight() > mLargestFavicon.getHeight()) {
            mLargestFavicon = favicon;
            updateTaskDescription();
        }
    }

    private void updateTaskDescription() {
        mActivity.setTaskDescription(
                new ActivityManager.TaskDescription(
                        computeTitle(), computeIcon(), computeThemeColor()));
    }

    /** Computes the title for the task description. */
    private String computeTitle() {
        if (!TextUtils.isEmpty(mForceTitle)) return mForceTitle;

        Tab currentTab = mTabProvider.getTab();
        if (currentTab == null) return null;

        String label = currentTab.getTitle();
        String domain = UrlUtilities.getDomainAndRegistry(currentTab.getUrl().getSpec(), false);
        if (TextUtils.isEmpty(label)) {
            label = domain;
        }
        return label;
    }

    /** Computes the icon for the task description. */
    private Bitmap computeIcon() {
        if (mUseClientIcon) return null;

        if (mForceIcon != null) return mForceIcon;

        Tab currentTab = mTabProvider.getTab();
        if (currentTab == null) return null;

        Bitmap bitmap = null;
        if (!currentTab.isIncognito()) {
            bitmap = mIconGenerator.getBitmap(currentTab.getUrl(), mLargestFavicon);
        }
        return bitmap;
    }

    /** Computes the theme color for the task description. */
    private int computeThemeColor() {
        Tab tab = mTabProvider.getTab();
        int themeColor = mTopUiThemeColorProvider.getThemeColorOrFallback(tab, mDefaultThemeColor);
        return ColorUtils.getOpaqueColor(themeColor);
    }

    private void fetchIcon() {
        Tab currentTab = mTabProvider.getTab();
        if (currentTab == null) return;

        final GURL currentUrl = currentTab.getUrl();
        mFaviconHelper.getLocalFaviconImageForURL(
                currentTab.getProfile(),
                currentTab.getUrl(),
                0,
                (image, iconUrl) -> {
                    if (mTabProvider.getTab() == null
                            || !currentUrl.equals(mTabProvider.getTab().getUrl())) {
                        return;
                    }

                    updateFavicon(image);
                });
    }

    /** Returns true when the activity has been launched in a separate task. */
    private boolean usesSeparateTask() {
        final int separateTaskFlags =
                Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
        return (mActivity.getIntent().getFlags() & separateTaskFlags) != 0;
    }

    /** Destroys all dependent components of the task description helper. */
    @Override
    public void onDestroy() {
        if (mFaviconHelper != null) {
            mFaviconHelper.destroy();
        }
        stopObserving();
    }
}