chromium/chrome/android/java/src/org/chromium/chrome/browser/ui/AppLaunchDrawBlocker.java

// Copyright 2021 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.ui;

import android.content.Intent;
import android.os.SystemClock;
import android.text.TextUtils;
import android.view.View;

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

import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.homepage.HomepageManager;
import org.chromium.chrome.browser.incognito.IncognitoTabLauncher;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.lifecycle.InflationObserver;
import org.chromium.chrome.browser.lifecycle.StartStopWithNativeObserver;
import org.chromium.chrome.browser.ntp.NewTabPage;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
import org.chromium.chrome.browser.tabmodel.TabPersistentStore;
import org.chromium.chrome.browser.tabmodel.TabPersistentStore.ActiveTabState;
import org.chromium.components.embedder_support.util.UrlUtilities;

/**
 * Helper class for blocking {@link ChromeTabbedActivity} content view draw on launch until the
 * initial tab is available and recording related metrics. It will start blocking the view in
 * #onPostInflationStartup. Once the tab is available, #onActiveTabAvailable should be called stop
 * blocking.
 */
public class AppLaunchDrawBlocker {
    private final ActivityLifecycleDispatcher mActivityLifecycleDispatcher;
    private final InflationObserver mInflationObserver;
    private final StartStopWithNativeObserver mStartStopWithNativeObserver;
    private final Supplier<View> mViewSupplier;
    private final Supplier<Intent> mIntentSupplier;
    private final Supplier<Boolean> mShouldIgnoreIntentSupplier;
    private final Supplier<Boolean> mIsTabletSupplier;
    private final ObservableSupplier<Profile> mProfileSupplier;

    /**
     * An app draw blocker that takes care of blocking the draw when we are restoring tabs with
     * Incognito.
     */
    private final IncognitoRestoreAppLaunchDrawBlocker mIncognitoRestoreAppLaunchDrawBlocker;

    /**
     * Whether to return false from #onPreDraw of the content view to prevent drawing the browser UI
     * before the tab is ready.
     */
    private boolean mBlockDrawForInitialTab;

    private boolean mBlockDrawForOverviewPage;
    private boolean mBlockDrawForIncognitoRestore;
    private long mTimeStartedBlockingDrawForInitialTab;
    private long mTimeStartedBlockingDrawForIncognitoRestore;

    /**
     * Constructor for AppLaunchDrawBlocker.
     *
     * @param activityLifecycleDispatcher {@link ActivityLifecycleDispatcher} for the {@link
     *     ChromeTabbedActivity}.
     * @param viewSupplier {@link Supplier<Boolean>} for the Activity's content view.
     * @param intentSupplier The {@link Intent} the app was launched with.
     * @param shouldIgnoreIntentSupplier {@link Supplier<Boolean>} for whether the ignore should be
     *     ignored.
     * @param isTabletSupplier {@link Supplier<Boolean>} for whether the device is a tablet.
     * @param incognitoRestoreAppLaunchDrawBlockerFactory Factory to create {@link
     *     IncognitoRestoreAppLaunchDrawBlocker}.
     */
    public AppLaunchDrawBlocker(
            @NonNull ActivityLifecycleDispatcher activityLifecycleDispatcher,
            @NonNull Supplier<View> viewSupplier,
            @NonNull Supplier<Intent> intentSupplier,
            @NonNull Supplier<Boolean> shouldIgnoreIntentSupplier,
            @NonNull Supplier<Boolean> isTabletSupplier,
            @NonNull ObservableSupplier<Profile> profileSupplier,
            @NonNull
                    IncognitoRestoreAppLaunchDrawBlockerFactory
                            incognitoRestoreAppLaunchDrawBlockerFactory) {
        mActivityLifecycleDispatcher = activityLifecycleDispatcher;
        mViewSupplier = viewSupplier;
        mInflationObserver =
                new InflationObserver() {
                    @Override
                    public void onPreInflationStartup() {}

                    @Override
                    public void onPostInflationStartup() {
                        maybeBlockDraw();
                        maybeBlockDrawForIncognitoRestore();
                    }
                };
        mActivityLifecycleDispatcher.register(mInflationObserver);
        mStartStopWithNativeObserver =
                new StartStopWithNativeObserver() {
                    @Override
                    public void onStartWithNative() {}

                    @Override
                    public void onStopWithNative() {
                        writeSearchEngineHadLogoPref();
                    }
                };
        mActivityLifecycleDispatcher.register(mStartStopWithNativeObserver);
        mIntentSupplier = intentSupplier;
        mShouldIgnoreIntentSupplier = shouldIgnoreIntentSupplier;
        mIsTabletSupplier = isTabletSupplier;
        mProfileSupplier = profileSupplier;
        mIncognitoRestoreAppLaunchDrawBlocker =
                incognitoRestoreAppLaunchDrawBlockerFactory.create(
                        intentSupplier,
                        shouldIgnoreIntentSupplier,
                        activityLifecycleDispatcher,
                        this::onIncognitoRestoreUnblockConditionsFired);
    }

    /** Unregister lifecycle observers. */
    public void destroy() {
        mActivityLifecycleDispatcher.unregister(mInflationObserver);
        mActivityLifecycleDispatcher.unregister(mStartStopWithNativeObserver);
        mIncognitoRestoreAppLaunchDrawBlocker.destroy();
    }

    /** Should be called when the initial tab is available. */
    public void onActiveTabAvailable(boolean isTabNtp) {
        mBlockDrawForInitialTab = false;
    }

