chromium/chrome/android/junit/src/org/chromium/chrome/browser/history/HistoryUITest.java

// Copyright 2017 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.history;

import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction;
import static androidx.test.espresso.matcher.ViewMatchers.assertThat;

import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.Shadows.shadowOf;

import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.os.Build.VERSION_CODES;
import android.provider.Browser;
import android.view.MenuItem;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;

import androidx.activity.OnBackPressedDispatcher;
import androidx.lifecycle.LifecycleOwner;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import androidx.test.espresso.intent.matcher.IntentMatchers;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.filters.SmallTest;

import org.hamcrest.Matcher;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;

import org.chromium.base.ThreadUtils;
import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.base.test.util.JniMocker;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.back_press.BackPressHelper;
import org.chromium.chrome.browser.back_press.SecondaryActivityBackPressUma.SecondaryActivity;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.history.AppFilterCoordinator.AppInfo;
import org.chromium.chrome.browser.history.HistoryManagerToolbar.InfoHeaderPref;
import org.chromium.chrome.browser.incognito.IncognitoUtils;
import org.chromium.chrome.browser.preferences.Pref;
import org.chromium.chrome.browser.preferences.PrefChangeRegistrar;
import org.chromium.chrome.browser.preferences.PrefChangeRegistrarJni;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
import org.chromium.chrome.browser.signin.services.IdentityServicesProvider;
import org.chromium.chrome.browser.signin.services.SigninManager;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.test.util.browser.signin.AccountManagerTestRule;
import org.chromium.components.browser_ui.widget.DateDividedAdapter;
import org.chromium.components.browser_ui.widget.MoreProgressButton;
import org.chromium.components.browser_ui.widget.selectable_list.SelectableItemView;
import org.chromium.components.browser_ui.widget.selectable_list.SelectableItemViewHolder;
import org.chromium.components.browser_ui.widget.selectable_list.SelectableListToolbar.NavigationButton;
import org.chromium.components.favicon.LargeIconBridge;
import org.chromium.components.favicon.LargeIconBridgeJni;
import org.chromium.components.prefs.PrefService;
import org.chromium.components.search_engines.TemplateUrlService;
import org.chromium.components.user_prefs.UserPrefs;
import org.chromium.components.user_prefs.UserPrefsJni;
import org.chromium.ui.base.PageTransition;
import org.chromium.ui.base.TestActivity;
import org.chromium.url.GURL;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;

/** Tests the History UI. */
@RunWith(BaseRobolectricTestRunner.class)
@DisableFeatures({ChromeFeatureList.APP_SPECIFIC_HISTORY})
public class HistoryUITest {
    private static final int PAGE_INCREMENT = 2;
    private static final String HISTORY_SEARCH_QUERY = "some page";

    @Rule public AccountManagerTestRule mAccountManagerTestRule = new AccountManagerTestRule();
    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
    @Rule public JniMocker mJniMocker = new JniMocker();

    @Rule
    public ActivityScenarioRule<TestActivity> mActivityScenarioRule =
            new ActivityScenarioRule<>(TestActivity.class);

    private StubbedHistoryProvider mHistoryProvider;
    private HistoryAdapter mAdapter;
    private HistoryManager mHistoryManager;
    private HistoryContentManager mContentManager;
    private RecyclerView mRecyclerView;
    private Activity mActivity;

    private HistoryItem mItem1;
    private HistoryItem mItem2;
    private int mHeight;
    private OnBackPressedDispatcher mOnBackPressedDispatcher;
    private LifecycleOwner mLifecycleOwner;

    @Mock private SnackbarManager mSnackbarManager;
    @Mock private Profile mProfile;
    @Mock LargeIconBridge.Natives mMockLargeIconBridgeJni;
    @Mock private UserPrefs.Natives mUserPrefsJni;
    @Mock private PrefService mPrefService;
    @Mock private IdentityServicesProvider mIdentityService;
    @Mock private SigninManager mSigninManager;
    @Mock private PrefChangeRegistrar.Natives mPrefChangeRegistrarJni;
    @Mock private TemplateUrlService mTemplateUrlService;
    @Mock private HistoryAdapter mMockAdapter;
    @Mock private PackageManager mPackageManager;
    @Mock private AppFilterCoordinator mAppFilterSheet;
    @Mock private ApplicationInfo mPackageAppInfo;

    public static Matcher<Intent> hasData(GURL uri) {
        return IntentMatchers.hasData(uri.getSpec());
    }

