// 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.init; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.SystemClock; import android.provider.Settings; import android.provider.Settings.SettingNotFoundException; import android.support.v7.app.AppCompatActivity; import android.view.Display; import android.view.Surface; import android.view.View; import android.view.ViewTreeObserver; import android.view.ViewTreeObserver.OnPreDrawListener; import android.view.WindowManager; import org.chromium.base.ApiCompatibilityUtils; import org.chromium.base.TraceEvent; import org.chromium.base.VisibleForTesting; import org.chromium.base.library_loader.LoaderErrors; import org.chromium.base.library_loader.ProcessInitException; import org.chromium.chrome.browser.ChromeApplication; import org.chromium.chrome.browser.WarmupManager; import org.chromium.chrome.browser.metrics.MemoryUma; import org.chromium.chrome.browser.profiles.Profile; import org.chromium.chrome.browser.tabmodel.DocumentModeAssassin; import org.chromium.chrome.browser.upgrade.UpgradeActivity; import org.chromium.content.browser.ChildProcessCreationParams; import org.chromium.ui.base.DeviceFormFactor; import java.lang.reflect.Field; /** * An activity that talks with application and activity level delegates for async initialization. */ public abstract class AsyncInitializationActivity extends AppCompatActivity implements ChromeActivityNativeDelegate, BrowserParts { protected final Handler mHandler; // Time at which onCreate is called. This is realtime, counted in ms since device boot. private long mOnCreateTimestampMs; // Time at which onCreate is called. This is uptime, to be sent to native code. private long mOnCreateTimestampUptimeMs; private Bundle mSavedInstanceState; private int mCurrentOrientation = Surface.ROTATION_0; private boolean mDestroyed; private NativeInitializationController mNativeInitializationController; private MemoryUma mMemoryUma; private long mLastUserInteractionTime; private boolean mIsTablet; public AsyncInitializationActivity() { mHandler = new Handler(); } @Override protected void onDestroy() { mDestroyed = true; super.onDestroy(); } @Override // TODO(estevenson): Replace with Build.VERSION_CODES.N when available. @TargetApi(24) protected void attachBaseContext(Context newBase) { super.attachBaseContext(newBase); // On N+, Chrome should always retain the tab strip layout on tablets. Normally in // multi-window, if Chrome is launched into a smaller screen Android will load the tab // switcher resources. Overriding the smallestScreenWidthDp in the Configuration ensures // Android will load the tab strip resources. See crbug.com/588838. if (Build.VERSION.CODENAME.equals("N") || Build.VERSION.SDK_INT > Build.VERSION_CODES.M) { int smallestDeviceWidthDp = DeviceFormFactor.getSmallestDeviceWidthDp(this); if (smallestDeviceWidthDp >= DeviceFormFactor.MINIMUM_TABLET_WIDTH_DP) { Configuration overrideConfiguration = new Configuration(); overrideConfiguration.smallestScreenWidthDp = smallestDeviceWidthDp; applyOverrideConfiguration(overrideConfiguration); } } } @Override public void preInflationStartup() { mIsTablet = DeviceFormFactor.isTablet(this); } @Override public final void setContentViewAndLoadLibrary() { // setContentView inflating the decorView and the basic UI hierarhcy as stubs. // This is done here before kicking long running I/O because inflation includes accessing // resource files(xmls etc) even if we are inflating views defined by the framework. If this // operation gets blocked because other long running I/O are running, we delay onCreate(), // onStart() and first draw consequently. setContentView(); if (mLaunchBehindWorkaround != null) mLaunchBehindWorkaround.onSetContentView(); // Kick off long running IO tasks that can be done in parallel. mNativeInitializationController = new NativeInitializationController(this); initializeChildProcessCreationParams(); mNativeInitializationController.startBackgroundTasks(shouldAllocateChildConnection()); } /** Controls the parameter of {@link NativeInitializationController#startBackgroundTasks()}.*/ @VisibleForTesting public boolean shouldAllocateChildConnection() { return true; } /** * Allow derived classes to initialize their own {@link ChildProcessCreationParams}. */ protected void initializeChildProcessCreationParams() {} @Override public void postInflationStartup() { final View firstDrawView = getViewToBeDrawnBeforeInitializingNative(); assert firstDrawView != null; ViewTreeObserver.OnPreDrawListener firstDrawListener = new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { firstDrawView.getViewTreeObserver().removeOnPreDrawListener(this); onFirstDrawComplete(); return true; } }; firstDrawView.getViewTreeObserver().addOnPreDrawListener(firstDrawListener); } /** * @return The primary view that must have completed at least one draw before initializing * native. This must be non-null. */ protected View getViewToBeDrawnBeforeInitializingNative() { return findViewById(android.R.id.content); } @Override public void maybePreconnect() { TraceEvent.begin("maybePreconnect"); Intent intent = getIntent(); if (intent != null && Intent.ACTION_VIEW.equals(intent.getAction())) { final String url = intent.getDataString(); WarmupManager.getInstance() .maybePreconnectUrlAndSubResources(Profile.getLastUsedProfile(), url); } TraceEvent.end("maybePreconnect"); } @Override public void initializeCompositor() { } @Override public void initializeState() { } @Override public void finishNativeInitialization() { // Set up the initial orientation of the device. checkOrientation(); findViewById(android.R.id.content).addOnLayoutChangeListener( new View.OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { checkOrientation(); } }); mMemoryUma = new MemoryUma(); mNativeInitializationController.onNativeInitializationComplete(); } /** * Actions that may be run at some point after startup. Place tasks that are not critical to the * startup path here. This method will be called automatically and should not be called * directly by subclasses. */ protected void onDeferredStartup() { } @Override public void onStartupFailure() { ProcessInitException e = new ProcessInitException(LoaderErrors.LOADER_ERROR_NATIVE_STARTUP_FAILED); ChromeApplication.reportStartupErrorAndExit(e); } /** * Extending classes should override {@link AsyncInitializationActivity#preInflationStartup()}, * {@link AsyncInitializationActivity#setContentView()} and * {@link AsyncInitializationActivity#postInflationStartup()} instead of this call which will * be called on that order. */ @Override @SuppressLint("MissingSuperCall") // Called in onCreateInternal. protected final void onCreate(Bundle savedInstanceState) { TraceEvent.begin("AsyncInitializationActivity.onCreate()"); onCreateInternal(savedInstanceState); TraceEvent.end("AsyncInitializationActivity.onCreate()"); } private final void onCreateInternal(Bundle savedInstanceState) { if (DocumentModeAssassin.getInstance().isMigrationNecessary()) { super.onCreate(null); // Kick the user to the MigrationActivity. UpgradeActivity.launchInstance(this, getIntent()); // Don't remove this task -- it may be a DocumentActivity that exists only in Recents. finish(); return; } if (!isStartedUpCorrectly(getIntent())) { super.onCreate(null); ApiCompatibilityUtils.finishAndRemoveTask(this); return; } super.onCreate(savedInstanceState); mOnCreateTimestampMs = SystemClock.elapsedRealtime(); mOnCreateTimestampUptimeMs = SystemClock.uptimeMillis(); mSavedInstanceState = savedInstanceState; ChromeBrowserInitializer.getInstance(this).handlePreNativeStartup(this); } /** * Whether or not the Activity was started up via a valid Intent. */ protected boolean isStartedUpCorrectly(Intent intent) { return true; } /** * @return The elapsed real time for the activity creation in ms. */ protected long getOnCreateTimestampUptimeMs() { return mOnCreateTimestampUptimeMs; } /** * @return The uptime for the activity creation in ms. */ protected long getOnCreateTimestampMs() { return mOnCreateTimestampMs; } /** * @return The saved bundle for the last recorded state. */ protected Bundle getSavedInstanceState() { return mSavedInstanceState; } /** * Resets the saved state and makes it unavailable for the rest of the activity lifecycle. */ protected void resetSavedInstanceState() { mSavedInstanceState = null; } @Override public void onStart() { super.onStart(); mNativeInitializationController.onStart(); } @Override public void onResume() { super.onResume(); mNativeInitializationController.onResume(); if (mLaunchBehindWorkaround != null) mLaunchBehindWorkaround.onResume(); } @Override public void onPause() { mNativeInitializationController.onPause(); super.onPause(); if (mLaunchBehindWorkaround != null) mLaunchBehindWorkaround.onPause(); } @Override public void onStop() { super.onStop(); if (mMemoryUma != null) mMemoryUma.onStop(); mNativeInitializationController.onStop(); } @Override protected void onNewIntent(Intent intent) { if (intent == null) return; mNativeInitializationController.onNewIntent(intent); setIntent(intent); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { mNativeInitializationController.onActivityResult(requestCode, resultCode, data); } @Override public final void onCreateWithNative() { try { ChromeBrowserInitializer.getInstance(this).handlePostNativeStartup(true, this); } catch (ProcessInitException e) { ChromeApplication.reportStartupErrorAndExit(e); } } @Override public void onStartWithNative() { } @Override public void onResumeWithNative() { } @Override public void onPauseWithNative() { } @Override public void onStopWithNative() { } @Override public boolean isActivityDestroyed() { return mDestroyed; } @Override public boolean isActivityFinishing() { return isFinishing(); } @Override public boolean shouldStartGpuProcess() { return true; } @Override public final void onFirstDrawComplete() { mHandler.post(new Runnable() { @Override public void run() { mNativeInitializationController.firstDrawComplete(); } }); } @Override public void onNewIntentWithNative(Intent intent) { } @Override public boolean onActivityResultWithNative(int requestCode, int resultCode, Intent data) { return false; } @Override public void onLowMemory() { super.onLowMemory(); if (mMemoryUma != null) mMemoryUma.onLowMemory(); } @Override public void onTrimMemory(int level) { super.onTrimMemory(level); if (mMemoryUma != null) mMemoryUma.onTrimMemory(level); } /** * @return Whether the activity is running in tablet mode. */ public boolean isTablet() { return mIsTablet; } @Override public void onUserInteraction() { mLastUserInteractionTime = SystemClock.elapsedRealtime(); } /** * @return timestamp when the last user interaction was made. */ public long getLastUserInteractionTime() { return mLastUserInteractionTime; } /** * Called when the orientation of the device changes. The orientation is checked/detected on * root view layouts. * @param orientation One of {@link Surface#ROTATION_0} (no rotation), * {@link Surface#ROTATION_90}, {@link Surface#ROTATION_180}, or * {@link Surface#ROTATION_270}. */ protected void onOrientationChange(int orientation) { } private void checkOrientation() { WindowManager wm = getWindowManager(); if (wm == null) return; Display display = wm.getDefaultDisplay(); if (display == null) return; int oldOrientation = mCurrentOrientation; mCurrentOrientation = display.getRotation(); if (oldOrientation != mCurrentOrientation) onOrientationChange(mCurrentOrientation); } /** * Removes the window background. */ protected void removeWindowBackground() { boolean removeWindowBackground = true; try { Field field = Settings.Secure.class.getField( "ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED"); field.setAccessible(true); if (field.getType() == String.class) { String accessibilityMagnificationSetting = (String) field.get(null); // When Accessibility magnification is turned on, setting a null window // background causes the overlaid android views to stretch when panning. // (crbug/332994) if (Settings.Secure.getInt( getContentResolver(), accessibilityMagnificationSetting) == 1) { removeWindowBackground = false; } } } catch (SettingNotFoundException e) { // Window background is removed if an exception occurs. } catch (NoSuchFieldException e) { // Window background is removed if an exception occurs. } catch (IllegalAccessException e) { // Window background is removed if an exception occurs. } catch (IllegalArgumentException e) { // Window background is removed if an exception occurs. } if (removeWindowBackground) getWindow().setBackgroundDrawable(null); } /** * Extending classes should implement this and call {@link Activity#setContentView(int)} in it. */ protected abstract void setContentView(); /** * Lollipop (pre-MR1) makeTaskLaunchBehind() workaround. * * Our activity's surface is destroyed at the end of the new activity animation * when ActivityOptions.makeTaskLaunchBehind() is used, which causes a crash. * Making everything invisible when paused prevents the crash, since view changes * will not trigger draws to the missing surface. However, we need to wait until * after the first draw to make everything invisible, as the activity launch * animation needs a full frame (or it will delay the animation excessively). */ private final LaunchBehindWorkaround mLaunchBehindWorkaround = (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) ? new LaunchBehindWorkaround() : null; private class LaunchBehindWorkaround { private boolean mPaused = false; private View getDecorView() { return getWindow().getDecorView(); } private ViewTreeObserver getViewTreeObserver() { return getDecorView().getViewTreeObserver(); } private void onPause() { mPaused = true; } public void onResume() { mPaused = false; getDecorView().setVisibility(View.VISIBLE); } public void onSetContentView() { getViewTreeObserver().addOnPreDrawListener(mPreDrawListener); } // Note, we probably want onDrawListener here, but it isn't being called // when I add this to the decorView. However, it should be the same for // this purpose as long as no other pre-draw listener returns false. private final OnPreDrawListener mPreDrawListener = new OnPreDrawListener() { @Override public boolean onPreDraw() { mHandler.post(new Runnable() { @Override public void run() { if (mPaused) { getDecorView().setVisibility(View.GONE); } getViewTreeObserver().removeOnPreDrawListener(mPreDrawListener); } }); return true; } }; } }