    /** Should be called when the overview page is available. */
    public void onOverviewPageAvailable() {
        mBlockDrawForOverviewPage = false;
    }

    /**
     * A method that is passed as a {@link Runnable} to {@link
     * IncognitoRestoreAppLaunchDrawBlocker}.
     *
     * This gets fired when all the conditions needed to unblock the draw from the Incognito restore
     * are fired.
     */
    @VisibleForTesting
    public void onIncognitoRestoreUnblockConditionsFired() {
        if (mBlockDrawForIncognitoRestore) {
            mBlockDrawForIncognitoRestore = false;
            RecordHistogram.recordTimesHistogram(
                    "Android.AppLaunch.DurationDrawWasBlocked.OnIncognitoReauth",
                    SystemClock.elapsedRealtime() - mTimeStartedBlockingDrawForIncognitoRestore);
        }
    }

    private void writeSearchEngineHadLogoPref() {
        Profile profile = mProfileSupplier.get();
        if (profile == null) return;
        boolean searchEngineHasLogo =
                TemplateUrlServiceFactory.getForProfile(profile.getOriginalProfile())
                        .doesDefaultSearchEngineHaveLogo();
        ChromeSharedPreferences.getInstance()
                .writeBoolean(
                        ChromePreferenceKeys.APP_LAUNCH_SEARCH_ENGINE_HAD_LOGO,
                        searchEngineHasLogo);
    }

    /**
     * Conditionally blocks the draw independently from the other clients for the Incognito restore
     * use-case.
     */
    private void maybeBlockDrawForIncognitoRestore() {
        if (!mIncognitoRestoreAppLaunchDrawBlocker.shouldBlockDraw()) return;
        mBlockDrawForIncognitoRestore = true;
        mTimeStartedBlockingDrawForIncognitoRestore = SystemClock.elapsedRealtime();
        ViewDrawBlocker.blockViewDrawUntilReady(
                mViewSupplier.get(), () -> !mBlockDrawForIncognitoRestore);
    }

    /** Only block the draw if we believe the initial tab will be the NTP. */
    private void maybeBlockDraw() {
        @ActiveTabState int tabState = TabPersistentStore.readLastKnownActiveTabStatePref();
        boolean searchEngineHasLogo =
                ChromeSharedPreferences.getInstance()
                        .readBoolean(ChromePreferenceKeys.APP_LAUNCH_SEARCH_ENGINE_HAD_LOGO, true);
        boolean singleUrlBarMode =
                NewTabPage.isInSingleUrlBarMode(mIsTabletSupplier.get(), searchEngineHasLogo);

        String url = IntentHandler.getUrlFromIntent(mIntentSupplier.get());
        boolean hasValidIntentUrl = !mShouldIgnoreIntentSupplier.get() && !TextUtils.isEmpty(url);
        boolean isNtpUrl = UrlUtilities.isCanonicalizedNtpUrl(url);

        boolean shouldBlockWithoutIntent =
                shouldBlockDrawForNtpOnColdStartWithoutIntent(
                        tabState,
                        HomepageManager.getInstance().isHomepageNonNtp(),
                        singleUrlBarMode);

        if (shouldBlockDrawForNtpOnColdStartWithIntent(
                hasValidIntentUrl,
                isNtpUrl,
                IncognitoTabLauncher.didCreateIntent(mIntentSupplier.get()),
                shouldBlockWithoutIntent)) {
            mTimeStartedBlockingDrawForInitialTab = SystemClock.elapsedRealtime();
            mBlockDrawForInitialTab = true;
            ViewDrawBlocker.blockViewDrawUntilReady(
                    mViewSupplier.get(), () -> !mBlockDrawForInitialTab);
        }
    }

    /**
     * @param lastKnownActiveTabState Last known {@link @ActiveTabState}.
     * @param homepageNonNtp Whether the homepage is Non-Ntp.
     * @param singleUrlBarMode Whether in single UrlBar mode, i.e. the url bar is shown in-line in
     *        the NTP.
     * @return Whether the View draw should be blocked because the NTP will be shown on cold start.
     */
    private boolean shouldBlockDrawForNtpOnColdStartWithoutIntent(
            @ActiveTabState int lastKnownActiveTabState,
            boolean homepageNonNtp,
            boolean singleUrlBarMode) {
        boolean willShowNtp =
                lastKnownActiveTabState == ActiveTabState.NTP
                        || (lastKnownActiveTabState == ActiveTabState.EMPTY && !homepageNonNtp);
        return willShowNtp && singleUrlBarMode;
    }

    /**
     * @param hasValidIntentUrl Whether there is an intent that isn't ignored with a non-empty Url.
     * @param isNtpUrl Whether the intent has NTP Url.
     * @param shouldLaunchIncognitoTab Whether the intent is launching an incognito tab.
     * @param shouldBlockDrawForNtpOnColdStartWithoutIntent Result of
     *        {@link #shouldBlockDrawForNtpOnColdStartWithoutIntent}.
     * @return Whether the View draw should be blocked because the NTP will be shown on cold start.
     */
    private boolean shouldBlockDrawForNtpOnColdStartWithIntent(
            boolean hasValidIntentUrl,
            boolean isNtpUrl,
            boolean shouldLaunchIncognitoTab,
            boolean shouldBlockDrawForNtpOnColdStartWithoutIntent) {
        if (hasValidIntentUrl && isNtpUrl) {
            return !shouldLaunchIncognitoTab;
        } else if (hasValidIntentUrl && !isNtpUrl) {
            return false;
        } else {
            return shouldBlockDrawForNtpOnColdStartWithoutIntent;
        }
    }
}