    @Before
    public void setUp() throws Exception {
        mHistoryProvider = new StubbedHistoryProvider();
        long timestamp = new Date().getTime();
        mItem1 = StubbedHistoryProvider.createHistoryItem(0, timestamp);
        mItem2 = StubbedHistoryProvider.createHistoryItem(1, timestamp);
        mHistoryProvider.addItem(mItem1);
        mHistoryProvider.addItem(mItem2);

        ProfileManager.setLastUsedProfileForTesting(mProfile);

        mJniMocker.mock(LargeIconBridgeJni.TEST_HOOKS, mMockLargeIconBridgeJni);
        doReturn(1L).when(mMockLargeIconBridgeJni).init();
        mJniMocker.mock(UserPrefsJni.TEST_HOOKS, mUserPrefsJni);
        doReturn(mPrefService).when(mUserPrefsJni).get(mProfile);
        doReturn(true).when(mPrefService).getBoolean(Pref.ALLOW_DELETING_BROWSER_HISTORY);
        doReturn(true).when(mPrefService).getBoolean(HistoryManager.HISTORY_CLUSTERS_VISIBLE_PREF);
        IdentityServicesProvider.setInstanceForTests(mIdentityService);
        doReturn(mSigninManager).when(mIdentityService).getSigninManager(mProfile);
        mJniMocker.mock(PrefChangeRegistrarJni.TEST_HOOKS, mPrefChangeRegistrarJni);
        IncognitoUtils.setEnabledForTesting(true);
        TemplateUrlServiceFactory.setInstanceForTesting(mTemplateUrlService);
        mActivityScenarioRule
                .getScenario()
                .onActivity(
                        activity -> {
                            mActivity = activity;
                            mOnBackPressedDispatcher = activity.getOnBackPressedDispatcher();
                            mLifecycleOwner = activity;
                        });
        boolean isAppSpecificHistoryEnabled =
                ChromeFeatureList.isEnabled(ChromeFeatureList.APP_SPECIFIC_HISTORY);
        mHistoryManager =
                new HistoryManager(
                        mActivity,
                        true,
                        mSnackbarManager,
                        mProfile,
                        /* bottomSheetController= */ null,
                        /* Supplier<Tab>= */ null,
                        mHistoryProvider,
                        new HistoryUmaRecorder(),
                        /* clientPackageName= */ null,
                        /* shouldShowClearData= */ true,
                        /* launchedForApp= */ false,
                        /* showAppFilter= */ isAppSpecificHistoryEnabled);
        mContentManager = mHistoryManager.getContentManagerForTests();
        mAdapter = mContentManager.getAdapter();
        mRecyclerView = mContentManager.getRecyclerView();

        // Layout the recycler view with ample height so that we can measure how much height it
        // needs to fully display its initial set of items.
        mRecyclerView.measure(0, 0);
        mRecyclerView.layout(0, 0, 600, 1000);
        // Constrain the recycler view to only the height it needs and lay it out again.
        mHeight = mRecyclerView.getMeasuredHeight();
        layoutRecyclerView();

        // App-specific history always enables the privacy disclaimer header item.
        int expectedItemCount = 4 + (isAppSpecificHistoryEnabled ? 1 : 0);

        Assert.assertEquals(expectedItemCount, mAdapter.getItemCount());

        BackPressHelper.create(
                mLifecycleOwner,
                mOnBackPressedDispatcher,
                mHistoryManager,
                SecondaryActivity.HISTORY);
    }

    @Test
    @SmallTest
    public void testRemove_SingleItem() throws Exception {
        final HistoryItemView itemView = (HistoryItemView) getItemView(2);

        itemView.getRemoveButtonForTests().performClick();

        // Check that one item was removed.
        ShadowLooper.idleMainLooper();
        Assert.assertEquals(1, mHistoryProvider.markItemForRemovalCallback.getCallCount());
        Assert.assertEquals(1, mHistoryProvider.removeItemsCallback.getCallCount());
        Assert.assertEquals(3, mAdapter.getItemCount());
        Assert.assertEquals(View.VISIBLE, mRecyclerView.getVisibility());
        Assert.assertEquals(View.GONE, mHistoryManager.getEmptyViewForTests().getVisibility());
    }

    @Test
    @SmallTest
    public void testRemove_AllItems() throws Exception {
        toggleItemSelection(2);
        toggleItemSelection(3);

        performMenuAction(R.id.selection_mode_delete_menu_id);

        // Check that all items were removed. The onChangedCallback should be called three times -
        // once for each item that is being removed and once for the removal of the header.
        Assert.assertEquals(0, mAdapter.getItemCount());
        Assert.assertEquals(2, mHistoryProvider.markItemForRemovalCallback.getCallCount());
        Assert.assertEquals(1, mHistoryProvider.removeItemsCallback.getCallCount());
        Assert.assertFalse(mHistoryManager.getSelectionDelegateForTests().isSelectionEnabled());
        Assert.assertEquals(View.GONE, mRecyclerView.getVisibility());
        Assert.assertEquals(View.VISIBLE, mHistoryManager.getEmptyViewForTests().getVisibility());
    }

    @Test
    @SmallTest
    public void testPrivacyDisclaimers_SignedOut() {
        // The user is signed out by default.
        Assert.assertEquals(1, mAdapter.getFirstGroupForTests().size());
    }

    @Test
    @SmallTest
    @EnableFeatures(ChromeFeatureList.APP_SPECIFIC_HISTORY)
    @Config(sdk = VERSION_CODES.UPSIDE_DOWN_CAKE)
    public void testPrivacyDisclaimers_SignedOut_Ash() {
        // With ASH enabled, the header for disclaimer text is always visible.
        Assert.assertEquals(2, mAdapter.getFirstGroupForTests().size());
    }

    @Test
    @SmallTest
    public void testPrivacyDisclaimers_SignedIn() {
        mAccountManagerTestRule.addAccount(AccountManagerTestRule.TEST_ACCOUNT_EMAIL);

        setHasOtherFormsOfBrowsingData(false);

        Assert.assertEquals(1, mAdapter.getFirstGroupForTests().size());
    }

    @Test
    @SmallTest
    public void testPrivacyDisclaimers_SignedInSynced() {
        mAccountManagerTestRule.addAccount(AccountManagerTestRule.TEST_ACCOUNT_EMAIL);

        setHasOtherFormsOfBrowsingData(false);

        Assert.assertEquals(1, mAdapter.getFirstGroupForTests().size());
    }

    @Test
    @SmallTest
    public void testPrivacyDisclaimers_SignedInSyncedAndOtherForms() {
        mAccountManagerTestRule.addAccount(AccountManagerTestRule.TEST_ACCOUNT_EMAIL);

        setHasOtherFormsOfBrowsingData(true);

        Assert.assertEquals(2, mAdapter.getFirstGroupForTests().size());
    }

    @Test
    @SmallTest
    public void testOpenItem() throws Exception {
        clickItem(2);
        assertThat(
                shadowOf(mActivity).peekNextStartedActivity(),
                allOf(hasAction(equalTo(Intent.ACTION_VIEW)), hasData(mItem1.getUrl())));
    }

    @Test
    @SmallTest
    public void testOpenSelectedItems() throws Exception {
        toggleItemSelection(2);
        toggleItemSelection(3);

        performMenuAction(R.id.selection_mode_open_in_incognito);
        Intent intent = shadowOf(mActivity).getNextStartedActivity();

        assertThat(intent, hasData(mItem1.getUrl()));
        Assert.assertEquals(
                intent.getSerializableExtra(IntentHandler.EXTRA_ADDITIONAL_URLS),
                Arrays.asList(mItem2.getUrl().getSpec()));
    }

    @Test
    @SmallTest
    public void testOpenItemIntent() {
        Intent intent =
                mHistoryManager
                        .getContentManagerForTests()
                        .getOpenUrlIntent(mItem1.getUrl(), null, false);
        Assert.assertEquals(mItem1.getUrl().getSpec(), intent.getDataString());
        Assert.assertFalse(intent.hasExtra(IntentHandler.EXTRA_OPEN_NEW_INCOGNITO_TAB));
        Assert.assertFalse(intent.hasExtra(Browser.EXTRA_CREATE_NEW_TAB));
        Assert.assertEquals(
                PageTransition.AUTO_BOOKMARK,
                intent.getIntExtra(IntentHandler.EXTRA_PAGE_TRANSITION_TYPE, -1));

        intent =
                mHistoryManager
                        .getContentManagerForTests()
                        .getOpenUrlIntent(mItem2.getUrl(), true, true);
        Assert.assertEquals(mItem2.getUrl().getSpec(), intent.getDataString());
        Assert.assertTrue(
                intent.getBooleanExtra(IntentHandler.EXTRA_OPEN_NEW_INCOGNITO_TAB, false));
        Assert.assertTrue(intent.getBooleanExtra(Browser.EXTRA_CREATE_NEW_TAB, false));
        Assert.assertEquals(
                PageTransition.AUTO_BOOKMARK,
                intent.getIntExtra(IntentHandler.EXTRA_PAGE_TRANSITION_TYPE, -1));
    }

    @Test
    @SmallTest
    public void testOnHistoryDeleted() throws Exception {
        toggleItemSelection(2);

        mHistoryProvider.removeItem(mItem1);

        mAdapter.onHistoryDeleted();

        // The selection should be cleared and the items in the adapter should be reloaded.
        Assert.assertFalse(mHistoryManager.getSelectionDelegateForTests().isSelectionEnabled());
        Assert.assertEquals(3, mAdapter.getItemCount());
    }

    @Test
    @SmallTest
    public void testSupervisedUser() {
        final HistoryManagerToolbar toolbar = mHistoryManager.getToolbarForTests();
        final HistoryItemView item = (HistoryItemView) getItemView(2);
        View itemRemoveButton = item.getRemoveButtonForTests();

        // First check the behaviour for non-supervised users.

        // The item's remove button is visible when there is no selection.
        Assert.assertEquals(View.VISIBLE, itemRemoveButton.getVisibility());

        toggleItemSelection(2);
        Assert.assertTrue(toolbar.getItemById(R.id.selection_mode_open_in_incognito).isVisible());
        Assert.assertTrue(toolbar.getItemById(R.id.selection_mode_open_in_incognito).isEnabled());
        Assert.assertTrue(toolbar.getItemById(R.id.selection_mode_delete_menu_id).isVisible());
        Assert.assertTrue(toolbar.getItemById(R.id.selection_mode_delete_menu_id).isEnabled());
        // The item's remove button is invisible for non-supervised users when there is a selection.
        Assert.assertEquals(View.INVISIBLE, item.getRemoveButtonForTests().getVisibility());

        // Turn selection off and check the remove button is visible.
        toggleItemSelection(2);
        Assert.assertFalse(mHistoryManager.getSelectionDelegateForTests().isSelectionEnabled());
        Assert.assertEquals(View.VISIBLE, item.getRemoveButtonForTests().getVisibility());

        // Now check the behaviour for supervised users.
        signInToSupervisedAccount();

        // The item's remove button remains visible when there is no selection.
        Assert.assertEquals(View.VISIBLE, itemRemoveButton.getVisibility());

        // Incognito is hidden.
        toggleItemSelection(2);
        Assert.assertNull(toolbar.getItemById(R.id.selection_mode_open_in_incognito));

        // History deletion behaviour is unchanged from the non-supervised case.
        Assert.assertTrue(toolbar.getItemById(R.id.selection_mode_delete_menu_id).isVisible());
        Assert.assertTrue(toolbar.getItemById(R.id.selection_mode_delete_menu_id).isEnabled());
        Assert.assertTrue(mHistoryManager.getSelectionDelegateForTests().isSelectionEnabled());
        Assert.assertEquals(View.INVISIBLE, item.getRemoveButtonForTests().getVisibility());

        // Make sure selection is no longer enabled.
        toggleItemSelection(2);
        Assert.assertFalse(mHistoryManager.getSelectionDelegateForTests().isSelectionEnabled());
        Assert.assertEquals(View.VISIBLE, item.getRemoveButtonForTests().getVisibility());

        signOut();
    }

