chromium/chrome/android/javatests/src/org/chromium/chrome/browser/tabmodel/TabbedModeTabPersistencePolicyTest.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.tabmodel;

import static androidx.test.espresso.matcher.ViewMatchers.assertThat;

import android.app.Activity;
import android.content.Context;

import androidx.test.filters.MediumTest;
import androidx.test.platform.app.InstrumentationRegistry;

import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

import org.chromium.base.ApplicationStatus;
import org.chromium.base.ContextUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.supplier.OneshotSupplier;
import org.chromium.base.supplier.OneshotSupplierImpl;
import org.chromium.base.test.util.AdvancedMockContext;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.app.tabmodel.TabWindowManagerSingleton;
import org.chromium.chrome.browser.app.tabmodel.TabbedModeTabModelOrchestrator;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.price_tracking.PriceTrackingFeatures;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileProvider;
import org.chromium.chrome.browser.tab.MockTab;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabState;
import org.chromium.chrome.browser.tab.TabStateExtractor;
import org.chromium.chrome.browser.tab.WebContentsState;
import org.chromium.chrome.browser.tabmodel.NextTabPolicy.NextTabPolicySupplier;
import org.chromium.chrome.browser.tabmodel.TabPersistenceFileInfo.TabStateFileInfo;
import org.chromium.chrome.browser.tabmodel.TabPersistentStore.TabModelSelectorMetadata;
import org.chromium.chrome.browser.tabmodel.TabPersistentStore.TabPersistentStoreObserver;
import org.chromium.chrome.browser.tabpersistence.TabStateDirectory;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.util.browser.tabmodel.MockTabModel;
import org.chromium.chrome.test.util.browser.tabmodel.MockTabModelSelector;
import org.chromium.url.GURL;

import java.nio.ByteBuffer;

/**
 * Tests for the tabbed-mode persisitence policy. TODO: Consider turning this into a unit test after
 * resolving the task involving disk I/O.
 */
@RunWith(ChromeJUnit4ClassRunner.class)
public class TabbedModeTabPersistencePolicyTest {
    private static final WebContentsState WEB_CONTENTS_STATE =
            new WebContentsState(ByteBuffer.allocateDirect(100));

    @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();

    @Mock ProfileProvider mProfileProvider;
    @Mock Profile mProfile;
    @Mock Profile mIncognitoProfile;
    @Mock ActivityLifecycleDispatcher mActivityLifecycleDispatcher;

    private TestTabModelDirectory mMockDirectory;
    private AdvancedMockContext mAppContext;

    @Before
    public void setUp() throws Exception {
        TabWindowManagerSingleton.setTabModelSelectorFactoryForTesting(
                new TabModelSelectorFactory() {
                    @Override
                    public TabModelSelector buildSelector(
                            Context context,
                            OneshotSupplier<ProfileProvider> profileProviderSupplier,
                            TabCreatorManager tabCreatorManager,
                            NextTabPolicySupplier nextTabPolicySupplier) {
                        return new MockTabModelSelector(mProfile, mIncognitoProfile, 0, 0, null);
                    }
                });
        mAppContext =
                new AdvancedMockContext(
                        InstrumentationRegistry.getInstrumentation()
                                .getTargetContext()
                                .getApplicationContext());
        ContextUtils.initApplicationContextForTests(mAppContext);

        mMockDirectory =
                new TestTabModelDirectory(
                        mAppContext,
                        "TabbedModeTabPersistencePolicyTest",
                        TabStateDirectory.TABBED_MODE_DIRECTORY);
        TabStateDirectory.setBaseStateDirectoryForTests(mMockDirectory.getBaseDirectory());

        Mockito.when(mProfileProvider.getOriginalProfile()).thenReturn(mProfile);
        Mockito.when(mIncognitoProfile.isOffTheRecord()).thenReturn(true);
        PriceTrackingFeatures.setPriceTrackingEnabledForTesting(false);
    }

    @After
    public void tearDown() {
        mMockDirectory.tearDown();

        for (Activity activity : ApplicationStatus.getRunningActivities()) {
            activity.finishAndRemoveTask();
        }

        TabWindowManagerSingleton.resetTabModelSelectorFactoryForTesting();
    }

