// Copyright 2015 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.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.app.SearchManager; import android.app.assist.AssistContent; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.StrictMode; import android.os.SystemClock; import android.support.v7.app.AlertDialog; import android.util.DisplayMetrics; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewStub; import android.view.Window; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener; import org.chromium.base.ActivityState; import org.chromium.base.ApiCompatibilityUtils; import org.chromium.base.ApplicationStatus; import org.chromium.base.BaseSwitches; import org.chromium.base.Callback; import org.chromium.base.CommandLine; import org.chromium.base.ObserverList; import org.chromium.base.SysUtils; import org.chromium.base.TraceEvent; import org.chromium.base.VisibleForTesting; import org.chromium.base.metrics.RecordHistogram; import org.chromium.base.metrics.RecordUserAction; import org.chromium.chrome.R; import org.chromium.chrome.browser.IntentHandler.IntentHandlerDelegate; import org.chromium.chrome.browser.IntentHandler.TabOpenType; import org.chromium.chrome.browser.appmenu.AppMenu; import org.chromium.chrome.browser.appmenu.AppMenuHandler; import org.chromium.chrome.browser.appmenu.AppMenuObserver; import org.chromium.chrome.browser.appmenu.AppMenuPropertiesDelegate; import org.chromium.chrome.browser.blimp.ChromeBlimpClientContextDelegate; import org.chromium.chrome.browser.bookmarks.BookmarkModel; import org.chromium.chrome.browser.bookmarks.BookmarkUtils; import org.chromium.chrome.browser.compositor.CompositorViewHolder; import org.chromium.chrome.browser.compositor.layouts.Layout; import org.chromium.chrome.browser.compositor.layouts.LayoutManager; import org.chromium.chrome.browser.compositor.layouts.LayoutManagerDocument; import org.chromium.chrome.browser.compositor.layouts.SceneChangeObserver; import org.chromium.chrome.browser.compositor.layouts.content.ContentOffsetProvider; import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager; import org.chromium.chrome.browser.contextualsearch.ContextualSearchFieldTrial; import org.chromium.chrome.browser.contextualsearch.ContextualSearchManager; import org.chromium.chrome.browser.contextualsearch.ContextualSearchManager.ContextualSearchTabPromotionDelegate; import org.chromium.chrome.browser.datausage.DataUseTabUIManager; import org.chromium.chrome.browser.device.DeviceClassManager; import org.chromium.chrome.browser.dom_distiller.DistilledPagePrefsView; import org.chromium.chrome.browser.dom_distiller.ReaderModeManager; import org.chromium.chrome.browser.download.DownloadManagerService; import org.chromium.chrome.browser.download.DownloadUtils; import org.chromium.chrome.browser.fullscreen.ChromeFullscreenManager; import org.chromium.chrome.browser.gsa.ContextReporter; import org.chromium.chrome.browser.gsa.GSAServiceClient; import org.chromium.chrome.browser.gsa.GSAState; import org.chromium.chrome.browser.help.HelpAndFeedback; import org.chromium.chrome.browser.infobar.InfoBarContainer; import org.chromium.chrome.browser.init.AsyncInitializationActivity; import org.chromium.chrome.browser.metrics.LaunchMetrics; import org.chromium.chrome.browser.metrics.StartupMetrics; import org.chromium.chrome.browser.metrics.UmaSessionStats; import org.chromium.chrome.browser.metrics.UmaUtils; import org.chromium.chrome.browser.multiwindow.MultiWindowUtils; import org.chromium.chrome.browser.nfc.BeamController; import org.chromium.chrome.browser.nfc.BeamProvider; import org.chromium.chrome.browser.offlinepages.OfflinePageBridge; import org.chromium.chrome.browser.offlinepages.OfflinePageUtils; import org.chromium.chrome.browser.omaha.UpdateMenuItemHelper; import org.chromium.chrome.browser.pageinfo.WebsiteSettingsPopup; import org.chromium.chrome.browser.partnercustomizations.PartnerBrowserCustomizations; import org.chromium.chrome.browser.preferences.ChromePreferenceManager; import org.chromium.chrome.browser.preferences.PrefServiceBridge; import org.chromium.chrome.browser.preferences.PreferencesLauncher; import org.chromium.chrome.browser.printing.PrintShareActivity; import org.chromium.chrome.browser.printing.TabPrinter; import org.chromium.chrome.browser.profiles.Profile; import org.chromium.chrome.browser.share.ShareHelper; import org.chromium.chrome.browser.snackbar.DataUseSnackbarController; import org.chromium.chrome.browser.snackbar.SnackbarManager; import org.chromium.chrome.browser.snackbar.SnackbarManager.SnackbarManageable; import org.chromium.chrome.browser.sync.ProfileSyncService; import org.chromium.chrome.browser.sync.SyncController; import org.chromium.chrome.browser.tab.Tab; import org.chromium.chrome.browser.tabmodel.AsyncTabParamsManager; import org.chromium.chrome.browser.tabmodel.EmptyTabModel; import org.chromium.chrome.browser.tabmodel.TabCreatorManager; import org.chromium.chrome.browser.tabmodel.TabModel; import org.chromium.chrome.browser.tabmodel.TabModelSelector; import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabObserver; import org.chromium.chrome.browser.tabmodel.TabModelUtils; import org.chromium.chrome.browser.tabmodel.TabWindowManager; import org.chromium.chrome.browser.toolbar.Toolbar; import org.chromium.chrome.browser.toolbar.ToolbarControlContainer; import org.chromium.chrome.browser.toolbar.ToolbarManager; import org.chromium.chrome.browser.util.ChromeFileProvider; import org.chromium.chrome.browser.util.ColorUtils; import org.chromium.chrome.browser.util.FeatureUtilities; import org.chromium.chrome.browser.webapps.AddToHomescreenManager; import org.chromium.chrome.browser.widget.ControlContainer; import org.chromium.components.bookmarks.BookmarkId; import org.chromium.content.browser.ContentVideoView; import org.chromium.content.browser.ContentViewCore; import org.chromium.content.common.ContentSwitches; import org.chromium.content_public.browser.ContentBitmapCallback; import org.chromium.content_public.browser.LoadUrlParams; import org.chromium.content_public.browser.WebContents; import org.chromium.content_public.browser.readback_types.ReadbackResponse; import org.chromium.policy.CombinedPolicyProvider; import org.chromium.policy.CombinedPolicyProvider.PolicyChangeListener; import org.chromium.printing.PrintManagerDelegateImpl; import org.chromium.printing.PrintingController; import org.chromium.printing.PrintingControllerImpl; import org.chromium.ui.base.ActivityWindowAndroid; import org.chromium.ui.base.DeviceFormFactor; import org.chromium.ui.base.PageTransition; import org.chromium.ui.base.WindowAndroid; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; /** * A {@link AsyncInitializationActivity} that builds and manages a {@link CompositorViewHolder} * and associated classes. */ public abstract class ChromeActivity extends AsyncInitializationActivity implements TabCreatorManager, AccessibilityStateChangeListener, PolicyChangeListener, ContextualSearchTabPromotionDelegate, SnackbarManageable, SceneChangeObserver { /** * Factory which creates the AppMenuHandler. */ public interface AppMenuHandlerFactory { /** * @return AppMenuHandler for the given activity and menu resource id. */ public AppMenuHandler get(Activity activity, AppMenuPropertiesDelegate delegate, int menuResourceId); } /** * No control container to inflate during initialization. */ static final int NO_CONTROL_CONTAINER = -1; private static final int RECORD_MULTI_WINDOW_SCREEN_WIDTH_DELAY_MS = 5000; /** * Timeout in ms for reading PartnerBrowserCustomizations provider. */ private static final int PARTNER_BROWSER_CUSTOMIZATIONS_TIMEOUT_MS = 10000; private static final String TAG = "ChromeActivity"; private static final Rect EMPTY_RECT = new Rect(); private TabModelSelector mTabModelSelector; private TabModelSelectorTabObserver mTabModelSelectorTabObserver; private TabCreatorManager.TabCreator mRegularTabCreator; private TabCreatorManager.TabCreator mIncognitoTabCreator; private TabContentManager mTabContentManager; private UmaSessionStats mUmaSessionStats; private ContextReporter mContextReporter; protected GSAServiceClient mGSAServiceClient; private boolean mPartnerBrowserRefreshNeeded; protected IntentHandler mIntentHandler; private boolean mDeferredStartupPosted; // The class cannot implement TouchExplorationStateChangeListener, // because it is only available for Build.VERSION_CODES.KITKAT and later. // We have to instantiate the TouchExplorationStateChangeListner object in the code. @SuppressLint("NewApi") private TouchExplorationStateChangeListener mTouchExplorationStateChangeListener; // Observes when sync becomes ready to create the mContextReporter. private ProfileSyncService.SyncStateChangedListener mSyncStateChangedListener; private ActivityWindowAndroid mWindowAndroid; private ChromeFullscreenManager mFullscreenManager; private CompositorViewHolder mCompositorViewHolder; private InsetObserverView mInsetObserverView; private ContextualSearchManager mContextualSearchManager; private ReaderModeManager mReaderModeManager; private SnackbarManager mSnackbarManager; private DataUseSnackbarController mDataUseSnackbarController; private AppMenuPropertiesDelegate mAppMenuPropertiesDelegate; private AppMenuHandler mAppMenuHandler; private ToolbarManager mToolbarManager; // Time in ms that it took took us to inflate the initial layout private long mInflateInitialLayoutDurationMs; private int mScreenWidthDp; private Runnable mRecordMultiWindowModeScreenWidthRunnable; private AssistStatusHandler mAssistStatusHandler; // A set of views obscuring all tabs. When this set is nonempty, // all tab content will be hidden from the accessibility tree. private List<View> mViewsObscuringAllTabs = new ArrayList<>(); // Callbacks to be called when a context menu is closed. private final ObserverList<Callback<Menu>> mContextMenuCloseObservers = new ObserverList<>(); private static AppMenuHandlerFactory sAppMenuHandlerFactory = new AppMenuHandlerFactory() { @Override public AppMenuHandler get( Activity activity, AppMenuPropertiesDelegate delegate, int menuResourceId) { return new AppMenuHandler(activity, delegate, menuResourceId); } }; // See enableHardwareAcceleration() private boolean mSetWindowHWA; // Chrome delegate that includes functionalities needed by Blimp client. private ChromeBlimpClientContextDelegate mBlimpClientContextDelegate; // Skips capturing screenshot for testing purpose. private boolean mScreenshotCaptureSkippedForTesting; /** * @param The {@link AppMenuHandlerFactory} for creating {@link mAppMenuHandler} */ @VisibleForTesting public static void setAppMenuHandlerFactoryForTesting(AppMenuHandlerFactory factory) { sAppMenuHandlerFactory = factory; } @Override public void preInflationStartup() { super.preInflationStartup(); // Force a partner customizations refresh if it has yet to be initialized. This can happen // if Chrome is killed and you refocus a previous activity from Android recents, which does // not go through ChromeLauncherActivity that would have normally triggered this. mPartnerBrowserRefreshNeeded = !PartnerBrowserCustomizations.isInitialized(); ApplicationInitialization.enableFullscreenFlags( getResources(), this, getControlContainerHeightResource()); getWindow().setBackgroundDrawable(getBackgroundDrawable()); mWindowAndroid = new ChromeWindow(this); mWindowAndroid.restoreInstanceState(getSavedInstanceState()); } @SuppressLint("NewApi") @Override public void postInflationStartup() { super.postInflationStartup(); mSnackbarManager = new SnackbarManager(this); mDataUseSnackbarController = new DataUseSnackbarController(this, getSnackbarManager()); mAssistStatusHandler = createAssistStatusHandler(); if (mAssistStatusHandler != null) { if (mTabModelSelector != null) { mAssistStatusHandler.setTabModelSelector(mTabModelSelector); } mAssistStatusHandler.updateAssistState(); } // If a user had ALLOW_LOW_END_DEVICE_UI explicitly set to false then we manually override // SysUtils.isLowEndDevice() with a switch so that they continue to see the normal UI. This // is only the case for grandfathered-in svelte users. We no longer do so for newer users. if (!ChromePreferenceManager.getInstance(this).getAllowLowEndDeviceUi()) { CommandLine.getInstance().appendSwitch( BaseSwitches.DISABLE_LOW_END_DEVICE_MODE); } AccessibilityManager manager = (AccessibilityManager) getBaseContext().getSystemService(Context.ACCESSIBILITY_SERVICE); manager.addAccessibilityStateChangeListener(this); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { mTouchExplorationStateChangeListener = new TouchExplorationStateChangeListener() { @Override public void onTouchExplorationStateChanged(boolean enabled) { checkAccessibility(); } }; manager.addTouchExplorationStateChangeListener(mTouchExplorationStateChangeListener); } // Make the activity listen to policy change events CombinedPolicyProvider.get().addPolicyChangeListener(this); // Set up the animation placeholder to be the SurfaceView. This disables the // SurfaceView's 'hole' clipping during animations that are notified to the window. mWindowAndroid.setAnimationPlaceholderView(mCompositorViewHolder.getSurfaceView()); // Inform the WindowAndroid of the keyboard accessory view. mWindowAndroid.setKeyboardAccessoryView((ViewGroup) findViewById(R.id.keyboard_accessory)); initializeToolbar(); } @Override protected View getViewToBeDrawnBeforeInitializingNative() { View controlContainer = findViewById(R.id.control_container); return controlContainer != null ? controlContainer : super.getViewToBeDrawnBeforeInitializingNative(); } /** * This function builds the {@link CompositorViewHolder}. Subclasses *must* call * super.setContentView() before using {@link #getTabModelSelector()} or * {@link #getCompositorViewHolder()}. */ @Override protected final void setContentView() { final long begin = SystemClock.elapsedRealtime(); TraceEvent.begin("onCreate->setContentView()"); enableHardwareAcceleration(); setLowEndTheme(); int controlContainerLayoutId = getControlContainerLayoutId(); WarmupManager warmupManager = WarmupManager.getInstance(); if (warmupManager.hasBuiltOrClearViewHierarchyWithToolbar(controlContainerLayoutId)) { View placeHolderView = new View(this); setContentView(placeHolderView); ViewGroup contentParent = (ViewGroup) placeHolderView.getParent(); WarmupManager.getInstance().transferViewHierarchyTo(contentParent); contentParent.removeView(placeHolderView); } else { // Allow disk access for the content view and toolbar container setup. // On certain android devices this setup sequence results in disk writes outside // of our control, so we have to disable StrictMode to work. See crbug.com/639352. StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); try { setContentView(R.layout.main); if (controlContainerLayoutId != NO_CONTROL_CONTAINER) { ViewStub toolbarContainerStub = ((ViewStub) findViewById(R.id.control_container_stub)); toolbarContainerStub.setLayoutResource(controlContainerLayoutId); toolbarContainerStub.inflate(); } } finally { StrictMode.setThreadPolicy(oldPolicy); } } TraceEvent.end("onCreate->setContentView()"); mInflateInitialLayoutDurationMs = SystemClock.elapsedRealtime() - begin; // Set the status bar color to black by default. This is an optimization for // Chrome not to draw under status and navigation bars when we use the default // black status bar ApiCompatibilityUtils.setStatusBarColor(getWindow(), Color.BLACK); ViewGroup rootView = (ViewGroup) getWindow().getDecorView().getRootView(); mCompositorViewHolder = (CompositorViewHolder) findViewById(R.id.compositor_view_holder); mCompositorViewHolder.setRootView(rootView); // Setting fitsSystemWindows to false ensures that the root view doesn't consume the insets. rootView.setFitsSystemWindows(false); // Add a custom view right after the root view that stores the insets to access later. // ContentViewCore needs the insets to determine the portion of the screen obscured by // non-content displaying things such as the OSK. mInsetObserverView = InsetObserverView.create(this); rootView.addView(mInsetObserverView, 0); } /** * Constructs {@link ToolbarManager} and the handler necessary for controlling the menu on the * {@link Toolbar}. Extending classes can override this call to avoid creating the toolbar. */ protected void initializeToolbar() { final View controlContainer = findViewById(R.id.control_container); assert controlContainer != null; ToolbarControlContainer toolbarContainer = (ToolbarControlContainer) controlContainer; mAppMenuPropertiesDelegate = createAppMenuPropertiesDelegate(); mAppMenuHandler = sAppMenuHandlerFactory.get(this, mAppMenuPropertiesDelegate, getAppMenuLayoutId()); mToolbarManager = new ToolbarManager(this, toolbarContainer, mAppMenuHandler, mAppMenuPropertiesDelegate, getCompositorViewHolder().getInvalidator()); mAppMenuHandler.addObserver(new AppMenuObserver() { @Override public void onMenuVisibilityChanged(boolean isVisible) { if (isVisible && !isInOverviewMode()) { // The app menu badge should be removed the first time the menu is opened. if (mToolbarManager.getToolbar().isShowingAppMenuUpdateBadge()) { mToolbarManager.getToolbar().removeAppMenuUpdateBadge(true); mCompositorViewHolder.requestRender(); } } if (!isVisible) { mAppMenuPropertiesDelegate.onMenuDismissed(); MenuItem updateMenuItem = mAppMenuHandler.getAppMenu().getMenu().findItem( R.id.update_menu_id); if (updateMenuItem != null && updateMenuItem.isVisible()) { UpdateMenuItemHelper.getInstance().onMenuDismissed(); } } } }); } /** * @return {@link ToolbarManager} that belongs to this activity. */ @VisibleForTesting public ToolbarManager getToolbarManager() { return mToolbarManager; } /** * @return The resource id for the menu to use in {@link AppMenu}. Default is R.menu.main_menu. */ protected int getAppMenuLayoutId() { return R.menu.main_menu; } /** * @return {@link AppMenuPropertiesDelegate} instance that the {@link AppMenuHandler} * should be using in this activity. */ protected AppMenuPropertiesDelegate createAppMenuPropertiesDelegate() { return new AppMenuPropertiesDelegate(this); } /** * @return The assist handler for this activity. */ protected AssistStatusHandler getAssistStatusHandler() { return mAssistStatusHandler; } /** * @return A newly constructed assist handler for this given activity type. */ protected AssistStatusHandler createAssistStatusHandler() { return new AssistStatusHandler(this); } /** * @return The resource id for the layout to use for {@link ControlContainer}. 0 by default. */ protected int getControlContainerLayoutId() { return NO_CONTROL_CONTAINER; } /** * @return Whether contextual search is allowed for this activity or not. */ protected boolean isContextualSearchAllowed() { return true; } @Override public void initializeState() { super.initializeState(); mBlimpClientContextDelegate = ChromeBlimpClientContextDelegate.createAndSetDelegateForContext( Profile.getLastUsedProfile().getOriginalProfile()); IntentHandler.setTestIntentsEnabled( CommandLine.getInstance().hasSwitch(ContentSwitches.ENABLE_TEST_INTENTS)); mIntentHandler = new IntentHandler(createIntentHandlerDelegate(), getPackageName()); } @Override public void initializeCompositor() { TraceEvent.begin("ChromeActivity:CompositorInitialization"); super.initializeCompositor(); setTabContentManager(new TabContentManager(this, getContentOffsetProvider(), DeviceClassManager.enableSnapshots())); mCompositorViewHolder.onNativeLibraryReady(mWindowAndroid, getTabContentManager()); if (isContextualSearchAllowed() && ContextualSearchFieldTrial.isEnabled()) { mContextualSearchManager = new ContextualSearchManager(this, mWindowAndroid, this); } if (ReaderModeManager.isEnabled(this)) { mReaderModeManager = new ReaderModeManager(getTabModelSelector(), this); if (mToolbarManager != null) { mToolbarManager.addFindToolbarObserver( mReaderModeManager.getFindToolbarObserver()); } } TraceEvent.end("ChromeActivity:CompositorInitialization"); } /** * Sets the {@link TabModelSelector} owned by this {@link ChromeActivity}. * @param tabModelSelector A {@link TabModelSelector} instance. */ protected void setTabModelSelector(TabModelSelector tabModelSelector) { mTabModelSelector = tabModelSelector; if (mTabModelSelectorTabObserver != null) mTabModelSelectorTabObserver.destroy(); mTabModelSelectorTabObserver = new TabModelSelectorTabObserver(tabModelSelector) { @Override public void didFirstVisuallyNonEmptyPaint(Tab tab) { if (DataUseTabUIManager.checkAndResetDataUseTrackingStarted(tab) && DataUseTabUIManager.shouldShowDataUseStartedUI()) { mDataUseSnackbarController.showDataUseTrackingStartedBar(); } else if (DataUseTabUIManager.shouldShowDataUseEndedUI() && DataUseTabUIManager.shouldShowDataUseEndedSnackbar( getApplicationContext()) && DataUseTabUIManager.checkAndResetDataUseTrackingEnded(tab)) { mDataUseSnackbarController.showDataUseTrackingEndedBar(); } } @Override public void onShown(Tab tab) { setStatusBarColor(tab, tab.getThemeColor()); } @Override public void onHidden(Tab tab) { mDataUseSnackbarController.dismissDataUseBar(); } @Override public void onDestroyed(Tab tab) { mDataUseSnackbarController.dismissDataUseBar(); } @Override public void onLoadStopped(Tab tab, boolean toDifferentDocument) { postDeferredStartupIfNeeded(); } @Override public void onPageLoadFinished(Tab tab) { postDeferredStartupIfNeeded(); OfflinePageUtils.showOfflineSnackbarIfNecessary(ChromeActivity.this, tab); } @Override public void onCrash(Tab tab, boolean sadTabShown) { postDeferredStartupIfNeeded(); } @Override public void onDidChangeThemeColor(Tab tab, int color) { if (getActivityTab() != tab) return; setStatusBarColor(tab, color); if (getToolbarManager() == null) return; getToolbarManager().updatePrimaryColor(color, true); ControlContainer controlContainer = (ControlContainer) findViewById(R.id.control_container); controlContainer.getToolbarResourceAdapter().invalidate(null); } }; if (mAssistStatusHandler != null) { mAssistStatusHandler.setTabModelSelector(tabModelSelector); } } @Override public void onStartWithNative() { super.onStartWithNative(); UpdateMenuItemHelper.getInstance().onStart(); ChromeActivitySessionTracker.getInstance().onStartWithNative(); if (GSAState.getInstance(this).isGsaAvailable()) { mGSAServiceClient = new GSAServiceClient(this); mGSAServiceClient.connect(); createContextReporterIfNeeded(); } else { ContextReporter.reportStatus(ContextReporter.STATUS_GSA_NOT_AVAILABLE); } mCompositorViewHolder.resetFlags(); // postDeferredStartupIfNeeded() is called in TabModelSelectorTabObsever#onLoadStopped(), // #onPageLoadFinished() and #onCrash(). If we are not actively loading a tab (e.g. // in Android N multi-instance, which is created by re-parenting an existing tab), // ensure onDeferredStartup() gets called by calling postDeferredStartupIfNeeded() here. if (getActivityTab() == null || !getActivityTab().isLoading()) { postDeferredStartupIfNeeded(); } } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); Tab tab = getActivityTab(); if (tab == null) return; if (hasFocus) { tab.onActivityShown(); } else { boolean stopped = ApplicationStatus.getStateForActivity(this) == ActivityState.STOPPED; if (stopped) tab.onActivityHidden(); } } /** * Set device status bar to a given color. * @param tab The tab that is currently showing. * @param color The color that the status bar should be set to. */ protected void setStatusBarColor(Tab tab, int color) { int statusBarColor = (tab != null && tab.isDefaultThemeColor()) ? Color.BLACK : ColorUtils.getDarkenedColorForStatusBar(color); ApiCompatibilityUtils.setStatusBarColor(getWindow(), statusBarColor); } private void createContextReporterIfNeeded() { if (mContextReporter != null || getActivityTab() == null) return; final SyncController syncController = SyncController.get(this); final ProfileSyncService syncService = ProfileSyncService.get(); if (syncController != null && syncController.isSyncingUrlsWithKeystorePassphrase()) { assert syncService != null; mContextReporter = ((ChromeApplication) getApplicationContext()).createGsaHelper() .getContextReporter(this); if (mSyncStateChangedListener != null) { syncService.removeSyncStateChangedListener(mSyncStateChangedListener); mSyncStateChangedListener = null; } return; } else { ContextReporter.reportSyncStatus(syncService); } if (mSyncStateChangedListener == null && syncService != null) { mSyncStateChangedListener = new ProfileSyncService.SyncStateChangedListener() { @Override public void syncStateChanged() { createContextReporterIfNeeded(); } }; syncService.addSyncStateChangedListener(mSyncStateChangedListener); } } @Override public void onResumeWithNative() { super.onResumeWithNative(); markSessionResume(); RecordUserAction.record("MobileComeToForeground"); if (getActivityTab() != null) { LaunchMetrics.commitLaunchMetrics(getActivityTab().getWebContents()); } FeatureUtilities.setCustomTabVisible(isCustomTab()); FeatureUtilities.setIsInMultiWindowMode( MultiWindowUtils.getInstance().isInMultiWindowMode(this)); } @Override public void onPauseWithNative() { RecordUserAction.record("MobileGoToBackground"); Tab tab = getActivityTab(); if (tab != null) { getTabContentManager().cacheTabThumbnail(tab); } markSessionEnd(); super.onPauseWithNative(); } @Override public void onStopWithNative() { Tab tab = getActivityTab(); if (tab != null && !hasWindowFocus()) tab.onActivityHidden(); if (mAppMenuHandler != null) mAppMenuHandler.hideAppMenu(); if (mGSAServiceClient != null) { mGSAServiceClient.disconnect(); mGSAServiceClient = null; if (mSyncStateChangedListener != null) { ProfileSyncService syncService = ProfileSyncService.get(); if (syncService != null) { syncService.removeSyncStateChangedListener(mSyncStateChangedListener); } mSyncStateChangedListener = null; } } super.onStopWithNative(); } @Override public void onNewIntentWithNative(Intent intent) { super.onNewIntentWithNative(intent); if (mIntentHandler.shouldIgnoreIntent(this, intent)) return; mIntentHandler.onNewIntent(this, intent); } /** * @return Whether the given activity contains a CustomTab. */ public boolean isCustomTab() { return false; } /** * Overriding methods should queue tasks on the DeferredStartupHandler before or after calling * super depending on whether the tasks should run before or after these ones. */ @Override protected void onDeferredStartup() { super.onDeferredStartup(); initDeferredStartupForActivity(); DeferredStartupHandler.getInstance().initDeferredStartupForApp(); DeferredStartupHandler.getInstance().queueDeferredTasksOnIdleHandler(); } /** * All deferred startup tasks that require the activity rather than the app should go here. */ private void initDeferredStartupForActivity() { DeferredStartupHandler.getInstance().addDeferredTask(new Runnable() { @Override public void run() { if (isActivityDestroyed()) return; BeamController.registerForBeam(ChromeActivity.this, new BeamProvider() { @Override public String getTabUrlForBeam() { if (isOverlayVisible()) return null; if (getActivityTab() == null) return null; return getActivityTab().getUrl(); } }); UpdateMenuItemHelper.getInstance().checkForUpdateOnBackgroundThread( ChromeActivity.this); } }); DeferredStartupHandler.getInstance().addDeferredTask(new Runnable() { @Override public void run() { if (isActivityDestroyed()) return; if (mToolbarManager != null) { String simpleName = getClass().getSimpleName(); RecordHistogram.recordTimesHistogram( "MobileStartup.ToolbarInflationTime." + simpleName, mInflateInitialLayoutDurationMs, TimeUnit.MILLISECONDS); mToolbarManager.onDeferredStartup(getOnCreateTimestampMs(), simpleName); } if (MultiWindowUtils.getInstance().isInMultiWindowMode(ChromeActivity.this)) { onDeferredStartupForMultiWindowMode(); } } }); } /** * Actions that may be run at some point after startup for Android N multi-window mode. Should * be called from #onDeferredStartup() if the activity is in multi-window mode. */ protected void onDeferredStartupForMultiWindowMode() { // If the Activity was launched in multi-window mode, record a user action and the screen // width. recordMultiWindowModeChangedUserAction(true); recordMultiWindowModeScreenWidth(); } @Override public void onStart() { if (AsyncTabParamsManager.hasParamsWithTabToReparent()) { mCompositorViewHolder.prepareForTabReparenting(); } super.onStart(); if (mContextReporter != null) mContextReporter.enable(); if (mPartnerBrowserRefreshNeeded) { mPartnerBrowserRefreshNeeded = false; PartnerBrowserCustomizations.initializeAsync(getApplicationContext(), PARTNER_BROWSER_CUSTOMIZATIONS_TIMEOUT_MS); PartnerBrowserCustomizations.setOnInitializeAsyncFinished(new Runnable() { @Override public void run() { if (PartnerBrowserCustomizations.isIncognitoDisabled()) { terminateIncognitoSession(); } } }); } if (mCompositorViewHolder != null) mCompositorViewHolder.onStart(); mSnackbarManager.onStart(); // Explicitly call checkAccessibility() so things are initialized correctly when Chrome has // been re-started after closing due to the last tab being closed when homepage is enabled. // See crbug.com/541546. checkAccessibility(); mScreenWidthDp = getResources().getConfiguration().screenWidthDp; } @Override public void onStop() { super.onStop(); if (mContextReporter != null) mContextReporter.disable(); // We want to refresh partner browser provider every onStart(). mPartnerBrowserRefreshNeeded = true; if (mCompositorViewHolder != null) mCompositorViewHolder.onStop(); mSnackbarManager.onStop(); } @Override @TargetApi(Build.VERSION_CODES.M) public void onProvideAssistContent(AssistContent outContent) { if (getAssistStatusHandler() == null || !getAssistStatusHandler().isAssistSupported()) { // No information is provided in incognito mode. return; } Tab tab = getActivityTab(); if (tab != null && !isInOverviewMode()) { outContent.setWebUri(Uri.parse(tab.getUrl())); } } @Override public long getOnCreateTimestampMs() { return super.getOnCreateTimestampMs(); } /** * This cannot be overridden in order to preserve destruction order. Override * {@link #onDestroyInternal()} instead to perform clean up tasks. */ @SuppressLint("NewApi") @Override protected final void onDestroy() { if (mReaderModeManager != null) { mReaderModeManager.destroy(); mReaderModeManager = null; } if (mContextualSearchManager != null) { mContextualSearchManager.destroy(); mContextualSearchManager = null; } if (mTabModelSelectorTabObserver != null) { mTabModelSelectorTabObserver.destroy(); mTabModelSelectorTabObserver = null; } if (mCompositorViewHolder != null) { if (mCompositorViewHolder.getLayoutManager() != null) { mCompositorViewHolder.getLayoutManager().removeSceneChangeObserver(this); } mCompositorViewHolder.shutDown(); mCompositorViewHolder = null; } onDestroyInternal(); if (mToolbarManager != null) { mToolbarManager.destroy(); mToolbarManager = null; } TabModelSelector selector = getTabModelSelector(); if (selector != null) selector.destroy(); if (mWindowAndroid != null) { mWindowAndroid.destroy(); mWindowAndroid = null; } CombinedPolicyProvider.get().removePolicyChangeListener(this); if (mTabContentManager != null) { mTabContentManager.destroy(); mTabContentManager = null; } AccessibilityManager manager = (AccessibilityManager) getBaseContext().getSystemService(Context.ACCESSIBILITY_SERVICE); manager.removeAccessibilityStateChangeListener(this); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { manager.removeTouchExplorationStateChangeListener(mTouchExplorationStateChangeListener); } if (mBlimpClientContextDelegate != null) { mBlimpClientContextDelegate.destroy(); mBlimpClientContextDelegate = null; } super.onDestroy(); } /** * Override this to perform destruction tasks. Note that by the time this is called, the * {@link CompositorViewHolder} will be destroyed, but the {@link WindowAndroid} and * {@link TabModelSelector} will not. * <p> * After returning from this, the {@link TabModelSelector} will be destroyed followed * by the {@link WindowAndroid}. */ protected void onDestroyInternal() { } /** * This will handle passing {@link Intent} results back to the {@link WindowAndroid}. It will * return whether or not the {@link WindowAndroid} has consumed the event or not. */ @Override public boolean onActivityResultWithNative(int requestCode, int resultCode, Intent intent) { if (super.onActivityResultWithNative(requestCode, resultCode, intent)) return true; return mWindowAndroid.onActivityResult(requestCode, resultCode, intent); } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { if (mWindowAndroid != null) { if (mWindowAndroid.onRequestPermissionsResult(requestCode, permissions, grantResults)) { return; } } super.onRequestPermissionsResult(requestCode, permissions, grantResults); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); mWindowAndroid.saveInstanceState(outState); } /** * @return The unified manager for all snackbar related operations. */ @Override public SnackbarManager getSnackbarManager() { return mSnackbarManager; } protected Drawable getBackgroundDrawable() { return new ColorDrawable( ApiCompatibilityUtils.getColor(getResources(), R.color.light_background_color)); } @Override public void finishNativeInitialization() { // The window background color is used as the resizing background color in Android N+ // multi-window mode. See crbug.com/602366. if (Build.VERSION.CODENAME.equals("N") || Build.VERSION.SDK_INT > Build.VERSION_CODES.M) { getWindow().setBackgroundDrawable(new ColorDrawable( ApiCompatibilityUtils.getColor(getResources(), R.color.resizing_background_color))); } else { removeWindowBackground(); } DownloadManagerService.getDownloadManagerService( getApplicationContext()).onActivityLaunched(); super.finishNativeInitialization(); } /** * Called when the accessibility status of this device changes. This might be triggered by * touch exploration or general accessibility status updates. It is an aggregate of two other * accessibility update methods. * @see #onAccessibilityModeChanged(boolean) * @see #onTouchExplorationStateChanged(boolean) * @param enabled Whether or not accessibility and touch exploration are currently enabled. */ protected void onAccessibilityModeChanged(boolean enabled) { InfoBarContainer.setIsAllowedToAutoHide(!enabled); if (mToolbarManager != null) mToolbarManager.onAccessibilityStatusChanged(enabled); if (mContextualSearchManager != null) { mContextualSearchManager.onAccessibilityModeChanged(enabled); } } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item != null && onMenuOrKeyboardAction(item.getItemId(), true)) { return true; } return super.onOptionsItemSelected(item); } @VisibleForTesting public void setScreenshotCaptureSkippedForTesting(boolean value) { mScreenshotCaptureSkippedForTesting = value; } /** * Triggered when the share menu item is selected. * This creates and shows a share intent picker dialog or starts a share intent directly. * @param shareDirectly Whether it should share directly with the activity that was most * recently used to share. * @param isIncognito Whether currentTab is incognito. */ @VisibleForTesting public void onShareMenuItemSelected(final boolean shareDirectly, final boolean isIncognito) { final Tab currentTab = getActivityTab(); if (currentTab == null) return; PrintingController printingController = PrintingControllerImpl.getInstance(); if (printingController != null && !currentTab.isNativePage() && !printingController.isBusy() && PrefServiceBridge.getInstance().isPrintingEnabled()) { PrintShareActivity.enablePrintShareOption(this, new Runnable() { @Override public void run() { triggerShare(currentTab, shareDirectly, isIncognito); } }); return; } triggerShare(currentTab, shareDirectly, isIncognito); } private void triggerShare( final Tab currentTab, final boolean shareDirectly, boolean isIncognito) { final Activity mainActivity = this; WebContents webContents = currentTab.getWebContents(); RecordHistogram.recordBooleanHistogram( "OfflinePages.SharedPageWasOffline", currentTab.isOfflinePage()); boolean canShareOfflinePage = OfflinePageBridge.isPageSharingEnabled(); // Share an empty blockingUri in place of screenshot file. The file ready notification is // sent by onScreenshotReady call below when the file is written. final Uri blockingUri = (isIncognito || webContents == null) ? null : ChromeFileProvider.generateUriAndBlockAccess(mainActivity); if (canShareOfflinePage) { OfflinePageUtils.shareOfflinePage(shareDirectly, true, mainActivity, null, blockingUri, null, currentTab); } else { ShareHelper.share(shareDirectly, true, mainActivity, currentTab.getTitle(), null, currentTab.getUrl(), null, blockingUri, null); if (shareDirectly) { RecordUserAction.record("MobileMenuDirectShare"); } else { RecordUserAction.record("MobileMenuShare"); } } if (blockingUri == null) return; // Start screenshot capture and notify the provider when it is ready. ContentBitmapCallback callback = new ContentBitmapCallback() { @Override public void onFinishGetBitmap(Bitmap bitmap, int response) { ShareHelper.saveScreenshotToDisk(bitmap, mainActivity, new Callback<Uri>() { @Override public void onResult(Uri result) { // Unblock the file once it is saved to disk. ChromeFileProvider.notifyFileReady(blockingUri, result); } }); } }; if (!mScreenshotCaptureSkippedForTesting) { webContents.getContentBitmapAsync(Bitmap.Config.ARGB_8888, 1.f, EMPTY_RECT, callback); } else { callback.onFinishGetBitmap(null, ReadbackResponse.SURFACE_UNAVAILABLE); } } /** * @return Whether the activity is in overview mode. */ public boolean isInOverviewMode() { return false; } /** * @return Whether the app menu should be shown. */ public boolean shouldShowAppMenu() { // Do not show the menu if Contextual Search or Reader Mode panel is opened. // TODO(mdjones): This could potentially be handled by the OverlayPanelManager or as // an event if the panels were SceneOverlays. if ((mContextualSearchManager != null && mContextualSearchManager.isSearchPanelOpened()) || (mReaderModeManager != null && mReaderModeManager.isPanelOpened())) { return false; } return true; } /** * Shows the app menu (if possible) for a key press on the keyboard with the correct anchor view * chosen depending on device configuration and the visible menu button to the user. */ protected void showAppMenuForKeyboardEvent() { if (getAppMenuHandler() == null) return; boolean hasPermanentMenuKey = ViewConfiguration.get(this).hasPermanentMenuKey(); getAppMenuHandler().showAppMenu( hasPermanentMenuKey ? null : getToolbarManager().getMenuAnchor(), false); } /** * Allows Activities that extend ChromeActivity to do additional hiding/showing of menu items. * @param menu Menu that is going to be shown when the menu button is pressed. */ public void prepareMenu(Menu menu) { } protected IntentHandlerDelegate createIntentHandlerDelegate() { return new IntentHandlerDelegate() { @Override public void processWebSearchIntent(String query) { Intent searchIntent = new Intent(Intent.ACTION_WEB_SEARCH); searchIntent.putExtra(SearchManager.QUERY, query); startActivity(searchIntent); } @Override public void processUrlViewIntent(String url, String referer, String headers, TabOpenType tabOpenType, String externalAppId, int tabIdToBringToFront, boolean hasUserGesture, Intent intent) { } }; } /** * @return The resource id that contains how large the top controls are. */ public int getControlContainerHeightResource() { return R.dimen.control_container_height; } @Override public final void onAccessibilityStateChanged(boolean enabled) { checkAccessibility(); } private void checkAccessibility() { onAccessibilityModeChanged(DeviceClassManager.isAccessibilityModeEnabled(this)); } /** * @return A casted version of {@link #getApplication()}. */ public ChromeApplication getChromeApplication() { return (ChromeApplication) getApplication(); } /** * Add the specified tab to bookmarks or allows to edit the bookmark if the specified tab is * already bookmarked. If a new bookmark is added, a snackbar will be shown. * @param tabToBookmark The tab that needs to be bookmarked. */ public void addOrEditBookmark(final Tab tabToBookmark) { if (tabToBookmark == null || tabToBookmark.isFrozen()) { return; } // Defense in depth against the UI being erroneously enabled. if (!mToolbarManager.getBookmarkBridge().isEditBookmarksEnabled()) { assert false; return; } // Note the use of getUserBookmarkId() over getBookmarkId() here: Managed bookmarks can't be // edited. If the current URL is only bookmarked by managed bookmarks, this will return // INVALID_BOOKMARK_ID, so the code below will fall back on adding a new bookmark instead. // TODO(bauerb): This does not take partner bookmarks into account. final long bookmarkId = tabToBookmark.getUserBookmarkId(); final BookmarkModel bookmarkModel = new BookmarkModel(); bookmarkModel.runAfterBookmarkModelLoaded(new Runnable() { @Override public void run() { // Gives up the bookmarking if the tab is being destroyed. if (!tabToBookmark.isClosing() && tabToBookmark.isInitialized()) { // The BookmarkModel will be destroyed by BookmarkUtils#addOrEditBookmark() when // done. BookmarkId newBookmarkId = BookmarkUtils.addOrEditBookmark(bookmarkId, bookmarkModel, tabToBookmark, getSnackbarManager(), ChromeActivity.this); // If a new bookmark was created, try to save an offline page for it. if (newBookmarkId != null && newBookmarkId.getId() != bookmarkId) { OfflinePageUtils.saveBookmarkOffline(newBookmarkId, tabToBookmark); } } else { bookmarkModel.destroy(); } } }); } /** * {@link TabModelSelector} no longer implements TabModel. Use getTabModelSelector() or * getCurrentTabModel() depending on your needs. * @return The {@link TabModelSelector}, possibly null. */ public TabModelSelector getTabModelSelector() { return mTabModelSelector; } /** * Returns the {@link InsetObserverView} that has the current system window * insets information. * @return The {@link InsetObserverView}, possibly null. */ public InsetObserverView getInsetObserverView() { return mInsetObserverView; } @Override public TabCreatorManager.TabCreator getTabCreator(boolean incognito) { return incognito ? mIncognitoTabCreator : mRegularTabCreator; } /** * Sets the {@link org.chromium.chrome.browser.tabmodel.TabCreatorManager.TabCreator}s owned by * this {@link ChromeActivity}. * @param regularTabCreator The creator for normal tabs. * @param incognitoTabCreator The creator for incognito tabs. */ protected void setTabCreators(TabCreatorManager.TabCreator regularTabCreator, TabCreatorManager.TabCreator incognitoTabCreator) { mRegularTabCreator = regularTabCreator; mIncognitoTabCreator = incognitoTabCreator; } /** * Convenience method that returns a tab creator for the currently selected {@link TabModel}. * @return A tab creator for the currently selected {@link TabModel}. */ public TabCreatorManager.TabCreator getCurrentTabCreator() { return getTabCreator(getTabModelSelector().isIncognitoSelected()); } /** * Gets the {@link TabContentManager} instance which holds snapshots of the tabs in this model. * @return The thumbnail cache, possibly null. */ public TabContentManager getTabContentManager() { return mTabContentManager; } /** * Sets the {@link TabContentManager} owned by this {@link ChromeActivity}. * @param tabContentManager A {@link TabContentManager} instance. */ protected void setTabContentManager(TabContentManager tabContentManager) { mTabContentManager = tabContentManager; } /** * Gets the current (inner) TabModel. This is a convenience function for * getModelSelector().getCurrentModel(). It is *not* equivalent to the former getModel() * @return Never null, if modelSelector or its field is uninstantiated returns a * {@link EmptyTabModel} singleton */ public TabModel getCurrentTabModel() { TabModelSelector modelSelector = getTabModelSelector(); if (modelSelector == null) return EmptyTabModel.getInstance(); return modelSelector.getCurrentModel(); } /** * Returns the tab being displayed by this ChromeActivity instance. This allows differentiation * between ChromeActivity subclasses that swap between multiple tabs (e.g. ChromeTabbedActivity) * and subclasses that only display one Tab (e.g. FullScreenActivity and DocumentActivity). * * The default implementation grabs the tab currently selected by the TabModel, which may be * null if the Tab does not exist or the system is not initialized. */ public Tab getActivityTab() { return TabModelUtils.getCurrentTab(getCurrentTabModel()); } /** * @return The current ContentViewCore, or null if the tab does not exist or is not showing a * ContentViewCore. */ public ContentViewCore getCurrentContentViewCore() { return TabModelUtils.getCurrentContentViewCore(getCurrentTabModel()); } /** * @return A {@link WindowAndroid} instance. */ public WindowAndroid getWindowAndroid() { return mWindowAndroid; } /** * @return A {@link CompositorViewHolder} instance. */ public CompositorViewHolder getCompositorViewHolder() { return mCompositorViewHolder; } /** * Gets the full screen manager. * @return The fullscreen manager, possibly null */ public ChromeFullscreenManager getFullscreenManager() { return mFullscreenManager; } /** * @return The content offset provider, may be null. */ public ContentOffsetProvider getContentOffsetProvider() { return mCompositorViewHolder.getContentOffsetProvider(); } /** * @return The {@code ContextualSearchManager} or {@code null} if none; */ public ContextualSearchManager getContextualSearchManager() { return mContextualSearchManager; } /** * @return The {@code ReaderModeManager} or {@code null} if none; */ @VisibleForTesting public ReaderModeManager getReaderModeManager() { return mReaderModeManager; } /** * Create a full-screen manager to be used by this activity. * @param controlContainer The control container that will be controlled by the full-screen * manager. * @return A {@link ChromeFullscreenManager} instance that's been created. */ protected ChromeFullscreenManager createFullscreenManager(ControlContainer controlContainer) { return new ChromeFullscreenManager(this, controlContainer, getTabModelSelector(), getControlContainerHeightResource(), true); } /** * Exits the fullscreen mode, if any. Does nothing if no fullscreen is present. * @return Whether the fullscreen mode is currently showing. */ protected boolean exitFullscreenIfShowing() { ContentVideoView view = ContentVideoView.getContentVideoView(); if (view != null && view.getContext() == this) { view.exitFullscreen(false); return true; } if (getFullscreenManager() != null && getFullscreenManager().getPersistentFullscreenMode()) { getFullscreenManager().setPersistentFullscreenMode(false); return true; } return false; } /** * Initializes the {@link CompositorViewHolder} with the relevant content it needs to properly * show content on the screen. * @param layoutManager A {@link LayoutManagerDocument} instance. This class is * responsible for driving all high level screen content and * determines which {@link Layout} is shown when. * @param urlBar The {@link View} representing the URL bar (must be * focusable) or {@code null} if none exists. * @param contentContainer A {@link ViewGroup} that can have content attached by * {@link Layout}s. * @param controlContainer A {@link ControlContainer} instance to draw. */ protected void initializeCompositorContent( LayoutManagerDocument layoutManager, View urlBar, ViewGroup contentContainer, ControlContainer controlContainer) { if (controlContainer != null) { mFullscreenManager = createFullscreenManager(controlContainer); } if (mContextualSearchManager != null) { mContextualSearchManager.initialize(contentContainer); mContextualSearchManager.setSearchContentViewDelegate(layoutManager); } layoutManager.addSceneChangeObserver(this); mCompositorViewHolder.setLayoutManager(layoutManager); mCompositorViewHolder.setFocusable(false); mCompositorViewHolder.setControlContainer(controlContainer); mCompositorViewHolder.setFullscreenHandler(mFullscreenManager); mCompositorViewHolder.setUrlBar(urlBar); mCompositorViewHolder.onFinishNativeInitialization(getTabModelSelector(), this, getTabContentManager(), contentContainer, mContextualSearchManager, mReaderModeManager); if (controlContainer != null && DeviceClassManager.enableToolbarSwipe()) { controlContainer.setSwipeHandler( getCompositorViewHolder().getLayoutManager().getTopSwipeHandler()); } } /** * Called when the back button is pressed. * @return Whether or not the back button was handled. */ protected abstract boolean handleBackPressed(); @Override public void onOrientationChange(int orientation) { if (mToolbarManager != null) mToolbarManager.onOrientationChange(); } @Override public void onConfigurationChanged(Configuration newConfig) { if (mAppMenuHandler != null) mAppMenuHandler.hideAppMenu(); super.onConfigurationChanged(newConfig); if (newConfig.screenWidthDp != mScreenWidthDp) { mScreenWidthDp = newConfig.screenWidthDp; final Activity activity = this; if (mRecordMultiWindowModeScreenWidthRunnable != null) { mHandler.removeCallbacks(mRecordMultiWindowModeScreenWidthRunnable); } // When exiting Android N multi-window mode, onConfigurationChanged() gets called before // isInMultiWindowMode() returns false. Delay to avoid recording width when exiting // multi-window mode. This also ensures that we don't record intermediate widths seen // only for a brief period of time. mRecordMultiWindowModeScreenWidthRunnable = new Runnable() { @Override public void run() { mRecordMultiWindowModeScreenWidthRunnable = null; if (MultiWindowUtils.getInstance().isInMultiWindowMode(activity)) { recordMultiWindowModeScreenWidth(); } } }; mHandler.postDelayed(mRecordMultiWindowModeScreenWidthRunnable, RECORD_MULTI_WINDOW_SCREEN_WIDTH_DELAY_MS); } } /** * Called by the system when the activity changes from fullscreen mode to multi-window mode * and visa-versa. * @param isInMultiWindowMode True if the activity is in multi-window mode. */ public void onMultiWindowModeChanged(boolean isInMultiWindowMode) { recordMultiWindowModeChangedUserAction(isInMultiWindowMode); if (!isInMultiWindowMode && ApplicationStatus.getStateForActivity(this) == ActivityState.RESUMED) { // Start a new UMA session when exiting multi-window mode if the activity is currently // resumed. When entering multi-window Android recents gains focus, so ChromeActivity // will get a call to onPauseWithNative(), ending the current UMA session. When exiting // multi-window, however, if ChromeActivity is resumed it stays in that state. markSessionEnd(); markSessionResume(); FeatureUtilities.setIsInMultiWindowMode( MultiWindowUtils.getInstance().isInMultiWindowMode(this)); } super.onMultiWindowModeChanged(isInMultiWindowMode); } /** * Records user actions associated with entering and exiting Android N multi-window mode * @param isInMultiWindowMode True if the activity is in multi-window mode. */ protected void recordMultiWindowModeChangedUserAction(boolean isInMultiWindowMode) { if (isInMultiWindowMode) { RecordUserAction.record("Android.MultiWindowMode.Enter"); } else { RecordUserAction.record("Android.MultiWindowMode.Exit"); } } @Override public final void onBackPressed() { RecordUserAction.record("SystemBack"); if (mCompositorViewHolder != null) { LayoutManager layoutManager = mCompositorViewHolder.getLayoutManager(); if (layoutManager != null && layoutManager.onBackPressed()) return; } ContentViewCore contentViewCore = getContentViewCore(); if (contentViewCore != null && contentViewCore.isSelectActionBarShowing()) { contentViewCore.clearSelection(); return; } if (mContextualSearchManager != null && mContextualSearchManager.onBackPressed()) return; if (handleBackPressed()) return; super.onBackPressed(); } private ContentViewCore getContentViewCore() { Tab tab = getActivityTab(); if (tab == null) return null; return tab.getContentViewCore(); } @Override public void createContextualSearchTab(String searchUrl) { Tab currentTab = getActivityTab(); if (currentTab == null) return; TabCreator tabCreator = getTabCreator(currentTab.isIncognito()); if (tabCreator == null) return; tabCreator.createNewTab( new LoadUrlParams(searchUrl, PageTransition.LINK), TabModel.TabLaunchType.FROM_LINK, getActivityTab()); } /** * @return The {@link AppMenuHandler} associated with this activity. */ @VisibleForTesting public AppMenuHandler getAppMenuHandler() { return mAppMenuHandler; } /** * @return The {@link AppMenuPropertiesDelegate} associated with this activity. */ @VisibleForTesting public AppMenuPropertiesDelegate getAppMenuPropertiesDelegate() { return mAppMenuPropertiesDelegate; } /** * Callback after UpdateMenuItemHelper#checkForUpdateOnBackgroundThread is complete. * @param updateAvailable Whether an update is available. */ public void onCheckForUpdate(boolean updateAvailable) { if (UpdateMenuItemHelper.getInstance().shouldShowToolbarBadge(this)) { mToolbarManager.getToolbar().showAppMenuUpdateBadge(); mCompositorViewHolder.requestRender(); } else { mToolbarManager.getToolbar().removeAppMenuUpdateBadge(false); } } /** * Handles menu item selection and keyboard shortcuts. * * @param id The ID of the selected menu item (defined in main_menu.xml) or * keyboard shortcut (defined in values.xml). * @param fromMenu Whether this was triggered from the menu. * @return Whether the action was handled. */ public boolean onMenuOrKeyboardAction(int id, boolean fromMenu) { if (id == R.id.preferences_id) { PreferencesLauncher.launchSettingsPage(this, null); RecordUserAction.record("MobileMenuSettings"); } else if (id == R.id.show_menu) { showAppMenuForKeyboardEvent(); } if (id == R.id.update_menu_id) { UpdateMenuItemHelper.getInstance().onMenuItemClicked(this); return true; } // All the code below assumes currentTab is not null, so return early if it is null. final Tab currentTab = getActivityTab(); if (currentTab == null) { return false; } else if (id == R.id.forward_menu_id) { if (currentTab.canGoForward()) { currentTab.goForward(); RecordUserAction.record("MobileMenuForward"); RecordUserAction.record("MobileTabClobbered"); } } else if (id == R.id.bookmark_this_page_id) { addOrEditBookmark(currentTab); RecordUserAction.record("MobileMenuAddToBookmarks"); } else if (id == R.id.offline_page_id) { DownloadUtils.downloadOfflinePage(this, currentTab); RecordUserAction.record("MobileMenuDownloadPage"); } else if (id == R.id.reload_menu_id) { if (currentTab.isLoading()) { currentTab.stopLoading(); RecordUserAction.record("MobileMenuStop"); } else { currentTab.reload(); RecordUserAction.record("MobileMenuReload"); } } else if (id == R.id.info_menu_id) { WebsiteSettingsPopup.show( this, currentTab, null, WebsiteSettingsPopup.OPENED_FROM_MENU); } else if (id == R.id.open_history_menu_id) { currentTab.loadUrl( new LoadUrlParams(UrlConstants.HISTORY_URL, PageTransition.AUTO_TOPLEVEL)); RecordUserAction.record("MobileMenuHistory"); StartupMetrics.getInstance().recordOpenedHistory(); } else if (id == R.id.share_menu_id || id == R.id.direct_share_menu_id) { onShareMenuItemSelected(id == R.id.direct_share_menu_id, getCurrentTabModel().isIncognito()); } else if (id == R.id.print_id) { PrintingController printingController = PrintingControllerImpl.getInstance(); if (printingController != null && !printingController.isBusy() && PrefServiceBridge.getInstance().isPrintingEnabled()) { printingController.startPrint(new TabPrinter(currentTab), new PrintManagerDelegateImpl(this)); RecordUserAction.record("MobileMenuPrint"); } } else if (id == R.id.add_to_homescreen_id) { AddToHomescreenManager addToHomescreenManager = new AddToHomescreenManager(this, currentTab); addToHomescreenManager.start(); RecordUserAction.record("MobileMenuAddToHomescreen"); } else if (id == R.id.request_desktop_site_id) { final boolean reloadOnChange = !currentTab.isNativePage(); final boolean usingDesktopUserAgent = currentTab.getUseDesktopUserAgent(); currentTab.setUseDesktopUserAgent(!usingDesktopUserAgent, reloadOnChange); RecordUserAction.record("MobileMenuRequestDesktopSite"); } else if (id == R.id.reader_mode_prefs_id) { if (currentTab.getWebContents() != null) { RecordUserAction.record("DomDistiller_DistilledPagePrefsOpened"); AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.AlertDialogTheme); builder.setView(DistilledPagePrefsView.create(this)); builder.show(); } } else if (id == R.id.help_id) { // Since reading back the compositor is asynchronous, we need to do the readback // before starting the GoogleHelp. String helpContextId = HelpAndFeedback.getHelpContextIdFromUrl( this, currentTab.getUrl(), getCurrentTabModel().isIncognito()); HelpAndFeedback.getInstance(this) .show(this, helpContextId, currentTab.getProfile(), currentTab.getUrl()); RecordUserAction.record("MobileMenuFeedback"); } else { return false; } return true; } /** * Tests if VR Shell (the mode displaying browser UI and tab contents in VR) is currently * enabled. */ public boolean isVrShellEnabled() { return false; } /** * Add a view to the set of views that obscure the content of all tabs for * accessibility. As long as this set is nonempty, all tabs should be * hidden from the accessibility tree. * * @param view The view that obscures the contents of all tabs. */ public void addViewObscuringAllTabs(View view) { mViewsObscuringAllTabs.add(view); Tab tab = getActivityTab(); if (tab != null) tab.updateAccessibilityVisibility(); } /** * Remove a view that previously obscured the content of all tabs. * * @param view The view that no longer obscures the contents of all tabs. */ public void removeViewObscuringAllTabs(View view) { mViewsObscuringAllTabs.remove(view); Tab tab = getActivityTab(); if (tab != null) tab.updateAccessibilityVisibility(); } /** * Returns whether or not any views obscure all tabs. */ public boolean isViewObscuringAllTabs() { return !mViewsObscuringAllTabs.isEmpty(); } private void markSessionResume() { // Start new session for UMA. if (mUmaSessionStats == null) { mUmaSessionStats = new UmaSessionStats(this); } mUmaSessionStats.updateMetricsServiceState(); mUmaSessionStats.startNewSession(getTabModelSelector()); } /** * Mark that the UMA session has ended. */ private void markSessionEnd() { if (mUmaSessionStats == null) { // If you hit this assert, please update crbug.com/172653 on how you got there. assert false; return; } // Record session metrics. mUmaSessionStats.logMultiWindowStats(windowArea(), displayArea(), TabWindowManager.getInstance().getNumberOfAssignedTabModelSelectors()); mUmaSessionStats.logAndEndSession(); } private int windowArea() { Window window = getWindow(); if (window != null) { View view = window.getDecorView(); return view.getWidth() * view.getHeight(); } return -1; } private int displayArea() { if (getResources() != null && getResources().getDisplayMetrics() != null) { DisplayMetrics metrics = getResources().getDisplayMetrics(); return metrics.heightPixels * metrics.widthPixels; } return -1; } protected final void postDeferredStartupIfNeeded() { if (!mDeferredStartupPosted) { mDeferredStartupPosted = true; RecordHistogram.recordLongTimesHistogram( "UMA.Debug.EnableCrashUpload.PostDeferredStartUptime2", SystemClock.uptimeMillis() - UmaUtils.getForegroundStartTime(), TimeUnit.MILLISECONDS); onDeferredStartup(); } } /** * Determines whether the ContentView is currently visible and not hidden by an overlay * @return true if the ContentView is fully hidden by another view (i.e. the tab stack) */ public boolean isOverlayVisible() { return false; } @Override public void terminateIncognitoSession() {} @Override public void onTabSelectionHinted(int tabId) { } @Override public void onSceneChange(Layout layout) { } @Override public void onAttachedToWindow() { super.onAttachedToWindow(); // See enableHardwareAcceleration() if (mSetWindowHWA) { mSetWindowHWA = false; getWindow().setWindowManager( getWindow().getWindowManager(), getWindow().getAttributes().token, getComponentName().flattenToString(), true /* hardwareAccelerated */); } } /** * Adds a {@link Callback} that will be triggered whenever a ContextMenu is closed. */ public void addContextMenuCloseCallback(Callback<Menu> callback) { mContextMenuCloseObservers.addObserver(callback); } @Override public void onContextMenuClosed(Menu menu) { for (Callback<Menu> callback : mContextMenuCloseObservers) { callback.onResult(menu); } } /** * Removes a {@link Callback} from the list of callbacks that will be triggered when a * ContextMenu is closed. */ public void removeContextMenuCloseCallback(Callback<Menu> callback) { mContextMenuCloseObservers.removeObserver(callback); } private boolean shouldDisableHardwareAcceleration() { // Low end devices should disable hardware acceleration for memory gains. if (SysUtils.isLowEndDevice()) return true; // Turning off hardware acceleration reduces crash rates. See http://crbug.com/651918 // GT-S7580 on JDQ39 accounts for 42% of crashes in libPowerStretch.so on dev and beta. if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1 && Build.MODEL.equals("GT-S7580")) { return true; } // SM-N9005 on JSS15J accounts for 44% of crashes in libPowerStretch.so on stable channel. if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR2 && Build.MODEL.equals("SM-N9005")) { return true; } return false; } private void enableHardwareAcceleration() { // HW acceleration is disabled in the manifest and may be re-enabled here. if (!shouldDisableHardwareAcceleration()) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); // When HW acceleration is enabled manually for an activity, child windows (e.g. // dialogs) don't inherit HW acceleration state. However, when HW acceleration is // enabled in the manifest, child windows do inherit HW acceleration state. That // looks like a bug, so I filed b/23036374 // // In the meanwhile the workaround is to call // window.setWindowManager(..., hardwareAccelerated=true) // to let the window know that it's HW accelerated. However, since there is no way // to know 'appToken' argument until window's view is attached to the window (!!), // we have to do the workaround in onAttachedToWindow() mSetWindowHWA = true; } } /** @return the theme ID to use. */ public static int getThemeId() { boolean useLowEndTheme = SysUtils.isLowEndDevice() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; return (useLowEndTheme ? R.style.MainTheme_LowEnd : R.style.MainTheme); } private void setLowEndTheme() { if (getThemeId() == R.style.MainTheme_LowEnd) setTheme(R.style.MainTheme_LowEnd); } /** * Records UMA histograms for the current screen width. Should only be called when the activity * is in Android N multi-window mode. */ protected void recordMultiWindowModeScreenWidth() { if (!DeviceFormFactor.isTablet(this)) return; RecordHistogram.recordBooleanHistogram( "Android.MultiWindowMode.IsTabletScreenWidthBelow600", mScreenWidthDp < DeviceFormFactor.MINIMUM_TABLET_WIDTH_DP); if (mScreenWidthDp < DeviceFormFactor.MINIMUM_TABLET_WIDTH_DP) { RecordHistogram.recordLinearCountHistogram( "Android.MultiWindowMode.TabletScreenWidth", mScreenWidthDp, 1, DeviceFormFactor.MINIMUM_TABLET_WIDTH_DP, 50); } } }