    @Test
    @SmallTest
    public void testToolbarShadow() throws Exception {
        View toolbarShadow = mHistoryManager.getSelectableListLayout().getToolbarShadowForTests();
        Assert.assertEquals(View.GONE, toolbarShadow.getVisibility());

        toggleItemSelection(2);
        Assert.assertTrue(mHistoryManager.getSelectionDelegateForTests().isSelectionEnabled());
        Assert.assertEquals(View.GONE, toolbarShadow.getVisibility());

        toggleItemSelection(2);
        Assert.assertFalse(mHistoryManager.getSelectionDelegateForTests().isSelectionEnabled());
        Assert.assertEquals(View.GONE, toolbarShadow.getVisibility());
    }

    @Test
    @SmallTest
    public void testSearchView() throws Exception {
        final HistoryManagerToolbar toolbar = mHistoryManager.getToolbarForTests();
        View toolbarShadow = mHistoryManager.getSelectableListLayout().getToolbarShadowForTests();
        View toolbarSearchView = toolbar.getSearchViewForTests();
        Assert.assertEquals(View.GONE, toolbarShadow.getVisibility());
        Assert.assertEquals(View.GONE, toolbarSearchView.getVisibility());

        toggleItemSelection(2);
        Assert.assertTrue(mHistoryManager.getSelectionDelegateForTests().isSelectionEnabled());

        performMenuAction(R.id.search_menu_id);

        // The selection should be cleared when a search is started.
        Assert.assertFalse(mHistoryManager.getSelectionDelegateForTests().isSelectionEnabled());
        Assert.assertEquals(View.GONE, toolbarShadow.getVisibility());
        Assert.assertEquals(View.VISIBLE, toolbarSearchView.getVisibility());

        // Select an item and assert that the search view is no longer showing.
        toggleItemSelection(2);
        Assert.assertTrue(mHistoryManager.getSelectionDelegateForTests().isSelectionEnabled());
        Assert.assertEquals(View.GONE, toolbarShadow.getVisibility());
        Assert.assertEquals(View.GONE, toolbarSearchView.getVisibility());

        // Clear the selection and assert that the search view is showing again.
        toggleItemSelection(2);
        Assert.assertFalse(mHistoryManager.getSelectionDelegateForTests().isSelectionEnabled());
        Assert.assertEquals(View.GONE, toolbarShadow.getVisibility());
        Assert.assertEquals(View.VISIBLE, toolbarSearchView.getVisibility());

        // Close the search view.
        Assert.assertTrue(mHistoryManager.getHandleBackPressChangedSupplier().get());
        toolbar.onSearchNavigationBack();
        Assert.assertEquals(View.GONE, toolbarShadow.getVisibility());
        Assert.assertEquals(View.GONE, toolbarSearchView.getVisibility());
    }

    @EnableFeatures(ChromeFeatureList.APP_SPECIFIC_HISTORY)
    @Config(sdk = VERSION_CODES.UPSIDE_DOWN_CAKE)
    @Test
    @SmallTest
    public void testSearch_AppFilterChipVisible() {
        mAdapter.setClearBrowsingDataButtonVisibilityForTest(false);
        DateDividedAdapter.ItemGroup headerGroup = mAdapter.getFirstGroupForTests();
        // Disclaimer header should be present.
        Assert.assertEquals(1, headerGroup.size());
        performMenuAction(R.id.search_menu_id);
        // Now only the header for 'Filter by app' chip is visible.
        Assert.assertEquals(1, headerGroup.size());
    }

    @EnableFeatures(ChromeFeatureList.APP_SPECIFIC_HISTORY)
    @Config(sdk = VERSION_CODES.UPSIDE_DOWN_CAKE)
    @Test
    @SmallTest
    public void testSearch_AppFilterChipEnabledWithNonEmptyAppResult() throws Exception {
        Assert.assertTrue(mHistoryProvider.isQueryAppsTriggered());
        mAdapter.setClearBrowsingDataButtonVisibilityForTest(false);
        mAdapter.setPrivacyDisclaimer();
        mContentManager.setPackageManagerForTesting(mPackageManager);
        performMenuAction(R.id.search_menu_id);

        // Verify the button starts disabled.
        Assert.assertFalse(isAppFilterButtonEnabled());

        // Verify the button remains disabled if the query app result is empty.
        var result = new ArrayList<String>();
        mAdapter.onQueryAppsComplete(result);
        Assert.assertFalse(isAppFilterButtonEnabled());

        // Verify the button becomes enabled if the app result is non-empty.
        final String app1 = "org.chromium.chrome.Ernie";
        final String app2 = "org.chromium.chrome.Bert";
        result.add(app1);
        result.add(app2);
        when(mPackageManager.getApplicationInfo(eq(app1), anyInt())).thenReturn(mPackageAppInfo);
        when(mPackageManager.getApplicationInfo(eq(app2), anyInt()))
                .thenThrow(NameNotFoundException.class);
        mAdapter.onQueryAppsComplete(result);
        Assert.assertTrue(isAppFilterButtonEnabled());
    }

