Commit 52c43975 authored by Michael Thiessen's avatar Michael Thiessen Committed by Commit Bot

No Touch Mode initial implementation

Introduces a new Activity type to replace ChromeTabbedActivity when
running on a device with no touch input.

Bug: 904993
Change-Id: I8d9496fdb21735141b74135be3dc975a08c3e850
Reviewed-on: https://chromium-review.googlesource.com/c/1313556
Commit-Queue: Michael Thiessen <mthiesse@chromium.org>
Reviewed-by: default avatarTed Choc <tedchoc@chromium.org>
Reviewed-by: default avatarYaron Friedman <yfriedman@chromium.org>
Cr-Commit-Position: refs/heads/master@{#608492}
parent 7abba9ec
......@@ -527,6 +527,14 @@ by a child template that "extends" this file.
{{ self.supports_video_persistence() }} >
{{ self.extra_web_rendering_activity_definitions() }}
</activity>
<activity android:name="org.chromium.chrome.browser.NoTouchActivity"
android:theme="@style/MainTheme"
android:exported="false"
android:launchMode="singleTask"
{{ self.chrome_activity_common() }}
{{ self.supports_video_persistence() }} >
{{ self.extra_web_rendering_activity_definitions() }}
</activity>
<activity android:name="org.chromium.chrome.browser.sync.ui.PassphraseActivity"
android:theme="@style/MainTheme"
......
......@@ -554,6 +554,12 @@ public abstract class ChromeActivity<C extends ChromeActivityComponent>
ControlContainer controlContainer =
(ControlContainer) findViewById(R.id.control_container);
if (controlContainer == null) {
// omnibox_results_container_stub anchors off of control_container, and will
// crash during layout if control_container doesn't exist.
UiUtils.removeViewFromParent(findViewById(R.id.omnibox_results_container_stub));
}
// Inflate the correct toolbar layout for the device.
int toolbarLayoutId = getToolbarLayoutId();
if (toolbarLayoutId != NO_TOOLBAR_LAYOUT && controlContainer != null) {
......
......@@ -96,6 +96,12 @@ public abstract class ChromeSwitches {
*/
public static final String DISABLE_TAB_MERGING_FOR_TESTING = "disable-tab-merging";
/**
* Turn on No Touch Mode, which will replace ChromeTabbedActivity with a single tab, non-touchy
* alternative.
*/
public static final String NO_TOUCH_MODE = "no-touch-mode";
///////////////////////////////////////////////////////////////////////////////////////////////
// Native Switches
///////////////////////////////////////////////////////////////////////////////////////////////
......
......@@ -422,8 +422,15 @@ public class LaunchIntentDispatcher implements IntentHandler.IntentHandlerDelega
maybePrefetchDnsInBackground();
Intent newIntent = new Intent(mIntent);
Class<?> tabbedActivityClass =
MultiWindowUtils.getInstance().getTabbedActivityForIntent(newIntent, mActivity);
Class<?> tabbedActivityClass = null;
if (CommandLine.getInstance().hasSwitch(ChromeSwitches.NO_TOUCH_MODE)) {
// When in No Touch Mode we don't support tabs, and replace the TabbedActivity with the
// NoTouchActivity.
tabbedActivityClass = NoTouchActivity.class;
} else {
tabbedActivityClass =
MultiWindowUtils.getInstance().getTabbedActivityForIntent(newIntent, mActivity);
}
newIntent.setClassName(
mActivity.getApplicationContext().getPackageName(), tabbedActivityClass.getName());
newIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
......
// Copyright 2018 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;
import android.content.Intent;
import android.os.Bundle;
import android.os.SystemClock;
import android.view.View;
import android.view.ViewGroup;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.browser.IntentHandler.IntentHandlerDelegate;
import org.chromium.chrome.browser.IntentHandler.TabOpenType;
import org.chromium.chrome.browser.compositor.layouts.LayoutManager;
import org.chromium.chrome.browser.fullscreen.ChromeFullscreenManager;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabRedirectHandler;
import org.chromium.chrome.browser.tab.TabUma.TabCreationState;
import org.chromium.chrome.browser.tabmodel.TabModel.TabLaunchType;
import org.chromium.chrome.browser.widget.ControlContainer;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.common.Referrer;
import org.chromium.ui.base.PageTransition;
import java.util.concurrent.TimeUnit;
/**
* An Activity used to display WebContents on devices that don't support touch.
*/
public class NoTouchActivity extends SingleTabActivity {
private static final String BUNDLE_TAB_ID = "tabId";
// Time at which an intent was received and handled.
private long mIntentHandlingTimeMs;
/**
* Internal class which performs the intent handling operations delegated by IntentHandler.
*/
private class InternalIntentDelegate implements IntentHandler.IntentHandlerDelegate {
/**
* Processes a url view intent.
*
* @param url The url from the intent.
*/
@Override
public void processUrlViewIntent(String url, String referer, String headers,
@TabOpenType int tabOpenType, String externalAppId, int tabIdToBringToFront,
boolean hasUserGesture, Intent intent) {
// TODO(mthiesse): ChromeTabbedActivity records a user Action here, we should do the
// same.
switch (tabOpenType) {
case TabOpenType.REUSE_URL_MATCHING_TAB_ELSE_NEW_TAB: // fall through
case TabOpenType.BRING_TAB_TO_FRONT: // fall through
case TabOpenType.REUSE_APP_ID_MATCHING_TAB_ELSE_NEW_TAB: // fall through
case TabOpenType.OPEN_NEW_TAB: // fall through
case TabOpenType.CLOBBER_CURRENT_TAB:
// TODO(mthiesse): For now, let's just clobber current tab always. Are the other
// tab open types meaningful when we only have a single tab?
Tab currentTab = getActivityTab();
assert currentTab != null;
TabRedirectHandler.from(currentTab).updateIntent(intent);
int transitionType = PageTransition.LINK | PageTransition.FROM_API;
LoadUrlParams loadUrlParams = new LoadUrlParams(url);
loadUrlParams.setIntentReceivedTimestamp(mIntentHandlingTimeMs);
loadUrlParams.setHasUserGesture(hasUserGesture);
loadUrlParams.setTransitionType(
IntentHandler.getTransitionTypeFromIntent(intent, transitionType));
if (referer != null) {
loadUrlParams.setReferrer(new Referrer(
referer, IntentHandler.getReferrerPolicyFromIntent(intent)));
}
currentTab.loadUrl(loadUrlParams);
break;
case TabOpenType.OPEN_NEW_INCOGNITO_TAB:
// Incognito is unsupported for this Activity.
assert false;
break;
default:
assert false : "Unknown TabOpenType: " + tabOpenType;
break;
}
}
@Override
public void processWebSearchIntent(String query) {
assert false;
}
}
@Override
public void finishNativeInitialization() {
View urlBar = null;
ControlContainer controlContainer = null;
initializeCompositorContent(new LayoutManager(getCompositorViewHolder()), urlBar,
(ViewGroup) findViewById(android.R.id.content), controlContainer);
if (getFullscreenManager() != null) getFullscreenManager().setTab(getActivityTab());
super.finishNativeInitialization();
}
@Override
public void initializeState() {
super.initializeState();
if (getSavedInstanceState() == null) {
boolean intentWithEffect = false;
Intent intent = getIntent();
mIntentHandlingTimeMs = SystemClock.uptimeMillis();
if (intent != null) {
if (!mIntentHandler.shouldIgnoreIntent(intent)) {
intentWithEffect = mIntentHandler.onNewIntent(intent);
}
}
if (!intentWithEffect) getTabCreator(false).launchNTP();
}
}
@Override
public void onNewIntent(Intent intent) {
mIntentHandlingTimeMs = SystemClock.uptimeMillis();
super.onNewIntent(intent);
}
@Override
protected IntentHandlerDelegate createIntentHandlerDelegate() {
return new InternalIntentDelegate();
}
@Override
protected void initializeToolbar() {}
@Override
protected ChromeFullscreenManager createFullscreenManager() {
return new ChromeFullscreenManager(this, ChromeFullscreenManager.ControlsPosition.NONE);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
saveTabState(outState);
}
private void saveTabState(Bundle outState) {
Tab tab = getActivityTab();
if (tab == null || tab.getUrl() == null || tab.getUrl().isEmpty()) return;
long time = SystemClock.elapsedRealtime();
outState.putInt(BUNDLE_TAB_ID, tab.getId());
TabState.saveState(outState, tab.getState());
RecordHistogram.recordTimesHistogram("Android.StrictMode.NoTouchActivitySaveState",
SystemClock.elapsedRealtime() - time, TimeUnit.MILLISECONDS);
}
@Override
protected Tab restoreTab(Bundle savedInstanceState) {
int tabId = getSavedInstanceState().getInt(BUNDLE_TAB_ID, Tab.INVALID_TAB_ID);
if (tabId == Tab.INVALID_TAB_ID) return null;
TabState tabState = TabState.restoreTabState(savedInstanceState);
assert tabState != null;
return new Tab(tabId, Tab.INVALID_TAB_ID, false, getWindowAndroid(),
TabLaunchType.FROM_RESTORE, TabCreationState.FROZEN_ON_RESTORE, tabState);
}
@Override
public void onStopWithNative() {
super.onStopWithNative();
if (getFullscreenManager() != null) getFullscreenManager().exitPersistentFullscreenMode();
}
}
......@@ -6,6 +6,7 @@ package org.chromium.chrome.browser;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.util.Log;
......@@ -60,6 +61,23 @@ public class TabState {
/** Checks if the TabState header is loaded properly. */
private static final long KEY_CHECKER = 0;
/**
* There's no official maximum size for a bundle, but if a Binder transaction fails and the
* parcel was bigger than 200kB, the platform blames it on the bundle being too large.
*/
private static final int MAX_BUNDLE_SIZE = 200 * 1024;
private static final String TAB_STATE_BUNDLE_PREFIX = "tab_";
private static final String TIMESTAMP_MILLIS = TAB_STATE_BUNDLE_PREFIX + "timestampMillis";
private static final String CONTENT_STATE_BYTES =
TAB_STATE_BUNDLE_PREFIX + "contentsStateBytes";
private static final String PARENT_ID = TAB_STATE_BUNDLE_PREFIX + "parentId";
private static final String OPENER_APP_ID = TAB_STATE_BUNDLE_PREFIX + "openerAppId";
private static final String VERSION = TAB_STATE_BUNDLE_PREFIX + "version";
private static final String SHOULD_PRESERVE = TAB_STATE_BUNDLE_PREFIX + "shouldPreserve";
private static final String THEME_COLOR = TAB_STATE_BUNDLE_PREFIX + "themeColor";
private static final String IS_INCOGNITO = TAB_STATE_BUNDLE_PREFIX + "isIncognito";
/** Overrides the Chrome channel/package name to test a variant channel-specific behaviour. */
private static String sChannelNameOverrideForTest;
......@@ -150,7 +168,7 @@ public class TabState {
protected boolean mIsIncognito;
/** Whether the theme color was set for this tab. */
private boolean mHasThemeColor;
protected boolean mHasThemeColor;
/** @return Whether a Stable channel build of Chrome is being used. */
private static boolean isStableChannelBuild() {
......@@ -186,15 +204,15 @@ public class TabState {
/**
* Restores a particular TabState file from storage.
* @param tabFile Location of the TabState file.
* @param isIncognito Whether the Tab is incognito or not.
* @param isEncrypted Whether the Tab state is encrypted or not.
* @return TabState that has been restored, or null if it failed.
*/
public static TabState restoreTabState(File tabFile, boolean isIncognito) {
public static TabState restoreTabState(File tabFile, boolean isEncrypted) {
FileInputStream stream = null;
TabState tabState = null;
try {
stream = new FileInputStream(tabFile);
tabState = TabState.readState(stream, isIncognito);
tabState = TabState.readState(stream, isEncrypted);
} catch (FileNotFoundException exception) {
Log.e(TAG, "Failed to restore tab state for tab: " + tabFile);
} catch (IOException exception) {
......@@ -205,6 +223,28 @@ public class TabState {
return tabState;
}
/**
* Restores a particular TabState file from the provided Bundle.
* @param bundle The Bundle to restore TabState from.
* @return TabState that has been restored.
*/
public static TabState restoreTabState(Bundle bundle) {
TabState tabState = new TabState();
tabState.timestampMillis = bundle.getLong(TIMESTAMP_MILLIS);
byte[] bytes = bundle.getByteArray(CONTENT_STATE_BYTES);
tabState.contentsState = new WebContentsState(ByteBuffer.allocateDirect(bytes.length));
tabState.contentsState.buffer().put(bytes);
tabState.parentId = bundle.getInt(PARENT_ID);
tabState.openerAppId = bundle.getString(OPENER_APP_ID);
tabState.contentsState.setVersion(bundle.getInt(VERSION));
tabState.shouldPreserve = bundle.getBoolean(SHOULD_PRESERVE);
tabState.themeColor = bundle.getInt(THEME_COLOR);
tabState.mHasThemeColor = ColorUtils.isValidThemeColor(tabState.themeColor);
tabState.mIsIncognito = bundle.getBoolean(IS_INCOGNITO);
return tabState;
}
/**
* Restores a particular TabState file from storage.
* @param input Location of the TabState file.
......@@ -305,6 +345,20 @@ public class TabState {
}
}
private static byte[] getContentStateByteArray(ByteBuffer buffer) {
byte[] contentsStateBytes = new byte[buffer.limit()];
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
buffer.rewind();
buffer.get(contentsStateBytes);
} else {
// For JellyBean and below a bug in MappedByteBufferAdapter causes rewind to not be
// propagated to the underlying ByteBuffer, and results in an underflow exception. See:
// http://b.android.com/53637.
for (int i = 0; i < buffer.limit(); i++) contentsStateBytes[i] = buffer.get(i);
}
return contentsStateBytes;
}
/**
* Writes the TabState to disk. This method may be called on either the UI or background thread.
* @param file File to write the tab's state to.
......@@ -317,18 +371,7 @@ public class TabState {
// Create the byte array from contentsState before opening the FileOutputStream, in case
// contentsState.buffer is an instance of MappedByteBuffer that is mapped to
// the tab state file.
byte[] contentsStateBytes = new byte[state.contentsState.buffer().limit()];
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
state.contentsState.buffer().rewind();
state.contentsState.buffer().get(contentsStateBytes);
} else {
// For JellyBean and below a bug in MappedByteBufferAdapter causes rewind to not be
// propagated to the underlying ByteBuffer, and results in an underflow exception. See:
// http://b.android.com/53637.
for (int i = 0; i < state.contentsState.buffer().limit(); i++) {
contentsStateBytes[i] = state.contentsState.buffer().get(i);
}
}
byte[] contentsStateBytes = getContentStateByteArray(state.contentsState.buffer());
DataOutputStream dataOutputStream = null;
FileOutputStream fileOutputStream = null;
......@@ -372,6 +415,33 @@ public class TabState {
}
}
/**
* Writes the TabState to a bundle. This method may be called on either the UI or background
* thread.
* @param bundle Bundle to write the tab's state to.
* @param state State object obtained from from {@link Tab#getState()}.
*/
public static void saveState(Bundle bundle, TabState state) {
if (state == null || state.contentsState == null) return;
byte[] contentsStateBytes = getContentStateByteArray(state.contentsState.buffer());
// Chrome Tab State only goes back 50 navigations, and the size of TabState typically caps
// out around 50kB.
// TODO(mthiesse): If this starts getting hit we'll need to reduce the history size or fall
// back to saving to disk.
assert contentsStateBytes.length < MAX_BUNDLE_SIZE;
bundle.putLong(TIMESTAMP_MILLIS, state.timestampMillis);
bundle.putByteArray(CONTENT_STATE_BYTES, contentsStateBytes);
bundle.putInt(PARENT_ID, state.parentId);
bundle.putString(OPENER_APP_ID, state.openerAppId);
bundle.putInt(VERSION, state.contentsState.version());
bundle.putBoolean(SHOULD_PRESERVE, state.shouldPreserve);
bundle.putInt(THEME_COLOR, state.themeColor);
bundle.putBoolean(IS_INCOGNITO, state.isIncognito());
}
/**
* Returns a File corresponding to the given TabState.
* @param directory Directory containing the TabState files.
......
......@@ -122,7 +122,10 @@ public class ChromeFullscreenManager
@Override
public void run() {
int visibility = shouldShowAndroidControls() ? View.VISIBLE : View.INVISIBLE;
if (mControlContainer.getView().getVisibility() == visibility) return;
if (mControlContainer == null
|| mControlContainer.getView().getVisibility() == visibility) {
return;
}
// requestLayout is required to trigger a new gatherTransparentRegion(), which
// only occurs together with a layout and let's SurfaceFlinger trim overlays.
// This may be almost equivalent to using View.GONE, but we still use View.INVISIBLE
......@@ -207,7 +210,7 @@ public class ChromeFullscreenManager
}
};
assert controlContainer != null;
assert controlContainer != null || mControlsPosition == ControlsPosition.NONE;
mControlContainer = controlContainer;
int controlContainerHeight =
......@@ -399,7 +402,7 @@ public class ChromeFullscreenManager
}
/**
* @return The toolbar control container, null until {@link #initialize} is called.
* @return The toolbar control container, may be null.
*/
@Nullable
public ControlContainer getControlContainer() {
......@@ -567,6 +570,7 @@ public class ChromeFullscreenManager
}
private boolean shouldShowAndroidControls() {
if (mControlContainer == null) return false;
if (mHidingTokenHolder.hasTokens()) {
return false;
}
......
......@@ -277,10 +277,9 @@ public abstract class AsyncInitializationActivity extends AppCompatActivity impl
@LaunchIntentDispatcher.Action
int dispatchAction = maybeDispatchLaunchIntent(getIntent());
if (dispatchAction != LaunchIntentDispatcher.Action.CONTINUE) {
abortLaunch();
abortLaunch(dispatchAction);
return;
}
if (DocumentModeAssassin.getInstance().isMigrationNecessary()) {
// Some Samsung devices load fonts from disk, crbug.com/691706.
try (StrictModeContext unused = StrictModeContext.allowDiskReads()) {
......@@ -297,14 +296,14 @@ public abstract class AsyncInitializationActivity extends AppCompatActivity impl
Intent intent = getIntent();
if (!isStartedUpCorrectly(intent)) {
abortLaunch();
abortLaunch(LaunchIntentDispatcher.Action.FINISH_ACTIVITY_REMOVE_TASK);
return;
}
if (requiresFirstRunToBeCompleted(intent)
&& FirstRunFlowSequencer.launch(this, intent, false /* requiresBroadcast */,
shouldPreferLightweightFre(intent))) {
abortLaunch();
abortLaunch(LaunchIntentDispatcher.Action.FINISH_ACTIVITY_REMOVE_TASK);
return;
}
......@@ -332,22 +331,28 @@ public abstract class AsyncInitializationActivity extends AppCompatActivity impl
*/
protected void initializeStartupMetrics() {}
private void abortLaunch() {
private void abortLaunch(@LaunchIntentDispatcher.Action int dispatchAction) {
super.onCreate(null);
ApiCompatibilityUtils.finishAndRemoveTask(this);
overridePendingTransition(0, R.anim.no_anim);
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP
|| Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP_MR1) {
// On L ApiCompatibilityUtils.finishAndRemoveTask() sometimes fails, which causes
// NPE in onStart() later, see crbug.com/781396. We can't let this activity to
// start, and we don't want to crash either. So try finishing one more time and
// suicide if that fails.
if (!isFinishing()) {
finish();
if (!isFinishing()) Process.killProcess(Process.myPid());
if (dispatchAction == LaunchIntentDispatcher.Action.FINISH_ACTIVITY) {
finish();
return;
} else {
assert dispatchAction == LaunchIntentDispatcher.Action.FINISH_ACTIVITY_REMOVE_TASK;
ApiCompatibilityUtils.finishAndRemoveTask(this);
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP
|| Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP_MR1) {
// On L ApiCompatibilityUtils.finishAndRemoveTask() sometimes fails, which causes
// NPE in onStart() later, see crbug.com/781396. We can't let this activity to
// start, and we don't want to crash either. So try finishing one more time and
// suicide if that fails.
if (!isFinishing()) {
finish();
if (!isFinishing()) Process.killProcess(Process.myPid());
}
}
}
overridePendingTransition(0, R.anim.no_anim);
}
/**
......
......@@ -60,6 +60,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/NavigationBarColorController.java",
"java/src/org/chromium/chrome/browser/NavigationPopup.java",
"java/src/org/chromium/chrome/browser/NearOomMonitor.java",
"java/src/org/chromium/chrome/browser/NoTouchActivity.java",
"java/src/org/chromium/chrome/browser/PowerBroadcastReceiver.java",
"java/src/org/chromium/chrome/browser/RepostFormWarningDialog.java",
"java/src/org/chromium/chrome/browser/SearchGeolocationDisclosureTabHelper.java",
......@@ -1805,6 +1806,7 @@ chrome_test_java_sources = [
"javatests/src/org/chromium/chrome/browser/ContentViewFocusTest.java",
"javatests/src/org/chromium/chrome/browser/CopylessPasteTest.java",
"javatests/src/org/chromium/chrome/browser/ExampleUiCaptureTest.java",
"javatests/src/org/chromium/chrome/browser/FeaturesAnnotationsTest.java",
"javatests/src/org/chromium/chrome/browser/FocusedEditableTextFieldZoomTest.java",
"javatests/src/org/chromium/chrome/browser/FullscreenActivityTest.java",
"javatests/src/org/chromium/chrome/browser/HTTPSTabsOpenedFromExternalAppTest.java",
......@@ -1815,9 +1817,9 @@ chrome_test_java_sources = [
"javatests/src/org/chromium/chrome/browser/MainActivityWithURLTest.java",
"javatests/src/org/chromium/chrome/browser/ModalDialogTest.java",
"javatests/src/org/chromium/chrome/browser/NavigateTest.java",
"javatests/src/org/chromium/chrome/browser/FeaturesAnnotationsTest.java",
"javatests/src/org/chromium/chrome/browser/NavigationBarColorControllerTest.java",
"javatests/src/org/chromium/chrome/browser/NavigationPopupTest.java",
"javatests/src/org/chromium/chrome/browser/NoTouchActivityTest.java",
"javatests/src/org/chromium/chrome/browser/OSKOverscrollTest.java",
"javatests/src/org/chromium/chrome/browser/PopularUrlsTest.java",
"javatests/src/org/chromium/chrome/browser/PopupTest.java",
......
// Copyright 2018 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;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.MediumTest;
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.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.chrome.browser.ntp.NewTabPage;
import org.chromium.chrome.test.ChromeActivityTestRule;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.util.ApplicationTestUtils;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.components.safe_browsing.SafeBrowsingApiBridge;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.test.util.CriteriaHelper;
import org.chromium.net.test.EmbeddedTestServer;
/**
* Tests for NoTouchActivity.
*/
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE, ChromeSwitches.NO_TOUCH_MODE})
public class NoTouchActivityTest {
private static final String TEST_PATH = "/chrome/test/data/android/simple.html";
@Rule
public ChromeActivityTestRule<NoTouchActivity> mActivityTestRule =
new ChromeActivityTestRule<>(NoTouchActivity.class);
private EmbeddedTestServer mTestServer;
private NoTouchActivity mActivity;
@Before
public void setUp() throws InterruptedException {
mTestServer = EmbeddedTestServer.createAndStartServer(InstrumentationRegistry.getContext());
mActivityTestRule.startMainActivityFromLauncher();
mActivity = mActivityTestRule.getActivity();
}
@After
public void tearDown() {
mTestServer.stopAndDestroyServer();
}
/**
* Tests that the NoTouchActivity starts up to the NTP.
*/
@Test
@MediumTest
public void testStartsUpToNewTabPage() throws Throwable {
Assert.assertTrue(mActivity.getActivityTab().getNativePage() instanceof NewTabPage);
}
/**
* Tests that the Tab persists through recreation.
*/
@Test
@MediumTest
public void testRecreateWithTabHistory() throws Throwable {
mActivityTestRule.loadUrl(mTestServer.getURL(TEST_PATH));
ThreadUtils.runOnUiThreadBlocking(() -> {
Assert.assertEquals(mActivity.getActivityTab().getWebContents().getLastCommittedUrl(),
mTestServer.getURL(TEST_PATH));
});
mActivity = ApplicationTestUtils.recreateActivity(mActivity);
mActivityTestRule.setActivity(mActivity);
mActivityTestRule.waitForActivityNativeInitializationComplete();
ChromeTabUtils.waitForTabPageLoaded(
mActivity.getActivityTab(), mTestServer.getURL(TEST_PATH));
ThreadUtils.runOnUiThreadBlocking(() -> mActivity.onBackPressed());
CriteriaHelper.pollUiThread(
() -> mActivity.getActivityTab().getNativePage() instanceof NewTabPage);
}
/**
* Tests that Safe Browsing and interstitials work.
*/
@Test
@MediumTest
public void testSafeBrowsing() throws Throwable {
SafeBrowsingApiBridge.setSafeBrowsingHandlerType(
new MockSafeBrowsingApiHandler().getClass());
final String url = mTestServer.getURL(TEST_PATH);
MockSafeBrowsingApiHandler.addMockResponse(url, "{\"matches\":[{\"threat_type\":\"5\"}]}");
ThreadUtils.runOnUiThreadBlocking(
() -> mActivity.getActivityTab().loadUrl(new LoadUrlParams(url)));
CriteriaHelper.pollUiThread(
()
-> mActivity.getActivityTab().getWebContents().isShowingInterstitialPage(),
"Failed to show Safe Browsing Interstitial page", 5000, 50);
MockSafeBrowsingApiHandler.clearMockResponses();
}
}
......@@ -4,6 +4,8 @@
package org.chromium.chrome.browser;
import android.graphics.Color;
import android.os.Bundle;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
......@@ -15,10 +17,12 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.chrome.browser.TabState.WebContentsState;
import org.chromium.chrome.browser.tabmodel.TestTabModelDirectory;
import org.chromium.chrome.browser.test.ChromeBrowserTestRule;
import java.io.File;
import java.nio.ByteBuffer;
/**
* Tests whether TabState can be saved and restored to disk properly. Also checks to see if
......@@ -85,4 +89,37 @@ public class TabStateTest {
// Hebrew, RTL.
loadAndCheckTabState(TestTabModelDirectory.V2_HAARETZ);
}
@Test
@SmallTest
public void testSaveLoadThroughBundle() throws Exception {
TabState tabState = new TabState();
byte[] bytes = {'A', 'B', 'C'};
tabState.contentsState = new WebContentsState(ByteBuffer.allocateDirect(bytes.length));
tabState.contentsState.buffer().put(bytes);
tabState.timestampMillis = 1234;
tabState.parentId = 2;
tabState.openerAppId = "app";
tabState.contentsState.setVersion(TabState.CONTENTS_STATE_CURRENT_VERSION);
tabState.shouldPreserve = true;
tabState.themeColor = Color.BLACK;
tabState.mHasThemeColor = true;
tabState.mIsIncognito = true;
Bundle b = new Bundle();
TabState.saveState(b, tabState);
TabState restoredState = TabState.restoreTabState(b);
Assert.assertEquals(restoredState.contentsState.buffer(), tabState.contentsState.buffer());
Assert.assertEquals(tabState.timestampMillis, restoredState.timestampMillis);
Assert.assertEquals(tabState.parentId, restoredState.parentId);
Assert.assertEquals(tabState.openerAppId, restoredState.openerAppId);
Assert.assertEquals(tabState.timestampMillis, restoredState.timestampMillis);
Assert.assertEquals(
tabState.contentsState.version(), restoredState.contentsState.version());
Assert.assertEquals(tabState.shouldPreserve, restoredState.shouldPreserve);
Assert.assertEquals(tabState.themeColor, restoredState.themeColor);
Assert.assertEquals(tabState.mHasThemeColor, restoredState.mHasThemeColor);
Assert.assertEquals(tabState.mIsIncognito, restoredState.mIsIncognito);
}
}
......@@ -11,6 +11,7 @@ import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Handler;
import android.os.PowerManager;
import android.util.Pair;
......@@ -34,6 +35,8 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
/**
* Methods used for testing Chrome at the Application-level.
......@@ -256,4 +259,43 @@ public class ApplicationTestUtils {
}
});
}
/**
* Recreates the provided Activity, returning the newly created Activity once it's finished
* starting up.
* @param activity The Activity to recreate.
* @return The newly created Activity.
*/
public static <T extends Activity> T recreateActivity(T activity) {
final Class<?> activityClass = activity.getClass();
final CallbackHelper activityCallback = new CallbackHelper();
final AtomicReference<T> activityRef = new AtomicReference<>();
ApplicationStatus.ActivityStateListener stateListener =
new ApplicationStatus.ActivityStateListener() {
@SuppressWarnings("unchecked")
@Override
public void onActivityStateChange(Activity activity, int newState) {
if (newState == ActivityState.RESUMED) {
if (!activityClass.isAssignableFrom(activity.getClass())) return;
activityRef.set((T) activity);
new Handler().post(() -> activityCallback.notifyCalled());
ApplicationStatus.unregisterActivityStateListener(this);
}
}
};
ApplicationStatus.registerStateListenerForAllActivities(stateListener);
try {
ThreadUtils.runOnUiThreadBlocking(() -> activity.recreate());
activityCallback.waitForCallback("Activity did not start as expected", 0);
T createdActivity = activityRef.get();
Assert.assertNotNull("Activity reference is null.", createdActivity);
return createdActivity;
} catch (InterruptedException | TimeoutException e) {
throw new RuntimeException(e);
} finally {
ApplicationStatus.unregisterActivityStateListener(stateListener);
}
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment