blob: 3aa0a5f2710634876e153e7b388403000bbdd92c [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// 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 org.chromium.base.test.util.Restriction.RESTRICTION_TYPE_NON_LOW_END_DEVICE;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ApplicationStatus.ActivityStateListener;
import org.chromium.base.ContextUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.MinAndroidSdkLevel;
import org.chromium.base.test.util.Restriction;
import org.chromium.base.test.util.UrlUtils;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.multiwindow.MultiWindowTestHelper;
import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
import org.chromium.chrome.browser.tabmodel.TabModel.TabLaunchType;
import org.chromium.chrome.browser.tabmodel.TabPersistentStoreTest.MockTabPersistentStoreObserver;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.chrome.test.util.OverviewModeBehaviorWatcher;
import org.chromium.content.browser.test.util.Criteria;
import org.chromium.content.browser.test.util.CriteriaHelper;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.ui.test.util.UiRestriction;
/**
* Tests merging tab models for Android N+ multi-instance.
*/
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@MinAndroidSdkLevel(24)
public class TabModelMergingTest {
@Rule
public ChromeTabbedActivityTestRule mActivityTestRule = new ChromeTabbedActivityTestRule();
private static final String TEST_URL_0 = UrlUtils.encodeHtmlDataUri("<html>test_url_0.</html>");
private static final String TEST_URL_1 = UrlUtils.encodeHtmlDataUri("<html>test_url_1.</html>");
private static final String TEST_URL_2 = UrlUtils.encodeHtmlDataUri("<html>test_url_2.</html>");
private static final String TEST_URL_3 = UrlUtils.encodeHtmlDataUri("<html>test_url_3.</html>");
private static final String TEST_URL_4 = UrlUtils.encodeHtmlDataUri("<html>test_url_4.</html>");
private static final String TEST_URL_5 = UrlUtils.encodeHtmlDataUri("<html>test_url_5.</html>");
private static final String TEST_URL_6 = UrlUtils.encodeHtmlDataUri("<html>test_url_6.</html>");
private ChromeTabbedActivity mActivity1;
private ChromeTabbedActivity mActivity2;
private int mActivity1State;
private int mActivity2State;
private String[] mMergeIntoActivity1ExpectedTabs;
private String[] mMergeIntoActivity2ExpectedTabs;
@Before
public void setUp() throws Exception {
mActivityTestRule.startMainActivityOnBlankPage();
// Make sure file migrations don't run as they are unnecessary since app data was cleared.
ContextUtils.getAppSharedPreferences().edit().putBoolean(
TabbedModeTabPersistencePolicy.PREF_HAS_RUN_FILE_MIGRATION, true).apply();
ContextUtils.getAppSharedPreferences().edit().putBoolean(
TabbedModeTabPersistencePolicy.PREF_HAS_RUN_MULTI_INSTANCE_FILE_MIGRATION, true)
.apply();
// Some of the logic for when to trigger a merge depends on whether the activity is in
// multi-window mode. Set isInMultiWindowMode to true to avoid merging unexpectedly.
MultiWindowUtils.getInstance().setIsInMultiWindowModeForTesting(true);
// Initialize activities.
mActivity1 = mActivityTestRule.getActivity();
// Start multi-instance mode so that ChromeTabbedActivity's check for whether the activity
// is started up correctly doesn't fail.
ChromeTabbedActivity.onMultiInstanceModeStarted();
mActivity2 = MultiWindowTestHelper.createSecondChromeTabbedActivity(mActivity1);
CriteriaHelper.pollUiThread(new Criteria() {
@Override
public boolean isSatisfied() {
return mActivity2.areTabModelsInitialized()
&& mActivity2.getTabModelSelector().isTabStateInitialized();
}
});
// Create a few tabs in each activity.
createTabsOnUiThread();
// Initialize activity states and register for state change events.
mActivity1State = ApplicationStatus.getStateForActivity(mActivity1);
mActivity2State = ApplicationStatus.getStateForActivity(mActivity2);
ApplicationStatus.registerStateListenerForAllActivities(new ActivityStateListener() {
@Override
public void onActivityStateChange(Activity activity, int newState) {
if (activity.equals(mActivity1)) {
mActivity1State = newState;
} else if (activity.equals(mActivity2)) {
mActivity2State = newState;
}
}
});
}
/**
* Creates new tabs in both ChromeTabbedActivity and ChromeTabbedActivity2 and asserts that each
* has the expected number of tabs.
*/
private void createTabsOnUiThread() {
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
// Load a URL in ChromeTabbedActivity2 so that its first tab isn't chrome://newtab.
// chrome://newtab may be skipped when reading tab state during cold start.
mActivity2.getActivityTab().loadUrl(new LoadUrlParams(TEST_URL_5));
// Create normal tabs.
mActivity1.getTabCreator(false).createNewTab(new LoadUrlParams(TEST_URL_0),
TabLaunchType.FROM_CHROME_UI, null);
mActivity1.getTabCreator(false).createNewTab(new LoadUrlParams(TEST_URL_1),
TabLaunchType.FROM_CHROME_UI, null);
mActivity1.getTabCreator(false).createNewTab(new LoadUrlParams(TEST_URL_2),
TabLaunchType.FROM_CHROME_UI, null);
mActivity2.getTabCreator(false).createNewTab(new LoadUrlParams(TEST_URL_3),
TabLaunchType.FROM_CHROME_UI, null);
mActivity2.getTabCreator(false).createNewTab(new LoadUrlParams(TEST_URL_4),
TabLaunchType.FROM_CHROME_UI, null);
mActivity1.saveState();
mActivity2.saveState();
}
});
// ChromeTabbedActivity should have four normal tabs, the one it started with and the three
// just created.
Assert.assertEquals("Wrong number of tabs in ChromeTabbedActivity", 4,
mActivity1.getTabModelSelector().getModel(false).getCount());
// ChromeTabbedActivity2 should have three normal tabs, the one it started with and the two
// just created.
Assert.assertEquals("Wrong number of tabs in ChromeTabbedActivity2", 3,
mActivity2.getTabModelSelector().getModel(false).getCount());
// Construct expected tabs.
mMergeIntoActivity1ExpectedTabs = new String[7];
mMergeIntoActivity2ExpectedTabs = new String[7];
for (int i = 0; i < 4; i++) {
mMergeIntoActivity1ExpectedTabs[i] =
mActivity1.getTabModelSelector().getModel(false).getTabAt(i).getUrl();
mMergeIntoActivity2ExpectedTabs[i + 3] =
mActivity1.getTabModelSelector().getModel(false).getTabAt(i).getUrl();
}
for (int i = 0; i < 3; i++) {
mMergeIntoActivity2ExpectedTabs[i] =
mActivity2.getTabModelSelector().getModel(false).getTabAt(i).getUrl();
mMergeIntoActivity1ExpectedTabs[i + 4] =
mActivity2.getTabModelSelector().getModel(false).getTabAt(i).getUrl();
}
}
private void mergeTabsAndAssert(final ChromeTabbedActivity activity,
final String[] expectedTabUrls) {
String selectedTabUrl = activity.getTabModelSelector().getCurrentTab().getUrl();
mergeTabsAndAssert(activity, expectedTabUrls, expectedTabUrls.length, selectedTabUrl);
}
/**
* Merges tabs into the provided activity and asserts that the tab model looks as expected.
* @param activity The activity to merge into.
* @param expectedTabUrls The expected ordering of normal tab URLs after the merge.
* @param expectedNumberOfTabs The expected number of tabs after the merge.
* @param expectedSelectedTabUrl The expected URL of the selected tab after the merge.
*/
private void mergeTabsAndAssert(final ChromeTabbedActivity activity,
final String[] expectedTabUrls, final int expectedNumberOfTabs,
String expectedSelectedTabUrl) {
// Merge tabs into the activity.
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
activity.maybeMergeTabs();
}
});
// Wait for all tabs to be merged into the activity.
CriteriaHelper.pollUiThread(new Criteria() {
@Override
public boolean isSatisfied() {
return activity.getTabModelSelector().getTotalTabCount() == expectedNumberOfTabs;
}
});
assertTabModelMatchesExpectations(activity, expectedSelectedTabUrl, expectedTabUrls);
}
/**
* @param context The context to use when creating the intent.
* @return An intent that can be used to start a new ChromeTabbedActivity.
*/
private Intent createChromeTabbedActivityIntent(Context context) {
Intent intent = new Intent();
intent.setClass(context, ChromeTabbedActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
return intent;
}
/**
* Starts a new ChromeTabbedActivity and asserts that the tab model looks as expected.
* @param intent The intent to launch the new activity.
* @param expectedSelectedTabUrl The URL of the tab that's expected to be selected.
* @param expectedTabUrls The expected ordering of tab URLs after the merge.
* @return The newly created ChromeTabbedActivity.
*/
private ChromeTabbedActivity startNewChromeTabbedActivityAndAssert(Intent intent,
String expectedSelectedTabUrl, String[] expectedTabUrls) {
final ChromeTabbedActivity newActivity =
(ChromeTabbedActivity) InstrumentationRegistry.getInstrumentation()
.startActivitySync(intent);
// Wait for the tab state to be initialized.
CriteriaHelper.pollUiThread(new Criteria() {
@Override
public boolean isSatisfied() {
return newActivity.areTabModelsInitialized()
&& newActivity.getTabModelSelector().isTabStateInitialized();
}
});
assertTabModelMatchesExpectations(newActivity, expectedSelectedTabUrl, expectedTabUrls);
return newActivity;
}
private void assertTabModelMatchesExpectations(final ChromeTabbedActivity activity,
String expectedSelectedTabUrl, final String[] expectedTabUrls) {
// Assert there are the correct number of tabs.
Assert.assertEquals("Wrong number of normal tabs", expectedTabUrls.length,
activity.getTabModelSelector().getModel(false).getCount());
// Assert that the correct tab is selected.
Assert.assertEquals("Wrong tab selected", expectedSelectedTabUrl,
activity.getTabModelSelector().getCurrentTab().getUrl());
// Assert that tabs are in the correct order.
for (int i = 0; i < expectedTabUrls.length; i++) {
Assert.assertEquals("Wrong tab at position " + i, expectedTabUrls[i],
activity.getTabModelSelector().getModel(false).getTabAt(i).getUrl());
}
}
@Test
@LargeTest
@Feature({"TabPersistentStore", "MultiWindow"})
public void testMergeIntoChromeTabbedActivity1() throws Exception {
mergeTabsAndAssert(mActivity1, mMergeIntoActivity1ExpectedTabs);
mActivity1.finishAndRemoveTask();
}
@Test
@LargeTest
@Feature({"TabPersistentStore", "MultiWindow"})
public void testMergeIntoChromeTabbedActivity2() throws Exception {
mergeTabsAndAssert(mActivity2, mMergeIntoActivity2ExpectedTabs);
mActivity2.finishAndRemoveTask();
}
@Test
@LargeTest
@Feature({"TabPersistentStore", "MultiWindow"})
public void testMergeOnColdStart() throws Exception {
String expectedSelectedUrl = mActivity1.getTabModelSelector().getCurrentTab().getUrl();
// Create an intent to launch a new ChromeTabbedActivity.
Intent intent = createChromeTabbedActivityIntent(mActivity1);
// Save state.
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
mActivity1.saveState();
mActivity2.saveState();
}
});
// Destroy both activities.
mActivity1.finishAndRemoveTask();
mActivity2.finishAndRemoveTask();
CriteriaHelper.pollUiThread(new Criteria() {
@Override
public boolean isSatisfied() {
return mActivity1State == ActivityState.DESTROYED
&& mActivity2State == ActivityState.DESTROYED;
}
});
// Start the new ChromeTabbedActivity.
final ChromeTabbedActivity newActivity = startNewChromeTabbedActivityAndAssert(
intent, expectedSelectedUrl, mMergeIntoActivity1ExpectedTabs);
// Clean up.
newActivity.finishAndRemoveTask();
}
@Test
@LargeTest
@Feature({"TabPersistentStore", "MultiWindow"})
public void testMergeOnColdStartFromChromeTabbedActivity2() throws Exception {
String expectedSelectedUrl = mActivity2.getTabModelSelector().getCurrentTab().getUrl();
MockTabPersistentStoreObserver mockObserver = new MockTabPersistentStoreObserver();
TabModelSelectorImpl tabModelSelector =
(TabModelSelectorImpl) mActivity2.getTabModelSelector();
tabModelSelector.getTabPersistentStoreForTesting().addObserver(mockObserver);
// Merge tabs into ChromeTabbedActivity2. Wait for the merge to finish, ensuring the
// tab metadata file for ChromeTabbedActivity gets deleted before attempting to merge
// on cold start.
mergeTabsAndAssert(mActivity2, mMergeIntoActivity2ExpectedTabs);
mockObserver.stateMergedCallback.waitForCallback(0, 1);
// Create an intent to launch a new ChromeTabbedActivity.
Intent intent = createChromeTabbedActivityIntent(mActivity2);
// Destroy ChromeTabbedActivity2. ChromeTabbedActivity should have been destroyed during the
// merge.
mActivity2.finishAndRemoveTask();
CriteriaHelper.pollUiThread(new Criteria() {
@Override
public boolean isSatisfied() {
return mActivity1State == ActivityState.DESTROYED
&& mActivity2State == ActivityState.DESTROYED;
}
});
// Start the new ChromeTabbedActivity.
final ChromeTabbedActivity newActivity = startNewChromeTabbedActivityAndAssert(
intent, expectedSelectedUrl, mMergeIntoActivity2ExpectedTabs);
// Clean up.
newActivity.finishAndRemoveTask();
}
@Test
@LargeTest
@Feature({"TabPersistentStore", "MultiWindow"})
@Restriction({UiRestriction.RESTRICTION_TYPE_PHONE, RESTRICTION_TYPE_NON_LOW_END_DEVICE})
public void testMergeWhileInTabSwitcher() throws Exception {
OverviewModeBehaviorWatcher overviewModeWatcher = new OverviewModeBehaviorWatcher(
mActivity1.getLayoutManager(), true, false);
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
mActivity1.getLayoutManager().showOverview(false);
}
});
overviewModeWatcher.waitForBehavior();
mergeTabsAndAssert(mActivity1, mMergeIntoActivity1ExpectedTabs);
Assert.assertTrue("Overview mode should still be showing", mActivity1.isInOverviewMode());
mActivity1.finishAndRemoveTask();
}
@Test
@LargeTest
@Feature({"TabPersistentStore", "MultiWindow"})
public void testMergeWithNoTabs() throws Exception {
// Close all tabs and wait for the callback.
ChromeTabUtils.closeAllTabs(InstrumentationRegistry.getInstrumentation(), mActivity1);
String[] expectedTabUrls = new String[3];
for (int i = 0; i < 3; i++) {
expectedTabUrls[i] = mMergeIntoActivity2ExpectedTabs[i];
}
// The first tab should be selected after the merge.
mergeTabsAndAssert(mActivity1, expectedTabUrls, 3, expectedTabUrls[0]);
mActivity1.finishAndRemoveTask();
}
@Test
@LargeTest
@Feature({"TabPersistentStore", "MultiWindow"})
public void testMergingIncognitoTabs() throws InterruptedException {
// Incognito tabs must be fully loaded so that their tab states are written out.
ChromeTabUtils.fullyLoadUrlInNewTab(
InstrumentationRegistry.getInstrumentation(), mActivity1, TEST_URL_5, true);
ChromeTabUtils.fullyLoadUrlInNewTab(
InstrumentationRegistry.getInstrumentation(), mActivity2, TEST_URL_6, true);
// Save state.
ThreadUtils.runOnUiThreadBlocking(new Runnable() {
@Override
public void run() {
mActivity1.saveState();
mActivity2.saveState();
}
});
Assert.assertEquals("Wrong number of incognito tabs in ChromeTabbedActivity", 1,
mActivity1.getTabModelSelector().getModel(true).getCount());
Assert.assertEquals("Wrong number of tabs in ChromeTabbedActivity", 5,
mActivity1.getTabModelSelector().getTotalTabCount());
Assert.assertEquals("Wrong number of incognito tabs in ChromeTabbedActivity2", 1,
mActivity2.getTabModelSelector().getModel(true).getCount());
Assert.assertEquals("Wrong number of tabs in ChromeTabbedActivity2", 4,
mActivity2.getTabModelSelector().getTotalTabCount());
String selectedUrl = mActivity1.getTabModelSelector().getCurrentTab().getUrl();
mergeTabsAndAssert(mActivity1, mMergeIntoActivity1ExpectedTabs, 9, selectedUrl);
}
}