    private boolean isAppFilterButtonEnabled() {
        return mAdapter.hasListHeader() && mAdapter.getAppFilterButtonForTest().isEnabled();
    }

    @EnableFeatures(ChromeFeatureList.APP_SPECIFIC_HISTORY)
    @Config(sdk = VERSION_CODES.UPSIDE_DOWN_CAKE)
    @Test
    @SmallTest
    public void testSearch_AppFilterSheet() {
        mContentManager.setPackageManagerForTesting(mPackageManager);
        mContentManager.setAppFilterSheetForTesting(mAppFilterSheet);

        performMenuAction(R.id.search_menu_id);

        var result = new ArrayList<String>();
        String appId1 = "org.chromium.chrome.ernie";
        String appId2 = "org.chromium.chrome.bert";
        result.add(appId1);
        result.add(appId2);
        mAdapter.onQueryAppsComplete(result);

        mContentManager.onAppFilterClicked();
        verify(mAppFilterSheet).openSheet(eq(null));

        // Verify ContentManager is updated with the selected app info.
        AppInfo selected = new AppInfo("Ernie", null, appId1);
        mContentManager.onAppUpdated(selected);
        Assert.assertEquals(
                "The expected app 'Ernie' was not chosen",
                mContentManager.getAppInfoForTesting(),
                selected);

        AppInfo selected2 = new AppInfo("Bert", null, appId2);
        mContentManager.onAppUpdated(selected2);
        Assert.assertEquals(
                "The expected app 'Bert' was not chosen",
                mContentManager.getAppInfoForTesting(),
                selected2);

        // Revert to full history.
        mContentManager.onAppUpdated(null);
        Assert.assertEquals(
                "The history was not reverted to full",
                mContentManager.getAppInfoForTesting(),
                null);
    }

    @EnableFeatures(ChromeFeatureList.APP_SPECIFIC_HISTORY)
    @Config(sdk = VERSION_CODES.UPSIDE_DOWN_CAKE)
    @Test
    @SmallTest
    public void testAppInfoCache() throws Exception {
        var appInfoCache = mContentManager.getAppInfoCache();
        appInfoCache.setPackageManagerForTesting(mPackageManager);
        final String app1 = "org.chromium.chrome.AwesomeApp";
        when(mPackageManager.getApplicationInfo(eq(app1), anyInt())).thenReturn(mPackageAppInfo);

        AppInfo appInfo1 = appInfoCache.get(app1);
        verify(mPackageManager).getApplicationInfo(eq(app1), anyInt());
        clearInvocations(mPackageManager);

        // Get the info for the same app -> verify the cached item is returned, without
        // calling the system API again.
        Assert.assertEquals(appInfo1, appInfoCache.get(app1));
        verify(mPackageManager, never()).getApplicationInfo(eq(app1), anyInt());
        clearInvocations(mPackageManager);

        // Verify that a call with a non-existent app ID returns null.
        final String app2 = "org.chromium.chrome.UninstalledApp";
        when(mPackageManager.getApplicationInfo(eq(app2), anyInt()))
                .thenThrow(NameNotFoundException.class);
        AppInfo appInfo2 = appInfoCache.get(app2);
        Assert.assertFalse("Bad appId should return invalid AppInfo", appInfo2.isValid());
        clearInvocations(mPackageManager);

        // Verify that a call with the same non-exisitent app ID won't invoke the system API again.
        appInfo2 = appInfoCache.get(app2);
        verify(mPackageManager, never()).getApplicationInfo(eq(app2), anyInt());
        Assert.assertFalse("Bad appID should return invalid AppInfo again", appInfo2.isValid());
    }

    @Test
    @SmallTest
    public void testSearchViewDismissedByBackPress() {
        final HistoryManagerToolbar toolbar = mHistoryManager.getToolbarForTests();
        View toolbarShadow = mHistoryManager.getSelectableListLayout().getToolbarShadowForTests();
        View toolbarSearchView = toolbar.getSearchViewForTests();
        Assert.assertEquals(View.GONE, toolbarShadow.getVisibility());
        Assert.assertEquals(View.GONE, toolbarSearchView.getVisibility());

        performMenuAction(R.id.search_menu_id);

        // Select an item and assert that the search view is still not showing.
        toggleItemSelection(2);
        Assert.assertTrue(mHistoryManager.getSelectionDelegateForTests().isSelectionEnabled());
        Assert.assertEquals(View.GONE, toolbarShadow.getVisibility());
        Assert.assertEquals(View.GONE, toolbarSearchView.getVisibility());

        // Press back press to unselect item and the search view is showing again.
        var backPressRecorder =
                HistogramWatcher.newSingleRecordWatcher(
                        "Android.BackPress.SecondaryActivity", SecondaryActivity.HISTORY);
        Assert.assertTrue(mHistoryManager.getHandleBackPressChangedSupplier().get());
        ThreadUtils.runOnUiThreadBlocking(mOnBackPressedDispatcher::onBackPressed);
        Assert.assertFalse(mHistoryManager.getSelectionDelegateForTests().isSelectionEnabled());
        Assert.assertEquals(View.GONE, toolbarShadow.getVisibility());
        Assert.assertEquals(View.VISIBLE, toolbarSearchView.getVisibility());
        backPressRecorder.assertExpected();

        // Press back to close the search view.
        var backPressRecorder2 =
                HistogramWatcher.newSingleRecordWatcher(
                        "Android.BackPress.SecondaryActivity", SecondaryActivity.HISTORY);
        Assert.assertTrue(mHistoryManager.getHandleBackPressChangedSupplier().get());
        ThreadUtils.runOnUiThreadBlocking(mOnBackPressedDispatcher::onBackPressed);
        Assert.assertEquals(View.GONE, toolbarShadow.getVisibility());
        Assert.assertEquals(View.GONE, toolbarSearchView.getVisibility());
        backPressRecorder2.assertExpected();
    }

