// 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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Intent;
import android.net.Uri;
import android.os.SystemClock;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnPreDrawListener;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.SmallTest;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.LooperMode;
import org.robolectric.annotation.LooperMode.Mode;
import org.robolectric.shadows.ShadowSystemClock;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.supplier.Supplier;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.lifecycle.InflationObserver;
import org.chromium.chrome.browser.lifecycle.LifecycleObserver;
import org.chromium.chrome.browser.lifecycle.StartStopWithNativeObserver;
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.search_engines.TemplateUrlServiceFactoryJni;
import org.chromium.chrome.browser.tabmodel.TabPersistentStore.ActiveTabState;
import org.chromium.components.search_engines.TemplateUrlService;
import java.util.List;
/** Unit tests for AppLaunchDrawBlocker behavior. */
@RunWith(BaseRobolectricTestRunner.class)
@Config(
manifest = Config.NONE,
shadows = {ShadowSystemClock.class})
@LooperMode(Mode.PAUSED)
public class AppLaunchDrawBlockerUnitTest {
@Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule public JniMocker mJniMocker = new JniMocker();
@Mock private ActivityLifecycleDispatcher mActivityLifecycleDispatcher;
@Mock private View mView;
@Mock private ViewTreeObserver mViewTreeObserver;
@Mock private Intent mIntent;
@Mock private Profile mProfile;
@Mock private TemplateUrlServiceFactory.Natives mTemplateUrlServiceFactory;
@Mock private TemplateUrlService mTemplateUrlService;
@Mock private Supplier<Boolean> mShouldIgnoreIntentSupplier;
@Mock private Supplier<Boolean> mIsTabletSupplier;
@Mock private Supplier<Boolean> mShouldShowTabSwitcherOnStartSupplier;
private ObservableSupplierImpl<Profile> mProfileSupplier = new ObservableSupplierImpl<>();
@Mock
private IncognitoRestoreAppLaunchDrawBlockerFactory
mIncognitoRestoreAppLaunchDrawBlockerFactoryMock;
@Mock private IncognitoRestoreAppLaunchDrawBlocker mIncognitoRestoreAppLaunchDrawBlockerMock;
@Captor private ArgumentCaptor<OnPreDrawListener> mOnPreDrawListenerArgumentCaptor;
@Captor private ArgumentCaptor<LifecycleObserver> mLifecycleArgumentCaptor;
private static final int INITIAL_TIME = 1000;
private final Supplier<View> mViewSupplier = () -> mView;
private final Supplier<Intent> mIntentSupplier = () -> mIntent;
private InflationObserver mInflationObserver;
private StartStopWithNativeObserver mStartStopWithNativeObserver;
private AppLaunchDrawBlocker mAppLaunchDrawBlocker;
@Before
public void setUp() {
when(mView.getViewTreeObserver()).thenReturn(mViewTreeObserver);
mJniMocker.mock(TemplateUrlServiceFactoryJni.TEST_HOOKS, mTemplateUrlServiceFactory);
TemplateUrlServiceFactory.setInstanceForTesting(mTemplateUrlService);
when(mProfile.getOriginalProfile()).thenReturn(mProfile);
mProfileSupplier.set(mProfile);
when(mShouldIgnoreIntentSupplier.get()).thenReturn(false);
when(mIsTabletSupplier.get()).thenReturn(false);
when(mShouldShowTabSwitcherOnStartSupplier.get()).thenReturn(false);
when(mIncognitoRestoreAppLaunchDrawBlockerFactoryMock.create(
eq(mIntentSupplier),
eq(mShouldIgnoreIntentSupplier),
eq(mActivityLifecycleDispatcher),
any()))
.thenReturn(mIncognitoRestoreAppLaunchDrawBlockerMock);
mAppLaunchDrawBlocker =
new AppLaunchDrawBlocker(
mActivityLifecycleDispatcher,
mViewSupplier,
mIntentSupplier,
mShouldIgnoreIntentSupplier,
mIsTabletSupplier,
mProfileSupplier,
mIncognitoRestoreAppLaunchDrawBlockerFactoryMock);
validateConstructorAndCaptureObservers();
SystemClock.setCurrentTimeMillis(INITIAL_TIME);
}
@Test
public void testSearchEngineHadLogoPrefWritten() {
// Set to false initially.
ChromeSharedPreferences.getInstance()
.writeBoolean(ChromePreferenceKeys.APP_LAUNCH_SEARCH_ENGINE_HAD_LOGO, false);
when(mTemplateUrlService.doesDefaultSearchEngineHaveLogo()).thenReturn(true);
mStartStopWithNativeObserver.onStopWithNative();
assertTrue(
"SearchEngineHadLogo pref isn't written.",
ChromeSharedPreferences.getInstance()
.readBoolean(
ChromePreferenceKeys.APP_LAUNCH_SEARCH_ENGINE_HAD_LOGO, false));
}
@Test
public void testLastTabNtp_phone_searchEngineHasLogo_noIntent() {
ChromeSharedPreferences.getInstance()
.writeInt(
ChromePreferenceKeys.APP_LAUNCH_LAST_KNOWN_ACTIVE_TAB_STATE,
ActiveTabState.NTP);
setSearchEngineHasLogo(true);
mInflationObserver.onPostInflationStartup();
verify(mViewTreeObserver).addOnPreDrawListener(mOnPreDrawListenerArgumentCaptor.capture());
assertFalse(
"Draw is not blocked.", mOnPreDrawListenerArgumentCaptor.getValue().onPreDraw());
SystemClock.setCurrentTimeMillis(INITIAL_TIME + 10);
mAppLaunchDrawBlocker.onActiveTabAvailable(true);
assertTrue(
"Draw is still blocked.", mOnPreDrawListenerArgumentCaptor.getValue().onPreDraw());
verify(mViewTreeObserver)
.removeOnPreDrawListener(mOnPreDrawListenerArgumentCaptor.getValue());
}
@Test
public void testLastTabEmpty_phone_searchEngineHasLogo_noIntent() {
ChromeSharedPreferences.getInstance()
.writeInt(
ChromePreferenceKeys.APP_LAUNCH_LAST_KNOWN_ACTIVE_TAB_STATE,
ActiveTabState.EMPTY);
setSearchEngineHasLogo(true);
mInflationObserver.onPostInflationStartup();
verify(mViewTreeObserver).addOnPreDrawListener(mOnPreDrawListenerArgumentCaptor.capture());
assertFalse(
"Draw is not blocked.", mOnPreDrawListenerArgumentCaptor.getValue().onPreDraw());
SystemClock.setCurrentTimeMillis(INITIAL_TIME + 20);
mAppLaunchDrawBlocker.onActiveTabAvailable(true);
assertTrue(
"Draw is still blocked.", mOnPreDrawListenerArgumentCaptor.getValue().onPreDraw());
verify(mViewTreeObserver)
.removeOnPreDrawListener(mOnPreDrawListenerArgumentCaptor.getValue());
}
@Test
public void testLastTabOther_phone_searchEngineHasLogo_noIntent() {
ChromeSharedPreferences.getInstance()
.writeInt(
ChromePreferenceKeys.APP_LAUNCH_LAST_KNOWN_ACTIVE_TAB_STATE,
ActiveTabState.OTHER);
setSearchEngineHasLogo(true);
mInflationObserver.onPostInflationStartup();
verify(mViewTreeObserver, never())
.addOnPreDrawListener(mOnPreDrawListenerArgumentCaptor.capture());
mAppLaunchDrawBlocker.onActiveTabAvailable(false);
}
@Test
public void testLastTabNtp_phone_searchEngineHasLogo_withIntent() {
ChromeSharedPreferences.getInstance()
.writeInt(
ChromePreferenceKeys.APP_LAUNCH_LAST_KNOWN_ACTIVE_TAB_STATE,
ActiveTabState.NTP);
setSearchEngineHasLogo(true);
mIntent = new Intent();
mIntent.setData(Uri.parse("https://www.google.com"));
when(mShouldIgnoreIntentSupplier.get()).thenReturn(false);
mInflationObserver.onPostInflationStartup();
verify(mViewTreeObserver, never())
.addOnPreDrawListener(mOnPreDrawListenerArgumentCaptor.capture());
mAppLaunchDrawBlocker.onActiveTabAvailable(false);
}
@Test
public void testLastTabEmpty_phone_searchEngineHasLogo_withIntentIgnore() {
ChromeSharedPreferences.getInstance()
.writeInt(
ChromePreferenceKeys.APP_LAUNCH_LAST_KNOWN_ACTIVE_TAB_STATE,
ActiveTabState.EMPTY);
setSearchEngineHasLogo(true);
mIntent = new Intent();
mIntent.setData(Uri.parse("some/link"));
when(mShouldIgnoreIntentSupplier.get()).thenReturn(true);
mInflationObserver.onPostInflationStartup();
verify(mViewTreeObserver).addOnPreDrawListener(mOnPreDrawListenerArgumentCaptor.capture());
assertFalse(
"Draw is not blocked.", mOnPreDrawListenerArgumentCaptor.getValue().onPreDraw());
SystemClock.setCurrentTimeMillis(INITIAL_TIME + 16);
mAppLaunchDrawBlocker.onActiveTabAvailable(true);
assertTrue(
"Draw is still blocked.", mOnPreDrawListenerArgumentCaptor.getValue().onPreDraw());
verify(mViewTreeObserver)
.removeOnPreDrawListener(mOnPreDrawListenerArgumentCaptor.getValue());
}
@Test
public void testLastTabEmpty_phone_noSearchEngineLogo_noIntent() {
ChromeSharedPreferences.getInstance()
.writeInt(
ChromePreferenceKeys.APP_LAUNCH_LAST_KNOWN_ACTIVE_TAB_STATE,
ActiveTabState.EMPTY);
setSearchEngineHasLogo(false);
mInflationObserver.onPostInflationStartup();
verify(mViewTreeObserver, never())
.addOnPreDrawListener(mOnPreDrawListenerArgumentCaptor.capture());
mAppLaunchDrawBlocker.onActiveTabAvailable(true);
}
@Test
public void testLastTabNtp_tablet_searchEngineHasLogo_noIntent() {
ChromeSharedPreferences.getInstance()
.writeInt(
ChromePreferenceKeys.APP_LAUNCH_LAST_KNOWN_ACTIVE_TAB_STATE,
ActiveTabState.NTP);
setSearchEngineHasLogo(true);
when(mIsTabletSupplier.get()).thenReturn(true);
mInflationObserver.onPostInflationStartup();
verify(mViewTreeObserver, never())
.addOnPreDrawListener(mOnPreDrawListenerArgumentCaptor.capture());
mAppLaunchDrawBlocker.onActiveTabAvailable(true);
}
@Test
@EnableFeatures({ChromeFeatureList.FOCUS_OMNIBOX_IN_INCOGNITO_TAB_INTENTS})
public void testLastTabNtp_phone_searchEngineHasLogo_withIntent_incognito() {
ChromeSharedPreferences.getInstance()
.writeInt(
ChromePreferenceKeys.APP_LAUNCH_LAST_KNOWN_ACTIVE_TAB_STATE,
ActiveTabState.NTP);
setSearchEngineHasLogo(true);
mIntent =
IntentHandler.createTrustedOpenNewTabIntent(
ApplicationProvider.getApplicationContext(), true);
mIntent.putExtra(IntentHandler.EXTRA_INVOKED_FROM_LAUNCH_NEW_INCOGNITO_TAB, true);
when(mShouldIgnoreIntentSupplier.get()).thenReturn(false);
mInflationObserver.onPostInflationStartup();
verify(mViewTreeObserver, never())
.addOnPreDrawListener(mOnPreDrawListenerArgumentCaptor.capture());
mAppLaunchDrawBlocker.onActiveTabAvailable(false);
}
@Test
public void testBlockedButShouldNotHaveRecorded() {
// Same scenario as #testLastTabNtp_phone_searchEngineHasLogo_noIntent, but we assume the
// prediction to block was wrong to verify the histogram is recorded correctly.
ChromeSharedPreferences.getInstance()
.writeInt(
ChromePreferenceKeys.APP_LAUNCH_LAST_KNOWN_ACTIVE_TAB_STATE,
ActiveTabState.NTP);
setSearchEngineHasLogo(true);
mInflationObserver.onPostInflationStartup();
mAppLaunchDrawBlocker.onActiveTabAvailable(false);
}
@Test
public void testDidNotBlockButShouldHaveRecorded() {
// Same scenario as #testLastTabEmpty_phone_noSearchEngineLogo_noIntent, but we assume the
// prediction to not block was wrong to verify the histogram is recorded correctly.
ChromeSharedPreferences.getInstance()
.writeInt(
ChromePreferenceKeys.APP_LAUNCH_LAST_KNOWN_ACTIVE_TAB_STATE,
ActiveTabState.OTHER);
setSearchEngineHasLogo(true);
mInflationObserver.onPostInflationStartup();
mAppLaunchDrawBlocker.onActiveTabAvailable(true);
}
@Test
@SmallTest
public void testShouldBlockDrawForIncognitoRestore_AddsOnPreDrawListener() {
when(mIncognitoRestoreAppLaunchDrawBlockerMock.shouldBlockDraw()).thenReturn(true);
mInflationObserver.onPostInflationStartup();
verify(mViewTreeObserver, times(2))
.addOnPreDrawListener(mOnPreDrawListenerArgumentCaptor.capture());
verify(mIncognitoRestoreAppLaunchDrawBlockerMock, times(1)).shouldBlockDraw();
}
@Test
@SmallTest
public void testShouldNotBlockDrawForIncognitoRestore_DoesNotAddOnPreDrawListener() {
when(mIncognitoRestoreAppLaunchDrawBlockerMock.shouldBlockDraw()).thenReturn(false);
mInflationObserver.onPostInflationStartup();
verify(mViewTreeObserver, times(1))
.addOnPreDrawListener(mOnPreDrawListenerArgumentCaptor.capture());
verify(mIncognitoRestoreAppLaunchDrawBlockerMock, times(1)).shouldBlockDraw();
}
@Test
@SmallTest
public void testOnPreDrawListenerRemoved_WhenNoLongerNeedToBlockDrawForIncognitoRestore() {
when(mIncognitoRestoreAppLaunchDrawBlockerMock.shouldBlockDraw()).thenReturn(true);
mInflationObserver.onPostInflationStartup();
verify(mViewTreeObserver, times(2))
.addOnPreDrawListener(mOnPreDrawListenerArgumentCaptor.capture());
// No longer need to block draw.
SystemClock.setCurrentTimeMillis(INITIAL_TIME + 10);
mAppLaunchDrawBlocker.onIncognitoRestoreUnblockConditionsFired();
mAppLaunchDrawBlocker.onActiveTabAvailable(true);
for (OnPreDrawListener listener : mOnPreDrawListenerArgumentCaptor.getAllValues()) {
assertTrue("Listener shouldn't be blocking the draw any longer.", listener.onPreDraw());
verify(mViewTreeObserver, times(1)).removeOnPreDrawListener(listener);
}
verify(mIncognitoRestoreAppLaunchDrawBlockerMock, times(1)).shouldBlockDraw();
assertEquals(
"Duration not recorded.",
1,
RecordHistogram.getHistogramValueCountForTesting(
"Android.AppLaunch.DurationDrawWasBlocked.OnIncognitoReauth", 10));
}
private void validateConstructorAndCaptureObservers() {
verify(mActivityLifecycleDispatcher, times(2)).register(mLifecycleArgumentCaptor.capture());
List<LifecycleObserver> observerList = mLifecycleArgumentCaptor.getAllValues();
if (observerList.get(0) instanceof InflationObserver) {
mInflationObserver = (InflationObserver) observerList.get(0);
mStartStopWithNativeObserver = (StartStopWithNativeObserver) observerList.get(1);
} else {
mStartStopWithNativeObserver = (StartStopWithNativeObserver) observerList.get(0);
mInflationObserver = (InflationObserver) observerList.get(1);
}
assertNotNull("Did not register an InflationObserver", mInflationObserver);
assertNotNull(
"Did not register a StartStopWithNativeObserver", mStartStopWithNativeObserver);
}
private void setSearchEngineHasLogo(boolean hasLogo) {
ChromeSharedPreferences.getInstance()
.writeBoolean(ChromePreferenceKeys.APP_LAUNCH_SEARCH_ENGINE_HAD_LOGO, hasLogo);
when(mTemplateUrlService.doesDefaultSearchEngineHaveLogo()).thenReturn(hasLogo);
}
}