Index: chrome/android/java_staging/src/org/chromium/chrome/browser/document/ChromeLauncherActivity.java |
diff --git a/chrome/android/java_staging/src/org/chromium/chrome/browser/document/ChromeLauncherActivity.java b/chrome/android/java_staging/src/org/chromium/chrome/browser/document/ChromeLauncherActivity.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..00dbb8cb0c6323dfc501d24a504a635daf5cac3a |
--- /dev/null |
+++ b/chrome/android/java_staging/src/org/chromium/chrome/browser/document/ChromeLauncherActivity.java |
@@ -0,0 +1,822 @@ |
+// 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.document; |
+ |
+import android.annotation.SuppressLint; |
+import android.annotation.TargetApi; |
+import android.app.Activity; |
+import android.app.ActivityManager; |
+import android.app.ActivityManager.AppTask; |
+import android.app.ActivityManager.RecentTaskInfo; |
+import android.app.ActivityOptions; |
+import android.app.Notification; |
+import android.app.PendingIntent; |
+import android.content.ClipData; |
+import android.content.Context; |
+import android.content.Intent; |
+import android.content.pm.PackageManager; |
+import android.net.Uri; |
+import android.os.Build; |
+import android.os.Bundle; |
+import android.provider.Browser; |
+import android.text.TextUtils; |
+import android.util.Base64; |
+import android.util.Log; |
+ |
+import org.chromium.base.ApiCompatibilityUtils; |
+import org.chromium.base.ApplicationStatus; |
+import org.chromium.base.CommandLine; |
+import org.chromium.chrome.browser.BookmarkUtils; |
+import org.chromium.chrome.browser.ChromeMobileApplication; |
+import org.chromium.chrome.browser.ChromeSwitches; |
+import org.chromium.chrome.browser.ChromeTabbedActivity; |
+import org.chromium.chrome.browser.IntentHandler; |
+import org.chromium.chrome.browser.IntentHandler.TabOpenType; |
+import org.chromium.chrome.browser.ShortcutHelper; |
+import org.chromium.chrome.browser.Tab; |
+import org.chromium.chrome.browser.UrlConstants; |
+import org.chromium.chrome.browser.WarmupManager; |
+import org.chromium.chrome.browser.WebappAuthenticator; |
+import org.chromium.chrome.browser.firstrun.FirstRunFlowSequencer; |
+import org.chromium.chrome.browser.hosted.HostedActivity; |
+import org.chromium.chrome.browser.metrics.LaunchHistogram; |
+import org.chromium.chrome.browser.metrics.LaunchMetrics; |
+import org.chromium.chrome.browser.multiwindow.MultiWindowUtils; |
+import org.chromium.chrome.browser.notifications.NotificationUIManager; |
+import org.chromium.chrome.browser.partnercustomizations.HomepageManager; |
+import org.chromium.chrome.browser.partnercustomizations.PartnerBrowserCustomizations; |
+import org.chromium.chrome.browser.preferences.DocumentModeManager; |
+import org.chromium.chrome.browser.tabmodel.document.ActivityDelegate; |
+import org.chromium.chrome.browser.tabmodel.document.DocumentTabModel; |
+import org.chromium.chrome.browser.tabmodel.document.DocumentTabModelSelector; |
+import org.chromium.chrome.browser.util.FeatureUtilities; |
+import org.chromium.chrome.browser.util.IntentUtils; |
+import org.chromium.chrome.browser.webapps.WebappActivity; |
+import org.chromium.content.browser.crypto.CipherFactory; |
+import org.chromium.content_public.common.ScreenOrientationValues; |
+import org.chromium.ui.base.PageTransition; |
+ |
+import java.lang.ref.WeakReference; |
+import java.util.List; |
+ |
+/** |
+ * Dispatches incoming intents to the appropriate activity based on the current configuration and |
+ * Intent fired. |
+ */ |
+public class ChromeLauncherActivity extends Activity |
+ implements IntentHandler.IntentHandlerDelegate { |
+ /** |
+ * Action fired when an Intent is trying to launch a WebappActivity. |
+ * Never change the package name or the Intents will fail to launch. |
+ */ |
+ public static final String ACTION_START_WEBAPP = |
+ "com.google.android.apps.chrome.webapps.WebappManager.ACTION_START_WEBAPP"; |
+ |
+ /** |
+ * Extra indicating that a Tab is trying to bring its WebappActivity to the foreground. |
+ * Never change the package name or the Intents will fail to launch. |
+ */ |
+ public static final String EXTRA_BRING_WEBAPP_TO_FRONT = |
+ "com.google.android.apps.chrome.EXTRA_BRING_WEBAPP_TO_FRONT"; |
+ |
+ /** |
+ * Extra indicating launch mode used. |
+ */ |
+ public static final String EXTRA_LAUNCH_MODE = |
+ "com.google.android.apps.chrome.EXTRA_LAUNCH_MODE"; |
+ |
+ /** |
+ * Action fired when the user selects the "Close all incognito tabs" notification. |
+ */ |
+ static final String ACTION_CLOSE_ALL_INCOGNITO = |
+ "com.google.android.apps.chrome.document.CLOSE_ALL_INCOGNITO"; |
+ |
+ private static final String TAG = "ChromeLauncherActivity"; |
+ |
+ /** New instance should be launched in the foreground. */ |
+ public static final int LAUNCH_MODE_FOREGROUND = 0; |
+ |
+ /** New instance should be launched as an affiliated task. */ |
+ public static final int LAUNCH_MODE_AFFILIATED = 1; |
+ |
+ /** Existing instance should be retargetted, if possible. */ |
+ public static final int LAUNCH_MODE_RETARGET = 2; |
+ |
+ private static final int FIRST_RUN_EXPERIENCE_REQUEST_CODE = 101; |
+ |
+ /** |
+ * Timeout in ms for reading PartnerBrowserCustomizations provider. We do not trust third party |
+ * provider by default. |
+ */ |
+ private static final int PARTNER_BROWSER_CUSTOMIZATIONS_TIMEOUT_MS = 10000; |
+ |
+ /** |
+ * Maximum delay for initial document activity launch. |
+ */ |
+ private static final int INITIAL_DOCUMENT_ACTIVITY_LAUNCH_TIMEOUT_MS = 500; |
+ |
+ private static final LaunchHistogram sMoveToFrontExceptionHistogram = |
+ new LaunchHistogram("DocumentActivity.MoveToFrontFailed"); |
+ |
+ private IntentHandler mIntentHandler; |
+ private boolean mIsInMultiInstanceMode; |
+ private boolean mIsFinishNeeded; |
+ |
+ /** When started with an intent, maybe pre-resolve the domain. */ |
+ private void maybePrefetchDnsInBackground() { |
+ if (getIntent() != null && Intent.ACTION_VIEW.equals(getIntent().getAction())) { |
+ String maybeUrl = IntentHandler.getUrlFromIntent(getIntent()); |
+ if (maybeUrl != null) { |
+ WarmupManager.getInstance().maybePrefetchDnsForUrlInBackground(this, maybeUrl); |
+ } |
+ } |
+ } |
+ |
+ /** |
+ * Figure out how to route the Intent. Because this is on the critical path to startup, please |
+ * avoid making the pathway any more complicated than it already is. Make sure that anything |
+ * you add _absolutely has_ to be here. |
+ */ |
+ @Override |
+ public void onCreate(Bundle savedInstanceState) { |
+ super.onCreate(savedInstanceState); |
+ |
+ // Initialize the command line in case we've disabled document mode from there. |
+ ((ChromeMobileApplication) getApplication()).initCommandLine(); |
+ |
+ // Read partner browser customizations information asynchronously. |
+ // We want to initialize early because when there is no tabs to restore, we should possibly |
+ // show homepage, which might require reading PartnerBrowserCustomizations provider. |
+ PartnerBrowserCustomizations.initializeAsync(getApplicationContext(), |
+ PARTNER_BROWSER_CUSTOMIZATIONS_TIMEOUT_MS); |
+ |
+ mIsInMultiInstanceMode = MultiWindowUtils.getInstance().shouldRunInMultiInstanceMode(this); |
+ mIntentHandler = new IntentHandler(this, getPackageName()); |
+ maybePerformMigrationTasks(); |
+ |
+ if (handleHostedActivityIntent()) { |
+ finish(); |
+ return; |
+ } |
+ |
+ // Check if we should launch a WebappActivity. |
+ if (IntentUtils.safeGetBooleanExtra(getIntent(), EXTRA_BRING_WEBAPP_TO_FRONT, false) |
+ || TextUtils.equals(getIntent().getAction(), ACTION_START_WEBAPP)) { |
+ Intent fallbackIntent = launchWebapp(getIntent()); |
+ if (fallbackIntent == null) { |
+ ApiCompatibilityUtils.finishAndRemoveTask(this); |
+ return; |
+ } else { |
+ // Try to launch the URL as a regular VIEW intent. |
+ setIntent(fallbackIntent); |
+ } |
+ } |
+ |
+ // Check if we should launch the ChromeTabbedActivity. |
+ if (!FeatureUtilities.isDocumentMode(this)) { |
+ launchTabbedMode(); |
+ finish(); |
+ return; |
+ } |
+ |
+ // Check if we're just closing all of the Incognito tabs. |
+ if (TextUtils.equals(getIntent().getAction(), ACTION_CLOSE_ALL_INCOGNITO)) { |
+ ChromeMobileApplication.getDocumentTabModelSelector().getModel(true).closeAllTabs(); |
+ ApiCompatibilityUtils.finishAndRemoveTask(this); |
+ return; |
+ } |
+ |
+ // The notification settings cog on the flipped side of Notifications and in the Android |
+ // Settings "App Notifications" view will open us with a specific category. |
+ if (getIntent().hasCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES)) { |
+ NotificationUIManager.launchNotificationPreferences(this, getIntent()); |
+ return; |
+ } |
+ |
+ // Check if we should launch the FirstRunActivity. This occurs after the check to launch |
+ // ChromeTabbedActivity because ChromeTabbedActivity handles FRE in its own way. |
+ if (launchFirstRunExperience()) return; |
+ |
+ // Launch a DocumentActivity to handle the Intent. |
+ handleDocumentActivityIntent(); |
+ if (!mIsFinishNeeded) ApiCompatibilityUtils.finishAndRemoveTask(this); |
+ } |
+ |
+ @Override |
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) { |
+ super.onActivityResult(requestCode, resultCode, data); |
+ if (requestCode == FIRST_RUN_EXPERIENCE_REQUEST_CODE) { |
+ if (resultCode == Activity.RESULT_OK) { |
+ // User might have opted out during FRE, so check again. |
+ if (FeatureUtilities.isDocumentMode(this)) { |
+ handleDocumentActivityIntent(); |
+ if (!mIsFinishNeeded) ApiCompatibilityUtils.finishAndRemoveTask(this); |
+ } else { |
+ launchTabbedMode(); |
+ finish(); |
+ } |
+ return; |
+ } |
+ |
+ // TODO(aruslan): FAIL. |
+ ApiCompatibilityUtils.finishAndRemoveTask(this); |
+ } |
+ } |
+ |
+ /** |
+ * If we have just opted in or opted out of document mode, perform pending migration tasks |
+ * such as cleaning up the recents. |
+ */ |
+ private void maybePerformMigrationTasks() { |
+ if (DocumentModeManager.getInstance(this).isOptOutCleanUpPending()) { |
+ cleanUpChromeRecents( |
+ DocumentModeManager.getInstance(this).isOptedOutOfDocumentMode()); |
+ DocumentModeManager.getInstance(this).setOptOutCleanUpPending(false); |
+ } |
+ } |
+ |
+ @Override |
+ public void processWebSearchIntent(String query) { |
+ assert false; |
+ } |
+ |
+ @Override |
+ public void processUrlViewIntent(String url, String headers, |
+ IntentHandler.TabOpenType tabOpenType, String externalAppId, |
+ int tabIdToBringToFront, Intent intent) { |
+ assert false; |
+ } |
+ |
+ /** |
+ * Handles launching an hosted activity, which will sit on top of a client's activity in the |
+ * same task. |
+ * @return True if the intent is handled here. |
+ */ |
+ private boolean handleHostedActivityIntent() { |
+ if (getIntent() == null) return false; |
+ |
+ boolean enabled = CommandLine.getInstance().hasSwitch(ChromeSwitches.ENABLE_EMBEDDED_MODE); |
+ boolean append = IntentUtils.safeGetBooleanExtra( |
+ getIntent(), IntentHandler.EXTRA_APPEND_TASK, false); |
+ if (!append || !enabled) return false; |
+ |
+ String url = IntentHandler.getUrlFromIntent(getIntent()); |
+ if (url == null) return false; |
+ |
+ // Create and fire a launch intent. Use the copy constructor to carry over the myriad of |
+ // extras. |
+ Intent newIntent = new Intent(getIntent()); |
+ newIntent.setAction(Intent.ACTION_VIEW); |
+ newIntent.setClassName(this, HostedActivity.class.getName()); |
+ newIntent.setData(Uri.parse(url)); |
+ startActivity(newIntent); |
+ return true; |
+ } |
+ |
+ /** |
+ * Handles the launching of a DocumentActivity from the current Intent. Routing Intents to |
+ * other types of Activities must be handled from onCreate() instead. |
+ */ |
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
+ private void handleDocumentActivityIntent() { |
+ if (getIntent() == null || mIntentHandler.shouldIgnoreIntent(this, getIntent())) { |
+ Log.e(TAG, "Ignoring intent: " + getIntent()); |
+ mIsFinishNeeded = true; |
+ return; |
+ } |
+ |
+ maybePrefetchDnsInBackground(); |
+ |
+ // Increment the Tab ID counter at this point since this Activity may not appear in |
+ // getAppTasks() when DocumentTabModelSelector is initialized. This can potentially happen |
+ // when Chrome is launched via the GSA/e200 search box and they relinquish their task. |
+ Tab.incrementIdCounterTo(getTaskId() + 1); |
+ |
+ // Handle MAIN Intent actions, usually fired when the user starts Chrome via the launcher. |
+ // Some launchers start Chrome by firing a VIEW Intent with an empty URL (crbug.com/459349); |
+ // treat it as a MAIN Intent. |
+ String url = IntentHandler.getUrlFromIntent(getIntent()); |
+ if ((url == null && TextUtils.equals(getIntent().getAction(), Intent.ACTION_VIEW)) |
+ || TextUtils.equals(getIntent().getAction(), Intent.ACTION_MAIN)) { |
+ handleMainDocumentIntent(); |
+ return; |
+ } |
+ |
+ // Sometimes an Intent requests that the current Document get clobbered. |
+ if (clobberCurrentDocument(url)) return; |
+ |
+ // Try to retarget existing Documents before creating a new one. |
+ boolean incognito = IntentUtils.safeGetBooleanExtra(getIntent(), |
+ IntentHandler.EXTRA_OPEN_NEW_INCOGNITO_TAB, false); |
+ boolean append = IntentUtils.safeGetBooleanExtra( |
+ getIntent(), IntentHandler.EXTRA_APPEND_TASK, false); |
+ boolean reuse = IntentUtils.safeGetBooleanExtra( |
+ getIntent(), BookmarkUtils.REUSE_URL_MATCHING_TAB_ELSE_NEW_TAB, false); |
+ boolean affiliated = IntentUtils.safeGetBooleanExtra( |
+ getIntent(), IntentHandler.EXTRA_OPEN_IN_BG, false); |
+ |
+ // Try to relaunch an existing task. |
+ if (reuse && !append) { |
+ LaunchMetrics.recordHomeScreenLaunchIntoTab(url); |
+ if (relaunchTask(incognito, url)) return; |
+ } |
+ |
+ // Create and fire a launch Intent to start a new Task. The old Intent is copied using |
+ // the constructor so that we pass through the myriad extras that were set on it. |
+ Intent newIntent = createLaunchIntent( |
+ getApplicationContext(), getIntent(), url, incognito, Tab.INVALID_TAB_ID); |
+ setRecentsFlagsOnIntent( |
+ newIntent, append ? 0 : Intent.FLAG_ACTIVITY_NEW_DOCUMENT, incognito); |
+ fireDocumentIntent(this, newIntent, incognito, url, affiliated, null); |
+ } |
+ |
+ /** |
+ * Handles actions pertaining to Chrome being started with a MAIN Intent. Typically, receiving |
+ * this Intent means that a user has selected the Chrome icon from their launcher, but it is |
+ * also used internally (e.g. when firing Intents back at Chrome via notifications). |
+ */ |
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
+ private void handleMainDocumentIntent() { |
+ // Bring a specific tab back to the foreground. |
+ int tabId = IntentUtils.safeGetIntExtra(getIntent(), |
+ TabOpenType.BRING_TAB_TO_FRONT.name(), Tab.INVALID_TAB_ID); |
+ if (tabId != Tab.INVALID_TAB_ID && relaunchTask(tabId)) return; |
+ |
+ // Bring the last viewed tab to the foreground, unless we're in Samsung's multi-instance |
+ // mode -- a MAIN Intent in that case results in the creation of a second default page. |
+ if (!mIsInMultiInstanceMode && launchLastViewedActivity()) return; |
+ |
+ // Launch the default page asynchronously because the homepage URL needs to be queried. |
+ // This is obviously not ideal, but we don't have a choice. |
+ mIsFinishNeeded = mIsInMultiInstanceMode; |
+ PartnerBrowserCustomizations.setOnInitializeAsyncFinished(new Runnable() { |
+ @Override |
+ public void run() { |
+ String url = HomepageManager.getHomepageUri(ChromeLauncherActivity.this); |
+ if (TextUtils.isEmpty(url)) url = UrlConstants.NTP_URL; |
+ |
+ int mode = mIsInMultiInstanceMode ? LAUNCH_MODE_FOREGROUND : LAUNCH_MODE_RETARGET; |
+ launchDocumentInstance(ChromeLauncherActivity.this, false, mode, url, |
+ DocumentMetricIds.STARTED_BY_LAUNCHER, PageTransition.AUTO_TOPLEVEL, false, |
+ null); |
+ |
+ if (mIsFinishNeeded) finish(); |
+ } |
+ }, INITIAL_DOCUMENT_ACTIVITY_LAUNCH_TIMEOUT_MS); |
+ } |
+ |
+ /** |
+ * If necessary, attempts to clobber the current DocumentActivity's tab with the given URL. |
+ * @param url URL to display. |
+ * @return Whether or not the clobber was successful. |
+ */ |
+ private boolean clobberCurrentDocument(String url) { |
+ boolean shouldOpenNewTab = IntentUtils.safeGetBooleanExtra( |
+ getIntent(), Browser.EXTRA_CREATE_NEW_TAB, false); |
+ String applicationId = |
+ IntentUtils.safeGetStringExtra(getIntent(), Browser.EXTRA_APPLICATION_ID); |
+ if (shouldOpenNewTab || !getPackageName().equals(applicationId)) return false; |
+ |
+ // Check if there's a Tab that can be clobbered. |
+ int tabId = ChromeMobileApplication.getDocumentTabModelSelector().getCurrentTabId(); |
+ if (tabId == Tab.INVALID_TAB_ID) return false; |
+ |
+ // Try to clobber the page. |
+ PendingDocumentData data = new PendingDocumentData(); |
+ data.url = url; |
+ data.originalIntent = new Intent(getIntent()); |
+ ChromeMobileApplication.getDocumentTabModelSelector().addPendingDocumentData(tabId, data); |
+ if (!relaunchTask(tabId)) { |
+ // Were not able to clobber, will fall through to handle in a new document. |
+ ChromeMobileApplication.getDocumentTabModelSelector().removePendingDocumentData(tabId); |
+ return false; |
+ } |
+ |
+ return true; |
+ } |
+ |
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
+ private boolean launchLastViewedActivity() { |
+ int tabId = ChromeMobileApplication.getDocumentTabModelSelector().getCurrentTabId(); |
+ DocumentTabModel model = |
+ ChromeMobileApplication.getDocumentTabModelSelector().getModelForTabId(tabId); |
+ if (tabId != Tab.INVALID_TAB_ID && model != null && !model.isCoveredByChildActivity(tabId) |
+ && relaunchTask(tabId)) { |
+ return true; |
+ } |
+ |
+ // Everything above failed, try to launch the last viewed activity based on app tasks list. |
+ ActivityManager am = (ActivityManager) getSystemService(Activity.ACTIVITY_SERVICE); |
+ PackageManager pm = getPackageManager(); |
+ for (AppTask task : am.getAppTasks()) { |
+ String className = DocumentUtils.getTaskClassName(task, pm); |
+ if (className == null || !DocumentActivity.isDocumentActivity(className)) continue; |
+ |
+ int id = ActivityDelegate.getTabIdFromIntent(task.getTaskInfo().baseIntent); |
+ model = ChromeMobileApplication.getDocumentTabModelSelector().getModelForTabId(id); |
+ if (model != null && model.isCoveredByChildActivity(id)) continue; |
+ |
+ if (!moveToFront(task)) continue; |
+ return true; |
+ } |
+ return false; |
+ } |
+ |
+ /** |
+ * Starts a Document for the given URL. |
+ * |
+ * NOTE: this method adds trusted intent extra to authenticate that Chrome set the |
+ * EXTRA_PAGE_TRANSITION_TYPE extra which we only want Chrome to do. |
+ * This should never be exposed to non-Chrome callers. |
+ * @param activity Activity launching the new instance. May be null. |
+ * @param incognito Whether the created document should be incognito. |
+ * @param launchMode See LAUNCH_MODE_* above. |
+ * @param url URL to load. |
+ * @param intentSource What is causing the Intent to be fired. |
+ * See DocumentUma.DOCUMENT_ACTIVITY_STARTED_BY_ |
+ * @param pageTransitionType The page transition we will do on loading the given URL. |
+ * @param useDesktopUserAgent Whether to use a desktop user agent. |
+ * @param pendingUrlParams PendingUrlParams to store internally and use later once an intent is |
+ * received to launch the URL. May be null. |
+ */ |
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
+ public static void launchDocumentInstance(Activity activity, boolean incognito, int launchMode, |
+ String url, int intentSource, int pageTransitionType, |
+ boolean useDesktopUserAgent, PendingDocumentData pendingUrlParams) { |
+ // If we weren't given an initial URL, check the pending parameters. |
+ if (url == null && pendingUrlParams != null) { |
+ if (pendingUrlParams.url != null) { |
+ url = pendingUrlParams.url; |
+ } else if (pendingUrlParams.webContents != null) { |
+ url = pendingUrlParams.webContents.getUrl(); |
+ } |
+ } |
+ |
+ // Try to retarget an existing task. Make sure there is no pending data to go with the load |
+ // because relaunching an Activity won't send the parameters over. |
+ if (launchMode == LAUNCH_MODE_RETARGET) { |
+ assert pendingUrlParams == null; |
+ if (relaunchTask(incognito, url)) return; |
+ } |
+ |
+ // If the new tab is spawned by another tab, record the parent. |
+ int parentId = activity != null && (launchMode == LAUNCH_MODE_AFFILIATED |
+ || intentSource == DocumentMetricIds.STARTED_BY_WINDOW_OPEN |
+ || intentSource == DocumentMetricIds.STARTED_BY_CONTEXTUAL_SEARCH) |
+ ? ActivityDelegate.getTabIdFromIntent(activity.getIntent()) |
+ : Tab.INVALID_TAB_ID; |
+ |
+ // Fire an Intent to start a DocumentActivity instance. |
+ Context context = ApplicationStatus.getApplicationContext(); |
+ Intent intent = createLaunchIntent(context, null, url, incognito, parentId); |
+ setRecentsFlagsOnIntent(intent, Intent.FLAG_ACTIVITY_NEW_DOCUMENT, incognito); |
+ intent.putExtra(IntentHandler.EXTRA_OPEN_NEW_INCOGNITO_TAB, incognito); |
+ intent.putExtra(IntentHandler.EXTRA_PAGE_TRANSITION_TYPE, pageTransitionType); |
+ intent.putExtra(IntentHandler.EXTRA_STARTED_BY, intentSource); |
+ intent.putExtra(IntentHandler.EXTRA_USE_DESKTOP_USER_AGENT, useDesktopUserAgent); |
+ intent.putExtra(EXTRA_LAUNCH_MODE, launchMode); |
+ IntentHandler.addTrustedIntentExtras(intent, context); |
+ |
+ boolean affiliated = launchMode == LAUNCH_MODE_AFFILIATED; |
+ if (activity == null) { |
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
+ fireDocumentIntent(context, intent, incognito, url, affiliated, pendingUrlParams); |
+ } else { |
+ fireDocumentIntent(activity, intent, incognito, url, affiliated, pendingUrlParams); |
+ } |
+ } |
+ |
+ /** |
+ * Starts the document activity specified by the intent and options. Potentially first runs |
+ * {@link CipherKeyActivity} in order to restore cipher keys. |
+ * |
+ * Note that Android has a mechanism for retargeting existing tasks via Intents, which involves |
+ * firing an Intent to the same class with the same URI data. Firing an Intent via this method |
+ * may therefore _not_ create a new DocumentActivity instance. |
+ */ |
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
+ private static void fireDocumentIntent(Context context, Intent intent, boolean incognito, |
+ String url, boolean affiliated, PendingDocumentData pendingUrlParams) { |
+ assert url != null; |
+ assert incognito || TextUtils.equals(IntentHandler.getUrlFromIntent(intent), url); |
+ assert !affiliated || !incognito; |
+ |
+ // Remove any flags from the Intent that would prevent a second instance of Chrome from |
+ // appearing. |
+ if (context instanceof ChromeLauncherActivity |
+ && ((ChromeLauncherActivity) context).mIsInMultiInstanceMode) { |
+ MultiWindowUtils.getInstance().makeMultiInstanceIntent((ChromeLauncherActivity) context, |
+ intent); |
+ } |
+ |
+ // Incognito URLs are not passed through the Intent for privacy reasons. Instead, store it |
+ // as a parameter that gets retrieved when the IncognitoDocumentActivity starts. |
+ if (incognito) { |
+ if (pendingUrlParams == null) pendingUrlParams = new PendingDocumentData(); |
+ assert pendingUrlParams.url == null; |
+ pendingUrlParams.url = url; |
+ } |
+ |
+ // Store parameters for the new DocumentActivity, which are retrieved immediately after the |
+ // new Activity starts. This structure is used to avoid passing things like pointers to |
+ // native WebContents in the Intent, which are strictly under Android's control and is |
+ // re-delivered when a Chrome Activity is restarted. |
+ boolean isWebContentsPending = false; |
+ if (pendingUrlParams != null) { |
+ int tabId = ActivityDelegate.getTabIdFromIntent(intent); |
+ ChromeMobileApplication.getDocumentTabModelSelector().addPendingDocumentData( |
+ tabId, pendingUrlParams); |
+ isWebContentsPending = pendingUrlParams.webContents != null; |
+ } |
+ |
+ Bundle options = affiliated && !isWebContentsPending |
+ ? ActivityOptions.makeTaskLaunchBehind().toBundle() : null; |
+ if (incognito && !CipherFactory.getInstance().hasCipher() |
+ && ChromeMobileApplication.getDocumentTabModelSelector().getModel(true) |
+ .getCount() > 0) { |
+ // The CipherKeyActivity needs to be run to restore the Incognito decryption key. |
+ Intent cipherIntent = CipherKeyActivity.createIntent(context, intent, options); |
+ context.startActivity(cipherIntent); |
+ } else { |
+ context.startActivity(intent, options); |
+ } |
+ } |
+ |
+ /** |
+ * Get an intent that will close all incognito tabs through {@link ChromeLauncherActivity}. |
+ * @param context The context to use for creating the {@link PendingIntent}. |
+ * @return {@link PendingIntent} to use for closing all incognito tabs. |
+ */ |
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
+ public static PendingIntent getRemoveAllIncognitoTabsIntent(Context context) { |
+ Intent intent = new Intent( |
+ ACTION_CLOSE_ALL_INCOGNITO, null, context, ChromeLauncherActivity.class); |
+ return PendingIntent.getActivity(context, 0, intent, 0); |
+ } |
+ |
+ static String getDocumentClassName(boolean isIncognito) { |
+ return isIncognito ? IncognitoDocumentActivity.class.getName() : |
+ DocumentActivity.class.getName(); |
+ } |
+ |
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
+ private static Intent createLaunchIntent( |
+ Context context, Intent oldIntent, String url, boolean incognito, int parentId) { |
+ int newTabId = ChromeMobileApplication.getDocumentTabModelSelector().generateValidTabId(); |
+ |
+ // Copy the old Intent so that the extras carry over. |
+ Intent intent = oldIntent == null ? new Intent() : new Intent(oldIntent); |
+ intent.setAction(Intent.ACTION_VIEW); |
+ intent.setClassName(context, getDocumentClassName(incognito)); |
+ |
+ if (incognito) { |
+ // Incognito Intents don't pass URLs in their data. |
+ intent.setData(DocumentTabModelSelector.createDocumentDataString(newTabId, "")); |
+ } else { |
+ intent.setData(DocumentTabModelSelector.createDocumentDataString(newTabId, url)); |
+ } |
+ |
+ // For content URIs, because intent.getData().getScheme() begins with "document://, |
+ // we need to pass a ClipData so DocumentActivity can access the content. |
+ if (url != null && url.startsWith("content://")) { |
+ intent.setClipData(ClipData.newUri( |
+ context.getContentResolver(), "content", Uri.parse(url))); |
+ intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); |
+ } |
+ intent.putExtra(IntentHandler.EXTRA_PARENT_TAB_ID, parentId); |
+ if (oldIntent != null && Intent.ACTION_VIEW.equals(oldIntent.getAction())) { |
+ intent.putExtra(IntentHandler.EXTRA_ORIGINAL_INTENT, oldIntent); |
+ } |
+ |
+ return intent; |
+ } |
+ |
+ @SuppressLint("InlinedApi") |
+ private void launchTabbedMode() { |
+ maybePrefetchDnsInBackground(); |
+ |
+ Intent newIntent = new Intent(getIntent()); |
+ newIntent.setClassName(getApplicationContext().getPackageName(), |
+ ChromeTabbedActivity.class.getName()); |
+ newIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); |
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { |
+ newIntent.addFlags(Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS); |
+ } |
+ Uri uri = newIntent.getData(); |
+ if (uri != null && "content".equals(uri.getScheme())) { |
+ newIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); |
+ } |
+ if (mIsInMultiInstanceMode) { |
+ MultiWindowUtils.getInstance().makeMultiInstanceIntent(this, newIntent); |
+ } |
+ startActivity(newIntent); |
+ } |
+ |
+ /** |
+ * Bring the task matching the given tab ID to the front. |
+ * @param tabId tab ID to search for. |
+ * @return Whether the task was successfully brought back. |
+ */ |
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
+ private static boolean relaunchTask(int tabId) { |
+ if (tabId == Tab.INVALID_TAB_ID) return false; |
+ |
+ Context context = ApplicationStatus.getApplicationContext(); |
+ ActivityManager manager = |
+ (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); |
+ for (AppTask task : manager.getAppTasks()) { |
+ RecentTaskInfo info = DocumentUtils.getTaskInfoFromTask(task); |
+ if (info == null) continue; |
+ |
+ int id = ActivityDelegate.getTabIdFromIntent(info.baseIntent); |
+ if (id != tabId) continue; |
+ |
+ DocumentTabModelSelector.setPrioritizedTabId(id); |
+ if (!moveToFront(task)) continue; |
+ |
+ return true; |
+ } |
+ |
+ return false; |
+ } |
+ |
+ /** |
+ * Bring the task matching the given URL to the front if the task is retargetable. |
+ * @param incognito Whether or not the tab is incognito. |
+ * @param url URL that the tab would have been created for. If null, this param is ignored. |
+ * @return Whether the task was successfully brought back. |
+ */ |
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
+ private static boolean relaunchTask(boolean incognito, String url) { |
+ if (TextUtils.isEmpty(url)) return false; |
+ |
+ Context context = ApplicationStatus.getApplicationContext(); |
+ ActivityManager manager = |
+ (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); |
+ for (AppTask task : manager.getAppTasks()) { |
+ RecentTaskInfo info = DocumentUtils.getTaskInfoFromTask(task); |
+ if (info == null) continue; |
+ |
+ String initialUrl = ActivityDelegate.getInitialUrlForDocument(info.baseIntent); |
+ if (TextUtils.isEmpty(initialUrl) || !TextUtils.equals(initialUrl, url)) continue; |
+ |
+ int id = ActivityDelegate.getTabIdFromIntent(info.baseIntent); |
+ DocumentTabModelSelector.setPrioritizedTabId(id); |
+ if (!ChromeMobileApplication.getDocumentTabModelSelector().getModel(incognito) |
+ .isRetargetable(id)) { |
+ continue; |
+ } |
+ |
+ if (!moveToFront(task)) continue; |
+ return true; |
+ } |
+ |
+ return false; |
+ } |
+ |
+ /** |
+ * On opting out, remove all the old tasks from the recents. |
+ * @param fromDocument Whether any possible migration was from document mode to classic. |
+ */ |
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
+ private void cleanUpChromeRecents(boolean fromDocument) { |
+ ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); |
+ List<ActivityManager.AppTask> taskList = am.getAppTasks(); |
+ PackageManager pm = getPackageManager(); |
+ for (int i = 0; i < taskList.size(); i++) { |
+ AppTask task = taskList.get(i); |
+ String className = DocumentUtils.getTaskClassName(task, pm); |
+ if (className == null) continue; |
+ |
+ RecentTaskInfo taskInfo = DocumentUtils.getTaskInfoFromTask(task); |
+ if (taskInfo == null) continue; |
+ |
+ // Skip the document activities if we are migrating from classic to document. |
+ boolean skip = !fromDocument && DocumentActivity.isDocumentActivity(className); |
+ if (!skip && (taskInfo.id != getTaskId())) { |
+ taskList.get(i).finishAndRemoveTask(); |
+ } |
+ } |
+ } |
+ |
+ /** |
+ * Set flags that ensure that we control when our Activities disappear from Recents. |
+ * @param intent Intent to set the flags on. |
+ * @param extraFlags Other flags to add to the Intent, 0 if there's nothing to add. |
+ * @param incognito Whether we are launching an incognito document. |
+ */ |
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
+ private static void setRecentsFlagsOnIntent(Intent intent, int extraFlags, boolean incognito) { |
+ intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); |
+ if (!incognito) intent.addFlags(Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS); |
+ if (extraFlags != 0) intent.addFlags(extraFlags); |
+ } |
+ |
+ /** |
+ * @return Whether there is already an browser instance of Chrome already running. |
+ */ |
+ public boolean isChromeBrowserActivityRunning() { |
+ for (WeakReference<Activity> reference : ApplicationStatus.getRunningActivities()) { |
+ Activity activity = reference.get(); |
+ if (activity == null) continue; |
+ |
+ String className = activity.getClass().getName(); |
+ if (DocumentActivity.isDocumentActivity(className) |
+ || TextUtils.equals(className, ChromeTabbedActivity.class.getName())) { |
+ return true; |
+ } |
+ } |
+ return false; |
+ } |
+ |
+ /** |
+ * Attempt to move a task back to the front. This can FAIL for some reason because the UID |
+ * of the DocumentActivity we try to bring back to the front doesn't match the |
+ * ChromeLauncherActivities. |
+ * @param task Task to attempt to bring back to the foreground. |
+ * @return Whether or not this succeeded. |
+ */ |
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
+ private static boolean moveToFront(AppTask task) { |
+ try { |
+ task.moveToFront(); |
+ return true; |
+ } catch (SecurityException e) { |
+ sMoveToFrontExceptionHistogram.recordHit(); |
+ } |
+ return false; |
+ } |
+ |
+ /** |
+ * Tries to launch a WebappActivity for the given Intent. |
+ * @return Intent to fire if the webapp Intent failed to launch because of security checks, |
+ * null otherwise. |
+ */ |
+ private Intent launchWebapp(Intent intent) { |
+ String webappId = IntentUtils.safeGetStringExtra(intent, ShortcutHelper.EXTRA_ID); |
+ String webappUrl = IntentUtils.safeGetStringExtra(intent, ShortcutHelper.EXTRA_URL); |
+ String webappTitle = IntentUtils.safeGetStringExtra(intent, ShortcutHelper.EXTRA_TITLE); |
+ String webappIcon = IntentUtils.safeGetStringExtra(intent, ShortcutHelper.EXTRA_ICON); |
+ int webappOrientation = IntentUtils.safeGetIntExtra(intent, |
+ ShortcutHelper.EXTRA_ORIENTATION, ScreenOrientationValues.DEFAULT); |
+ |
+ if (webappId != null && webappUrl != null) { |
+ String webappMacString = IntentUtils.safeGetStringExtra( |
+ intent, ShortcutHelper.EXTRA_MAC); |
+ byte[] webappMac = |
+ webappMacString == null ? null : Base64.decode(webappMacString, Base64.DEFAULT); |
+ |
+ if (webappMac != null && WebappAuthenticator.isUrlValid(this, webappUrl, webappMac)) { |
+ if (TextUtils.equals(ACTION_START_WEBAPP, intent.getAction())) { |
+ LaunchMetrics.recordHomeScreenLaunchIntoStandaloneActivity(webappUrl); |
+ } |
+ |
+ WebappActivity.launchInstance( |
+ this, webappId, webappUrl, webappIcon, webappTitle, webappOrientation); |
+ } else { |
+ Log.e(TAG, "Shortcut (" + webappUrl + ") opened in Chrome."); |
+ |
+ // Tried and failed. Change the intent action and try the URL with a VIEW Intent. |
+ Intent fallbackIntent = new Intent(intent); |
+ fallbackIntent.setAction(Intent.ACTION_VIEW); |
+ fallbackIntent.setData(Uri.parse(webappUrl)); |
+ fallbackIntent.putExtra(BookmarkUtils.REUSE_URL_MATCHING_TAB_ELSE_NEW_TAB, true); |
+ return fallbackIntent; |
+ } |
+ } |
+ return null; |
+ } |
+ |
+ /** |
+ * Tries to launch the First Run Experience. If ChromeLauncherActivity is running with the |
+ * wrong Intent flags, we instead relaunch ChromeLauncherActivity to make sure it runs in its |
+ * own task, which then triggers First Run. |
+ * @return Whether or not the First Run Experience needed to be shown. |
+ */ |
+ private boolean launchFirstRunExperience() { |
+ final boolean isIntentActionMain = getIntent() != null |
+ && TextUtils.equals(getIntent().getAction(), Intent.ACTION_MAIN); |
+ final Intent freIntent = FirstRunFlowSequencer.checkIfFirstRunIsNecessary( |
+ this, getIntent(), isIntentActionMain); |
+ if (freIntent == null) return false; |
+ |
+ if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0) { |
+ startActivityForResult(freIntent, FIRST_RUN_EXPERIENCE_REQUEST_CODE); |
+ } else { |
+ Intent newIntent = new Intent(getIntent()); |
+ newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
+ startActivity(newIntent); |
+ finish(); |
+ } |
+ return true; |
+ } |
+ |
+ /** |
+ * Send the number of times an exception was caught when trying to move a task back to front. |
+ */ |
+ public static void sendExceptionCount() { |
+ sMoveToFrontExceptionHistogram.commitHistogram(); |
+ } |
+} |