    @Test
    @SmallTest
    public void testToggleInfoMenuItem() {
        final HistoryManagerToolbar toolbar = mHistoryManager.getToolbarForTests();
        final MenuItem infoMenuItem = toolbar.getItemById(R.id.info_menu_id);

        // Not signed in
        Assert.assertFalse(infoMenuItem.isVisible());
        DateDividedAdapter.ItemGroup headerGroup = mAdapter.getFirstGroupForTests();
        Assert.assertTrue(mAdapter.hasListHeader());
        Assert.assertEquals(1, headerGroup.size());

        // Signed in but not synced and history has items. The info button should be hidden.
        mAccountManagerTestRule.addAccount(AccountManagerTestRule.TEST_ACCOUNT_EMAIL);
        setHasOtherFormsOfBrowsingData(false);
        toolbar.onSignInStateChange();
        Assert.assertFalse(infoMenuItem.isVisible());

        // Signed in, synced, has other forms and has items
        // Privacy disclaimers should be shown by default
        setHasOtherFormsOfBrowsingData(true);
        Assert.assertTrue(infoMenuItem.isVisible());
        headerGroup = mAdapter.getFirstGroupForTests();
        Assert.assertTrue(mAdapter.hasListHeader());
        Assert.assertEquals(2, headerGroup.size());

        // Toggle Info Menu Item to off
        mHistoryManager.onMenuItemClick(infoMenuItem);
        headerGroup = mAdapter.getFirstGroupForTests();
        Assert.assertTrue(mAdapter.hasListHeader());
        Assert.assertEquals(1, headerGroup.size());

        // Toggle Info Menu Item to on
        mHistoryManager.onMenuItemClick(infoMenuItem);
        headerGroup = mAdapter.getFirstGroupForTests();
        Assert.assertTrue(mAdapter.hasListHeader());
        Assert.assertEquals(2, headerGroup.size());
    }

    @Test
    @SmallTest
    public void testInfoIcon_OtherFormsOfBrowsingData() {
        final HistoryManagerToolbar toolbar = mHistoryManager.getToolbarForTests();
        final MenuItem infoMenuItem = toolbar.getItemById(R.id.info_menu_id);
        setHasOtherFormsOfBrowsingData(true);
        Assert.assertTrue("Info icon should be visible.", infoMenuItem.isVisible());

        // Hide disclaimers to simulate setup for https://crbug.com/1071468.
        mHistoryManager.onMenuItemClick(infoMenuItem);
        Assert.assertFalse(
                "Privacy disclaimers should be hidden.",
                mHistoryManager
                        .getContentManagerForTests()
                        .getShouldShowPrivacyDisclaimersIfAvailable());

        // Simulate call indicating there are not other forms of browsing data.
        setHasOtherFormsOfBrowsingData(false);
        layoutRecyclerView();
        Assert.assertFalse("Info menu item should be hidden.", infoMenuItem.isVisible());

        // Simulate call indicating there are other forms of browsing data.
        setHasOtherFormsOfBrowsingData(true);
        Assert.assertTrue("Info menu item should bre visible.", infoMenuItem.isVisible());
    }

    @Test
    @SmallTest
    public void testInfoHeaderInSearchMode() {
        final HistoryManagerToolbar toolbar = mHistoryManager.getToolbarForTests();
        final MenuItem infoMenuItem = toolbar.getItemById(R.id.info_menu_id);

        // Sign in and set has other forms of browsing data to true.
        mAccountManagerTestRule.addAccount(AccountManagerTestRule.TEST_ACCOUNT_EMAIL);
        setHasOtherFormsOfBrowsingData(true);

        toolbar.onSignInStateChange();
        mAdapter.onSignInStateChange();

        ShadowLooper.idleMainLooper();
        DateDividedAdapter.ItemGroup firstGroup = mAdapter.getFirstGroupForTests();
        Assert.assertTrue(infoMenuItem.isVisible());
        Assert.assertTrue(mAdapter.hasListHeader());
        Assert.assertEquals(2, firstGroup.size());

        // Enter search mode
        performMenuAction(R.id.search_menu_id);

        ShadowLooper.idleMainLooper();
        firstGroup = mAdapter.getFirstGroupForTests();
        Assert.assertFalse(infoMenuItem.isVisible());
        // The first group should be the history item group from SetUp()
        Assert.assertFalse(mAdapter.hasListHeader());
        Assert.assertEquals(3, firstGroup.size());
    }