    private TabbedModeTabModelOrchestrator buildTestTabModelSelector(
            int[] normalTabIds, int[] incognitoTabIds) throws Exception {
        final CallbackHelper callbackSignal = new CallbackHelper();
        final int callCount = callbackSignal.getCallCount();

        MockTabModel.MockTabModelDelegate tabModelDelegate =
                new MockTabModel.MockTabModelDelegate() {
                    @Override
                    public MockTab createTab(int id, boolean incognito) {
                        Profile profile = incognito ? mIncognitoProfile : mProfile;
                        MockTab tab =
                                new MockTab(id, profile) {
                                    @Override
                                    public GURL getUrl() {
                                        return new GURL("https://www.google.com");
                                    }
                                };
                        tab.initialize(null, null, null, null, null, null, false, null, false);
                        return tab;
                    }
                };

        final MockTabModel normalTabModel =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> new MockTabModel(mProfile, tabModelDelegate));
        final MockTabModel incognitoTabModel =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> new MockTabModel(mIncognitoProfile, tabModelDelegate));
        TabbedModeTabModelOrchestrator orchestrator =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            OneshotSupplierImpl<ProfileProvider> profileProviderSupplier =
                                    new OneshotSupplierImpl<>();
                            profileProviderSupplier.set(mProfileProvider);
                            TabbedModeTabModelOrchestrator tmpOrchestrator =
                                    new TabbedModeTabModelOrchestrator(
                                            false, mActivityLifecycleDispatcher);
                            tmpOrchestrator.createTabModels(
                                    new ChromeTabbedActivity(),
                                    profileProviderSupplier,
                                    null,
                                    null,
                                    (activityAtRequestedIndex,
                                            isActivityInAppTasks,
                                            isActivityInSameTask) -> false,
                                    0);
                            TabModelSelector selector = tmpOrchestrator.getTabModelSelector();
                            ((MockTabModelSelector) selector)
                                    .initializeTabModels(normalTabModel, incognitoTabModel);
                            return tmpOrchestrator;
                        });
        TabPersistentStore store =
                ThreadUtils.runOnUiThreadBlocking(
                        () -> {
                            TabPersistentStore tmpStore =
                                    orchestrator.getTabPersistentStoreForTesting();
                            tmpStore.addObserver(
                                    new TabPersistentStoreObserver() {
                                        @Override
                                        public void onMetadataSavedAsynchronously(
                                                TabModelSelectorMetadata metadata) {
                                            callbackSignal.notifyCalled();
                                        }
                                    });
                            return tmpStore;
                        });

        // Adding tabs results in writing to disk running on AsyncTasks. Run on the main thread
        // to turn the async operations + completion callback into a synchronous operation.
        InstrumentationRegistry.getInstrumentation()
                .runOnMainSync(
                        () -> {
                            for (int tabId : normalTabIds) {
                                addTabToSaveQueue(
                                        store, normalTabModel, normalTabModel.addTab(tabId));
                            }
                            for (int tabId : incognitoTabIds) {
                                addTabToSaveQueue(
                                        store, incognitoTabModel, incognitoTabModel.addTab(tabId));
                            }
                            TabModelUtils.setIndex(normalTabModel, 0);
                            TabModelUtils.setIndex(incognitoTabModel, 0);
                        });
        callbackSignal.waitForCallback(callCount);
        return orchestrator;
    }

    private void addTabToSaveQueue(TabPersistentStore store, TabModel tabModel, Tab tab) {
        TabState tabState = new TabState();
        tabState.contentsState = WEB_CONTENTS_STATE;
        TabStateExtractor.setTabStateForTesting(tab.getId(), tabState);
        store.addTabToSaveQueue(tab);
    }

    /**
     * Test the cleanup task path that deletes all the persistent state files for an instance.
     * Ensure tabs not used by other instances only are collected for deletion. This may not be a
     * real scenario likey to happen.
     */
    @Test
    @Feature("TabPersistentStore")
    @MediumTest
    @DisableFeatures({
        ChromeFeatureList.TAB_WINDOW_MANAGER_REPORT_INDICES_MISMATCH,
        ChromeFeatureList.ANDROID_TAB_DECLUTTER_RESCUE_KILLSWITCH
    })
    public void testCleanupInstanceState() throws Throwable {
        Assert.assertNotNull(TabStateDirectory.getOrCreateBaseStateDirectory());

        // Delete instance 1. Among the tabs (4, 6, 7) (12, 14, 19), only (4, 12, 14)
        // are not used by any other instances, therefore will be the target for cleanup.
        buildTestTabModelSelector(new int[] {3, 5, 7}, new int[] {11, 13, 17});
        TabbedModeTabModelOrchestrator orchestrator1 =
                buildTestTabModelSelector(new int[] {4, 6, 7}, new int[] {12, 14, 19});
        buildTestTabModelSelector(new int[] {6, 8, 9}, new int[] {15, 18, 19});

        final int id = 1;
        TabPersistencePolicy policy =
                orchestrator1.getTabPersistentStoreForTesting().getTabPersistencePolicyForTesting();
        final CallbackHelper callbackSignal = new CallbackHelper();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    policy.cleanupInstanceState(
                            id,
                            (result) -> {
                                assertThat(
                                        result.getTabStateFileInfos(),
                                        Matchers.containsInAnyOrder(
                                                new TabStateFileInfo(4, false),
                                                new TabStateFileInfo(12, true),
                                                new TabStateFileInfo(14, true)));
                                callbackSignal.notifyCalled();
                            });
                });
        callbackSignal.waitForCallback(0);
    }
}