// Copyright 2022 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.Bundle;
import androidx.annotation.NonNull;
import org.chromium.base.Callback;
import org.chromium.base.CallbackController;
import org.chromium.base.CommandLine;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.crypto.CipherFactory;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.incognito.reauth.IncognitoReauthControllerImpl;
import org.chromium.chrome.browser.incognito.reauth.IncognitoReauthManager;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.lifecycle.NativeInitObserver;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorObserver;
import org.chromium.chrome.browser.tabmodel.TabModelUtils;
/** A class that provides functionality to block the initial draw for the Incognito restore flow. */
public class IncognitoRestoreAppLaunchDrawBlocker {
/**
* A key that is used to persist information about the last tab model selected to the saved
* instance state.
*/
public static final String IS_INCOGNITO_SELECTED = "is_incognito_selected";
/** A {@link Supplier<Bundle>} for the saved instance state supplier. */
private final @NonNull Supplier<Bundle> mSavedInstanceStateSupplier;
/** A supplier of {@link TabModelSelector} instance.*/
private final @NonNull ObservableSupplier<TabModelSelector> mTabModelSelectorSupplier;
/**
* A {@link ActivityLifecycleDispatcher} instance which allows to listen for {@link
* NativeInitObserver} signals.
*/
private final @NonNull ActivityLifecycleDispatcher mActivityLifecycleDispatcher;
/**
* A {@link Supplier<Intent>} intent supplier which allows to get the intent if Chrome was
* launched from one.
*/
private final @NonNull Supplier<Intent> mIntentSupplier;
/** A {@link Supplier<Boolean>} to indicate whether we should ignore the intent. */
private final @NonNull Supplier<Boolean> mShouldIgnoreIntentSupplier;
/**
* A {@link Runnable} to unblock the draw operation. This is fired when both native and tab
* state has been initialized.
*/
private final @NonNull Runnable mUnblockDrawRunnable;
/** A boolean so we don't fire unblock draw runnable twice. */
private boolean mIsUnblockDrawRunnableInvoked;
/**
* An observer to listen for the onFinishNativeInitialization events. This signal works as a
* fallback to unblock the draw,together with whether the tab state is initialized.
*/
private final NativeInitObserver mNativeInitObserver =
new NativeInitObserver() {
@Override
public void onFinishNativeInitialization() {
mIsNativeInitializationFinished = true;
maybeUnblockDraw();
}
};
/**
* The {@link CallbackController} for any callbacks that may run after the class is destroyed.
*/
private CallbackController mCallbackController = new CallbackController();
/**
* A callback to add a {@link TabModelSelectorObserver} which notifies about the event when the
* tab state is initialized. This is one of the signal along with the native initialization that
* we look for to unblock the draw.
*/
private final @NonNull Callback<TabModelSelector> mTabModelSelectorSupplierCallback =
(tabModelSelector) -> {
TabModelUtils.runOnTabStateInitialized(
tabModelSelector,
mCallbackController.makeCancelable(
unusedTabModelSelector -> maybeUnblockDraw()));
};
/**
* A boolean to indicate when native has finished initialization as by then we would have
* finished creating the {@link IncognitoReauthController}.
*/
private boolean mIsNativeInitializationFinished;
/**
* @param savedInstanceStateSupplier A {@link Supplier<Bundle>} instance to pass in the
* bundle that was persisted during onSaveInstanceState that
* allows to look for signals on whether to block the draw or
* not.
* @param tabModelSelectorSupplier A {@link ObservableSupplier<TabModelSelector>} that allows to
* listen for onTabStateInitialized signals which is used a
* fallback to unblock draw.
* @param intentSupplier The {@link Supplier<Intent>} which is passed when Chrome was launched
* through Intent.
* @param shouldIgnoreIntentSupplier A {@link Supplier<Boolean>} to indicate whether we need to
* ignore the intent.
* @param activityLifecycleDispatcher A {@link ActivityLifecycleDispatcher} which would allow to
* listen for onFinishNativeInitialization signal.
* @param unblockDrawRunnable A {@link Runnable} to unblock the draw operation.
*/
IncognitoRestoreAppLaunchDrawBlocker(
@NonNull Supplier<Bundle> savedInstanceStateSupplier,
@NonNull ObservableSupplier<TabModelSelector> tabModelSelectorSupplier,
@NonNull Supplier<Intent> intentSupplier,
@NonNull Supplier<Boolean> shouldIgnoreIntentSupplier,
@NonNull ActivityLifecycleDispatcher activityLifecycleDispatcher,
@NonNull Runnable unblockDrawRunnable) {
mSavedInstanceStateSupplier = savedInstanceStateSupplier;
mTabModelSelectorSupplier = tabModelSelectorSupplier;
mIntentSupplier = intentSupplier;
mShouldIgnoreIntentSupplier = shouldIgnoreIntentSupplier;
mActivityLifecycleDispatcher = activityLifecycleDispatcher;
mUnblockDrawRunnable = unblockDrawRunnable;
mActivityLifecycleDispatcher.register(mNativeInitObserver);
mTabModelSelectorSupplier.addObserver(mTabModelSelectorSupplierCallback);
}
/** The destroy method which would remove any added observers by this class. */
public void destroy() {
mCallbackController.destroy();
mActivityLifecycleDispatcher.unregister(mNativeInitObserver);
mTabModelSelectorSupplier.removeObserver(mTabModelSelectorSupplierCallback);
}
/**
* This method does some trivial level checks to quickly return false to unblock the draw. For
* more complex case, it would unblock once "both" tab state and native is initialized.
*
* @return True, if we need to block the draw. False, otherwise.
*/
public boolean shouldBlockDraw() {
// We can't test for UserPrefs here since the native may not be initialized and we will
// just wait for onTabStateInitialized to be triggered.
if (!IncognitoReauthManager.isIncognitoReauthFeatureAvailable()) return false;
if (CommandLine.getInstance().hasSwitch(ChromeSwitches.NO_RESTORE_STATE)) return false;
// A valid saved instance state is needed here.
if (!mSavedInstanceStateSupplier.hasValue()) return false;
Bundle savedInstanceState = mSavedInstanceStateSupplier.get();
if (!CipherFactory.getInstance().restoreFromBundle(savedInstanceState)) return false;
// There were no Incognito tabs before the Activity got destroyed. So we don't need to block
// draw here.
if (!savedInstanceState.getBoolean(
IncognitoReauthControllerImpl.KEY_IS_INCOGNITO_REAUTH_PENDING, false)) {
return false;
}
boolean isLastSelectedModelIncognito =
savedInstanceState.getBoolean(IS_INCOGNITO_SELECTED, false);
boolean isIncognitoFiredFromLauncherShortcut =
!mShouldIgnoreIntentSupplier.get()
&& mIntentSupplier.get() != null
&& mIntentSupplier
.get()
.getBooleanExtra(
IntentHandler.EXTRA_INVOKED_FROM_LAUNCH_NEW_INCOGNITO_TAB,
false);
// We have re-auth pending but we don't need to block draw if last tab model was regular and
// we are not trying to create a new incognito tab from launcher shortcut.
if (!isLastSelectedModelIncognito && !isIncognitoFiredFromLauncherShortcut) return false;
// This is part of the environment that is required to make decisions. Therefore we need
// this and we block.
if (mTabModelSelectorSupplier.get() == null) return true;
if (mTabModelSelectorSupplier.get().isTabStateInitialized()
&& mIsNativeInitializationFinished) {
return false;
}
// We block the draw.
return true;
}
private void maybeUnblockDraw() {
if (!mTabModelSelectorSupplier.hasValue()) return;
if (!mTabModelSelectorSupplier.get().isTabStateInitialized()) return;
if (!mIsNativeInitializationFinished) return;
if (mIsUnblockDrawRunnableInvoked) return;
mIsUnblockDrawRunnableInvoked = true;
mUnblockDrawRunnable.run();
}
/** Test-only method. */
public void resetIsUnblockDrawRunnableInvokedForTesting() {
mIsUnblockDrawRunnableInvoked = false;
}
}