    @Test
    @SmallTest
    public void testSearch_NotFound() {
        final HistoryManagerToolbar toolbar = mHistoryManager.getToolbarForTests();

        // Enter search mode
        performMenuAction(R.id.search_menu_id);
        EditText searchText = toolbar.findViewById(R.id.search_text);
        searchText.setText(HISTORY_SEARCH_QUERY);
        layoutRecyclerView();

        TextView emptyText =
                mHistoryManager.getSelectableListLayout().findViewById(R.id.empty_state_text_title);
        assertTrue(emptyText.getText().toString().startsWith("Can’t find that page."));
        assertNotNull(
                mHistoryManager.getSelectableListLayout().findViewById(R.id.empty_state_icon));
    }

    @Test
    @SmallTest
    public void testAppSpecificToolbar() throws Exception {
        final String appId = "org.chromium.app.AwesomeApp";
        when(mPackageManager.getApplicationInfo(eq(appId), anyInt())).thenReturn(mPackageAppInfo);
        mHistoryManager =
                new HistoryManager(
                        mActivity,
                        true,
                        mSnackbarManager,
                        mProfile,
                        /* bottomSheetController= */ null,
                        /* Supplier<Tab>= */ null,
                        mHistoryProvider,
                        new HistoryUmaRecorder(),
                        appId,
                        true,
                        true,
                        false);
        final HistoryManagerToolbar toolbar = mHistoryManager.getToolbarForTests();
        Assert.assertNull(toolbar.getItemById(R.id.close_menu_id));
        Assert.assertEquals(
                toolbar.getNavigationButtonForTests(), NavigationButton.NORMAL_VIEW_BACK);

        Resources res = mActivity.getResources();
        Assert.assertEquals(
                "Open in new Chrome text menu is wrong",
                toolbar.getItemById(R.id.selection_mode_open_in_new_tab).getTitle(),
                res.getString(R.string.history_open_in_chrome));
        Assert.assertEquals(
                "Open in new Incognito Chrome menu text is wrong",
                toolbar.getItemById(R.id.selection_mode_open_in_incognito).getTitle(),
                res.getString(R.string.history_open_in_incognito_chrome));
    }

    @Test
    @SmallTest
    public void testAppSpecificToolbarHeaderStateNotPersisted() throws Exception {
        final String appId = "org.chromium.app.AwesomeApp";
        when(mPackageManager.getApplicationInfo(eq(appId), anyInt())).thenReturn(mPackageAppInfo);
        mHistoryManager =
                new HistoryManager(
                        mActivity,
                        true,
                        mSnackbarManager,
                        mProfile,
                        /* bottomSheetController= */ null,
                        /* Supplier<Tab>= */ null,
                        mHistoryProvider,
                        new HistoryUmaRecorder(),
                        appId,
                        true,
                        true,
                        false);
        InfoHeaderPref headerPref = mHistoryManager.getInfoHeaderPrefForTests();
        Assert.assertFalse(headerPref.isVisible());
        HistoryManagerToolbar toolbar = mHistoryManager.getToolbarForTests();
        MenuItem infoMenuItem = toolbar.getItemById(R.id.info_menu_id);
        mHistoryManager.onMenuItemClick(infoMenuItem);

        // Verify that the toggled state is not persisted to preference storage.
        Assert.assertFalse(headerPref.isVisible());
    }

    @Test
    @SmallTest
    public void testInvisibleHeader() {
        Assert.assertTrue(mAdapter.hasListHeader());

        // Not sign in and set clear browsing data button to invisible
        mAdapter.setClearBrowsingDataButtonVisibilityForTest(false);
        mAdapter.setPrivacyDisclaimer();

        DateDividedAdapter.ItemGroup firstGroup = mAdapter.getFirstGroupForTests();
        Assert.assertFalse(mAdapter.hasListHeader());
        Assert.assertEquals(3, firstGroup.size());
    }

    @Test
    @SmallTest
    @Ignore // See https://crbug.com/1358628
    public void testCopyLink() {
        final ClipboardManager clipboardManager =
                (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
        Assert.assertNotNull(clipboardManager);
        clipboardManager.setPrimaryClip(ClipData.newPlainText(null, ""));
        // Clear the clipboard to make sure we start with a clean state.

        final HistoryManagerToolbar toolbar = mHistoryManager.getToolbarForTests();

        // Check that the copy link item is visible when one item is selected.
        toggleItemSelection(2);
        Assert.assertTrue(toolbar.getItemById(R.id.selection_mode_copy_link).isVisible());

        // Check that link is copied to the clipboard.
        performMenuAction(R.id.selection_mode_copy_link);
        Assert.assertEquals(mItem1.getUrl().getSpec(), clipboardManager.getText());

        // Check that the copy link item is not visible when more than one item is selected.
        toggleItemSelection(2);
        toggleItemSelection(3);
        Assert.assertFalse(toolbar.getItemById(R.id.selection_mode_copy_link).isVisible());
    }

    @Test
    @SmallTest
    public void testScrollToLoadEnabled() {
        // Reduce the height available to the recycler view to less than it needs so that scrolling
        // has an effect.
        mHeight--;
        layoutRecyclerView();

        mHistoryProvider.setPaging(PAGE_INCREMENT);
        long timestamp = new Date().getTime();
        mHistoryProvider.addItem(StubbedHistoryProvider.createHistoryItem(2, --timestamp));
        mHistoryProvider.addItem(StubbedHistoryProvider.createHistoryItem(3, --timestamp));
        mHistoryProvider.addItem(StubbedHistoryProvider.createHistoryItem(4, --timestamp));
        mHistoryProvider.addItem(StubbedHistoryProvider.createHistoryItem(5, --timestamp));
        int itemCount = mAdapter.getItemCount();
        // Trigger a reload of items so that the adapter sees that there are now more to load.
        mAdapter.startLoadingItems();

        scrollRecyclerViewToBottom();

        Assert.assertEquals(
                PAGE_INCREMENT + " more Items should be loaded",
                mAdapter.getItemCount(),
                itemCount + PAGE_INCREMENT);
        itemCount = mAdapter.getItemCount();

        scrollRecyclerViewToBottom();
        Assert.assertEquals(
                PAGE_INCREMENT + " more Items should be loaded",
                mAdapter.getItemCount(),
                itemCount + PAGE_INCREMENT);
    }

    @Test
    @SmallTest
    public void testScrollToLoadDisabled() throws Exception {
        mHistoryProvider.setPaging(PAGE_INCREMENT);
        HistoryContentManager.setScrollToLoadDisabledForTesting(true);
        mHistoryProvider.addItem(StubbedHistoryProvider.createHistoryItem(2, new Date().getTime()));
        mHistoryProvider.addItem(
                StubbedHistoryProvider.createHistoryItem(3, new Date().getTime() - 1));
        mHistoryProvider.addItem(
                StubbedHistoryProvider.createHistoryItem(4, new Date().getTime() - 2));
        mAdapter.startLoadingItems();
        int itemCount = mAdapter.getItemCount();
        scrollRecyclerViewToBottom();

        Assert.assertEquals(
                "Should not load more items into view after scroll",
                mAdapter.getItemCount(),
                itemCount);
        Assert.assertTrue(
                "Footer should be added to the end of the view", mAdapter.hasListFooter());
        Assert.assertEquals(
                "Footer group should contain one item", 1, mAdapter.getLastGroupForTests().size());

        // Verify the button is correctly displayed
        DateDividedAdapter.TimedItem item = mAdapter.getLastGroupForTests().getItemAt(0);
        MoreProgressButton button =
                (MoreProgressButton) ((DateDividedAdapter.FooterItem) item).getView();
        Assert.assertSame(
                "FooterItem view should be MoreProgressButton",
                mAdapter.getMoreProgressButtonForTest(),
                button);
        Assert.assertEquals(
                "State for the MPB should be button",
                button.getStateForTest(),
                MoreProgressButton.State.BUTTON);

        // Test click, should load more items
        button.findViewById(R.id.action_button).performClick();

        Assert.assertEquals(
                (PAGE_INCREMENT) + " more Items should be loaded",
                mAdapter.getItemCount(),
                itemCount + PAGE_INCREMENT);
    }

    private void toggleItemSelection(int position) {
        final SelectableItemView<HistoryItem> itemView = getItemView(position);
        itemView.performLongClick();
        layoutRecyclerView();
    }

    private void clickItem(int position) {
        getItemView(position).performClick();
    }

    @SuppressWarnings("unchecked")
    private SelectableItemView<HistoryItem> getItemView(int position) {
        ViewHolder mostRecentHolder = mRecyclerView.findViewHolderForAdapterPosition(position);
        Assert.assertTrue(
                mostRecentHolder + " should be instance of SelectableItemViewHolder",
                mostRecentHolder instanceof SelectableItemViewHolder);
        return ((SelectableItemViewHolder<HistoryItem>) mostRecentHolder).getItemView();
    }

    private void setHasOtherFormsOfBrowsingData(final boolean hasOtherForms) {
        mAdapter.hasOtherFormsOfBrowsingData(hasOtherForms);
    }

    private void signInToSupervisedAccount() {
        // Sign in to account. Note that if supervised user is set before sign in, the supervised
        // user setting will be reset.
        mAccountManagerTestRule.addAccount(AccountManagerTestRule.TEST_ACCOUNT_EMAIL);
        doReturn(true).when(mProfile).isChild();
        doReturn("ChildAccountSUID").when(mPrefService).getString(Pref.SUPERVISED_USER_ID);
        IncognitoUtils.setEnabledForTesting(false);
        mHistoryManager.getContentManagerForTests().onPreferenceChange();
        layoutRecyclerView();
    }

    private void signOut() {
        // Clear supervised user id.
        doReturn("").when(mPrefService).getString(Pref.SUPERVISED_USER_ID);
        mAccountManagerTestRule.removeAccount(AccountManagerTestRule.TEST_ACCOUNT_1.getId());
    }

    private void performMenuAction(int menuItemId) {
        mHistoryManager.getToolbarForTests().getMenu().performIdentifierAction(menuItemId, 0);
        layoutRecyclerView();
    }

    private void layoutRecyclerView() {
        mRecyclerView.measure(0, 0);
        mRecyclerView.layout(0, 0, 600, mHeight);
    }

    private void scrollRecyclerViewToBottom() {
        mRecyclerView.scrollBy(0, mHeight);
    }
}