OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 package org.chromium.chrome.browser.document; |
| 6 |
| 7 import android.annotation.SuppressLint; |
| 8 import android.annotation.TargetApi; |
| 9 import android.app.Activity; |
| 10 import android.app.ActivityManager; |
| 11 import android.app.ActivityManager.AppTask; |
| 12 import android.app.ActivityManager.RecentTaskInfo; |
| 13 import android.app.ActivityOptions; |
| 14 import android.app.Notification; |
| 15 import android.app.PendingIntent; |
| 16 import android.content.ClipData; |
| 17 import android.content.Context; |
| 18 import android.content.Intent; |
| 19 import android.content.pm.PackageManager; |
| 20 import android.net.Uri; |
| 21 import android.os.Build; |
| 22 import android.os.Bundle; |
| 23 import android.provider.Browser; |
| 24 import android.text.TextUtils; |
| 25 import android.util.Base64; |
| 26 import android.util.Log; |
| 27 |
| 28 import org.chromium.base.ApiCompatibilityUtils; |
| 29 import org.chromium.base.ApplicationStatus; |
| 30 import org.chromium.base.CommandLine; |
| 31 import org.chromium.chrome.browser.BookmarkUtils; |
| 32 import org.chromium.chrome.browser.ChromeMobileApplication; |
| 33 import org.chromium.chrome.browser.ChromeSwitches; |
| 34 import org.chromium.chrome.browser.ChromeTabbedActivity; |
| 35 import org.chromium.chrome.browser.IntentHandler; |
| 36 import org.chromium.chrome.browser.IntentHandler.TabOpenType; |
| 37 import org.chromium.chrome.browser.ShortcutHelper; |
| 38 import org.chromium.chrome.browser.Tab; |
| 39 import org.chromium.chrome.browser.UrlConstants; |
| 40 import org.chromium.chrome.browser.WarmupManager; |
| 41 import org.chromium.chrome.browser.WebappAuthenticator; |
| 42 import org.chromium.chrome.browser.firstrun.FirstRunFlowSequencer; |
| 43 import org.chromium.chrome.browser.hosted.HostedActivity; |
| 44 import org.chromium.chrome.browser.metrics.LaunchHistogram; |
| 45 import org.chromium.chrome.browser.metrics.LaunchMetrics; |
| 46 import org.chromium.chrome.browser.multiwindow.MultiWindowUtils; |
| 47 import org.chromium.chrome.browser.notifications.NotificationUIManager; |
| 48 import org.chromium.chrome.browser.partnercustomizations.HomepageManager; |
| 49 import org.chromium.chrome.browser.partnercustomizations.PartnerBrowserCustomiza
tions; |
| 50 import org.chromium.chrome.browser.preferences.DocumentModeManager; |
| 51 import org.chromium.chrome.browser.tabmodel.document.ActivityDelegate; |
| 52 import org.chromium.chrome.browser.tabmodel.document.DocumentTabModel; |
| 53 import org.chromium.chrome.browser.tabmodel.document.DocumentTabModelSelector; |
| 54 import org.chromium.chrome.browser.util.FeatureUtilities; |
| 55 import org.chromium.chrome.browser.util.IntentUtils; |
| 56 import org.chromium.chrome.browser.webapps.WebappActivity; |
| 57 import org.chromium.content.browser.crypto.CipherFactory; |
| 58 import org.chromium.content_public.common.ScreenOrientationValues; |
| 59 import org.chromium.ui.base.PageTransition; |
| 60 |
| 61 import java.lang.ref.WeakReference; |
| 62 import java.util.List; |
| 63 |
| 64 /** |
| 65 * Dispatches incoming intents to the appropriate activity based on the current
configuration and |
| 66 * Intent fired. |
| 67 */ |
| 68 public class ChromeLauncherActivity extends Activity |
| 69 implements IntentHandler.IntentHandlerDelegate { |
| 70 /** |
| 71 * Action fired when an Intent is trying to launch a WebappActivity. |
| 72 * Never change the package name or the Intents will fail to launch. |
| 73 */ |
| 74 public static final String ACTION_START_WEBAPP = |
| 75 "com.google.android.apps.chrome.webapps.WebappManager.ACTION_START_W
EBAPP"; |
| 76 |
| 77 /** |
| 78 * Extra indicating that a Tab is trying to bring its WebappActivity to the
foreground. |
| 79 * Never change the package name or the Intents will fail to launch. |
| 80 */ |
| 81 public static final String EXTRA_BRING_WEBAPP_TO_FRONT = |
| 82 "com.google.android.apps.chrome.EXTRA_BRING_WEBAPP_TO_FRONT"; |
| 83 |
| 84 /** |
| 85 * Extra indicating launch mode used. |
| 86 */ |
| 87 public static final String EXTRA_LAUNCH_MODE = |
| 88 "com.google.android.apps.chrome.EXTRA_LAUNCH_MODE"; |
| 89 |
| 90 /** |
| 91 * Action fired when the user selects the "Close all incognito tabs" notific
ation. |
| 92 */ |
| 93 static final String ACTION_CLOSE_ALL_INCOGNITO = |
| 94 "com.google.android.apps.chrome.document.CLOSE_ALL_INCOGNITO"; |
| 95 |
| 96 private static final String TAG = "ChromeLauncherActivity"; |
| 97 |
| 98 /** New instance should be launched in the foreground. */ |
| 99 public static final int LAUNCH_MODE_FOREGROUND = 0; |
| 100 |
| 101 /** New instance should be launched as an affiliated task. */ |
| 102 public static final int LAUNCH_MODE_AFFILIATED = 1; |
| 103 |
| 104 /** Existing instance should be retargetted, if possible. */ |
| 105 public static final int LAUNCH_MODE_RETARGET = 2; |
| 106 |
| 107 private static final int FIRST_RUN_EXPERIENCE_REQUEST_CODE = 101; |
| 108 |
| 109 /** |
| 110 * Timeout in ms for reading PartnerBrowserCustomizations provider. We do no
t trust third party |
| 111 * provider by default. |
| 112 */ |
| 113 private static final int PARTNER_BROWSER_CUSTOMIZATIONS_TIMEOUT_MS = 10000; |
| 114 |
| 115 /** |
| 116 * Maximum delay for initial document activity launch. |
| 117 */ |
| 118 private static final int INITIAL_DOCUMENT_ACTIVITY_LAUNCH_TIMEOUT_MS = 500; |
| 119 |
| 120 private static final LaunchHistogram sMoveToFrontExceptionHistogram = |
| 121 new LaunchHistogram("DocumentActivity.MoveToFrontFailed"); |
| 122 |
| 123 private IntentHandler mIntentHandler; |
| 124 private boolean mIsInMultiInstanceMode; |
| 125 private boolean mIsFinishNeeded; |
| 126 |
| 127 /** When started with an intent, maybe pre-resolve the domain. */ |
| 128 private void maybePrefetchDnsInBackground() { |
| 129 if (getIntent() != null && Intent.ACTION_VIEW.equals(getIntent().getActi
on())) { |
| 130 String maybeUrl = IntentHandler.getUrlFromIntent(getIntent()); |
| 131 if (maybeUrl != null) { |
| 132 WarmupManager.getInstance().maybePrefetchDnsForUrlInBackground(t
his, maybeUrl); |
| 133 } |
| 134 } |
| 135 } |
| 136 |
| 137 /** |
| 138 * Figure out how to route the Intent. Because this is on the critical path
to startup, please |
| 139 * avoid making the pathway any more complicated than it already is. Make s
ure that anything |
| 140 * you add _absolutely has_ to be here. |
| 141 */ |
| 142 @Override |
| 143 public void onCreate(Bundle savedInstanceState) { |
| 144 super.onCreate(savedInstanceState); |
| 145 |
| 146 // Initialize the command line in case we've disabled document mode from
there. |
| 147 ((ChromeMobileApplication) getApplication()).initCommandLine(); |
| 148 |
| 149 // Read partner browser customizations information asynchronously. |
| 150 // We want to initialize early because when there is no tabs to restore,
we should possibly |
| 151 // show homepage, which might require reading PartnerBrowserCustomizatio
ns provider. |
| 152 PartnerBrowserCustomizations.initializeAsync(getApplicationContext(), |
| 153 PARTNER_BROWSER_CUSTOMIZATIONS_TIMEOUT_MS); |
| 154 |
| 155 mIsInMultiInstanceMode = MultiWindowUtils.getInstance().shouldRunInMulti
InstanceMode(this); |
| 156 mIntentHandler = new IntentHandler(this, getPackageName()); |
| 157 maybePerformMigrationTasks(); |
| 158 |
| 159 if (handleHostedActivityIntent()) { |
| 160 finish(); |
| 161 return; |
| 162 } |
| 163 |
| 164 // Check if we should launch a WebappActivity. |
| 165 if (IntentUtils.safeGetBooleanExtra(getIntent(), EXTRA_BRING_WEBAPP_TO_F
RONT, false) |
| 166 || TextUtils.equals(getIntent().getAction(), ACTION_START_WEBAPP
)) { |
| 167 Intent fallbackIntent = launchWebapp(getIntent()); |
| 168 if (fallbackIntent == null) { |
| 169 ApiCompatibilityUtils.finishAndRemoveTask(this); |
| 170 return; |
| 171 } else { |
| 172 // Try to launch the URL as a regular VIEW intent. |
| 173 setIntent(fallbackIntent); |
| 174 } |
| 175 } |
| 176 |
| 177 // Check if we should launch the ChromeTabbedActivity. |
| 178 if (!FeatureUtilities.isDocumentMode(this)) { |
| 179 launchTabbedMode(); |
| 180 finish(); |
| 181 return; |
| 182 } |
| 183 |
| 184 // Check if we're just closing all of the Incognito tabs. |
| 185 if (TextUtils.equals(getIntent().getAction(), ACTION_CLOSE_ALL_INCOGNITO
)) { |
| 186 ChromeMobileApplication.getDocumentTabModelSelector().getModel(true)
.closeAllTabs(); |
| 187 ApiCompatibilityUtils.finishAndRemoveTask(this); |
| 188 return; |
| 189 } |
| 190 |
| 191 // The notification settings cog on the flipped side of Notifications an
d in the Android |
| 192 // Settings "App Notifications" view will open us with a specific catego
ry. |
| 193 if (getIntent().hasCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PR
EFERENCES)) { |
| 194 NotificationUIManager.launchNotificationPreferences(this, getIntent(
)); |
| 195 return; |
| 196 } |
| 197 |
| 198 // Check if we should launch the FirstRunActivity. This occurs after th
e check to launch |
| 199 // ChromeTabbedActivity because ChromeTabbedActivity handles FRE in its
own way. |
| 200 if (launchFirstRunExperience()) return; |
| 201 |
| 202 // Launch a DocumentActivity to handle the Intent. |
| 203 handleDocumentActivityIntent(); |
| 204 if (!mIsFinishNeeded) ApiCompatibilityUtils.finishAndRemoveTask(this); |
| 205 } |
| 206 |
| 207 @Override |
| 208 protected void onActivityResult(int requestCode, int resultCode, Intent data
) { |
| 209 super.onActivityResult(requestCode, resultCode, data); |
| 210 if (requestCode == FIRST_RUN_EXPERIENCE_REQUEST_CODE) { |
| 211 if (resultCode == Activity.RESULT_OK) { |
| 212 // User might have opted out during FRE, so check again. |
| 213 if (FeatureUtilities.isDocumentMode(this)) { |
| 214 handleDocumentActivityIntent(); |
| 215 if (!mIsFinishNeeded) ApiCompatibilityUtils.finishAndRemoveT
ask(this); |
| 216 } else { |
| 217 launchTabbedMode(); |
| 218 finish(); |
| 219 } |
| 220 return; |
| 221 } |
| 222 |
| 223 // TODO(aruslan): FAIL. |
| 224 ApiCompatibilityUtils.finishAndRemoveTask(this); |
| 225 } |
| 226 } |
| 227 |
| 228 /** |
| 229 * If we have just opted in or opted out of document mode, perform pending m
igration tasks |
| 230 * such as cleaning up the recents. |
| 231 */ |
| 232 private void maybePerformMigrationTasks() { |
| 233 if (DocumentModeManager.getInstance(this).isOptOutCleanUpPending()) { |
| 234 cleanUpChromeRecents( |
| 235 DocumentModeManager.getInstance(this).isOptedOutOfDocumentMo
de()); |
| 236 DocumentModeManager.getInstance(this).setOptOutCleanUpPending(false)
; |
| 237 } |
| 238 } |
| 239 |
| 240 @Override |
| 241 public void processWebSearchIntent(String query) { |
| 242 assert false; |
| 243 } |
| 244 |
| 245 @Override |
| 246 public void processUrlViewIntent(String url, String headers, |
| 247 IntentHandler.TabOpenType tabOpenType, String externalAppId, |
| 248 int tabIdToBringToFront, Intent intent) { |
| 249 assert false; |
| 250 } |
| 251 |
| 252 /** |
| 253 * Handles launching an hosted activity, which will sit on top of a client's
activity in the |
| 254 * same task. |
| 255 * @return True if the intent is handled here. |
| 256 */ |
| 257 private boolean handleHostedActivityIntent() { |
| 258 if (getIntent() == null) return false; |
| 259 |
| 260 boolean enabled = CommandLine.getInstance().hasSwitch(ChromeSwitches.ENA
BLE_EMBEDDED_MODE); |
| 261 boolean append = IntentUtils.safeGetBooleanExtra( |
| 262 getIntent(), IntentHandler.EXTRA_APPEND_TASK, false); |
| 263 if (!append || !enabled) return false; |
| 264 |
| 265 String url = IntentHandler.getUrlFromIntent(getIntent()); |
| 266 if (url == null) return false; |
| 267 |
| 268 // Create and fire a launch intent. Use the copy constructor to carry ov
er the myriad of |
| 269 // extras. |
| 270 Intent newIntent = new Intent(getIntent()); |
| 271 newIntent.setAction(Intent.ACTION_VIEW); |
| 272 newIntent.setClassName(this, HostedActivity.class.getName()); |
| 273 newIntent.setData(Uri.parse(url)); |
| 274 startActivity(newIntent); |
| 275 return true; |
| 276 } |
| 277 |
| 278 /** |
| 279 * Handles the launching of a DocumentActivity from the current Intent. Rou
ting Intents to |
| 280 * other types of Activities must be handled from onCreate() instead. |
| 281 */ |
| 282 @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| 283 private void handleDocumentActivityIntent() { |
| 284 if (getIntent() == null || mIntentHandler.shouldIgnoreIntent(this, getIn
tent())) { |
| 285 Log.e(TAG, "Ignoring intent: " + getIntent()); |
| 286 mIsFinishNeeded = true; |
| 287 return; |
| 288 } |
| 289 |
| 290 maybePrefetchDnsInBackground(); |
| 291 |
| 292 // Increment the Tab ID counter at this point since this Activity may no
t appear in |
| 293 // getAppTasks() when DocumentTabModelSelector is initialized. This can
potentially happen |
| 294 // when Chrome is launched via the GSA/e200 search box and they relinqui
sh their task. |
| 295 Tab.incrementIdCounterTo(getTaskId() + 1); |
| 296 |
| 297 // Handle MAIN Intent actions, usually fired when the user starts Chrome
via the launcher. |
| 298 // Some launchers start Chrome by firing a VIEW Intent with an empty URL
(crbug.com/459349); |
| 299 // treat it as a MAIN Intent. |
| 300 String url = IntentHandler.getUrlFromIntent(getIntent()); |
| 301 if ((url == null && TextUtils.equals(getIntent().getAction(), Intent.ACT
ION_VIEW)) |
| 302 || TextUtils.equals(getIntent().getAction(), Intent.ACTION_MAIN)
) { |
| 303 handleMainDocumentIntent(); |
| 304 return; |
| 305 } |
| 306 |
| 307 // Sometimes an Intent requests that the current Document get clobbered. |
| 308 if (clobberCurrentDocument(url)) return; |
| 309 |
| 310 // Try to retarget existing Documents before creating a new one. |
| 311 boolean incognito = IntentUtils.safeGetBooleanExtra(getIntent(), |
| 312 IntentHandler.EXTRA_OPEN_NEW_INCOGNITO_TAB, false); |
| 313 boolean append = IntentUtils.safeGetBooleanExtra( |
| 314 getIntent(), IntentHandler.EXTRA_APPEND_TASK, false); |
| 315 boolean reuse = IntentUtils.safeGetBooleanExtra( |
| 316 getIntent(), BookmarkUtils.REUSE_URL_MATCHING_TAB_ELSE_NEW_TAB,
false); |
| 317 boolean affiliated = IntentUtils.safeGetBooleanExtra( |
| 318 getIntent(), IntentHandler.EXTRA_OPEN_IN_BG, false); |
| 319 |
| 320 // Try to relaunch an existing task. |
| 321 if (reuse && !append) { |
| 322 LaunchMetrics.recordHomeScreenLaunchIntoTab(url); |
| 323 if (relaunchTask(incognito, url)) return; |
| 324 } |
| 325 |
| 326 // Create and fire a launch Intent to start a new Task. The old Intent
is copied using |
| 327 // the constructor so that we pass through the myriad extras that were s
et on it. |
| 328 Intent newIntent = createLaunchIntent( |
| 329 getApplicationContext(), getIntent(), url, incognito, Tab.INVALI
D_TAB_ID); |
| 330 setRecentsFlagsOnIntent( |
| 331 newIntent, append ? 0 : Intent.FLAG_ACTIVITY_NEW_DOCUMENT, incog
nito); |
| 332 fireDocumentIntent(this, newIntent, incognito, url, affiliated, null); |
| 333 } |
| 334 |
| 335 /** |
| 336 * Handles actions pertaining to Chrome being started with a MAIN Intent. T
ypically, receiving |
| 337 * this Intent means that a user has selected the Chrome icon from their lau
ncher, but it is |
| 338 * also used internally (e.g. when firing Intents back at Chrome via notific
ations). |
| 339 */ |
| 340 @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| 341 private void handleMainDocumentIntent() { |
| 342 // Bring a specific tab back to the foreground. |
| 343 int tabId = IntentUtils.safeGetIntExtra(getIntent(), |
| 344 TabOpenType.BRING_TAB_TO_FRONT.name(), Tab.INVALID_TAB_ID); |
| 345 if (tabId != Tab.INVALID_TAB_ID && relaunchTask(tabId)) return; |
| 346 |
| 347 // Bring the last viewed tab to the foreground, unless we're in Samsung'
s multi-instance |
| 348 // mode -- a MAIN Intent in that case results in the creation of a secon
d default page. |
| 349 if (!mIsInMultiInstanceMode && launchLastViewedActivity()) return; |
| 350 |
| 351 // Launch the default page asynchronously because the homepage URL needs
to be queried. |
| 352 // This is obviously not ideal, but we don't have a choice. |
| 353 mIsFinishNeeded = mIsInMultiInstanceMode; |
| 354 PartnerBrowserCustomizations.setOnInitializeAsyncFinished(new Runnable()
{ |
| 355 @Override |
| 356 public void run() { |
| 357 String url = HomepageManager.getHomepageUri(ChromeLauncherActivi
ty.this); |
| 358 if (TextUtils.isEmpty(url)) url = UrlConstants.NTP_URL; |
| 359 |
| 360 int mode = mIsInMultiInstanceMode ? LAUNCH_MODE_FOREGROUND : LAU
NCH_MODE_RETARGET; |
| 361 launchDocumentInstance(ChromeLauncherActivity.this, false, mode,
url, |
| 362 DocumentMetricIds.STARTED_BY_LAUNCHER, PageTransition.AU
TO_TOPLEVEL, false, |
| 363 null); |
| 364 |
| 365 if (mIsFinishNeeded) finish(); |
| 366 } |
| 367 }, INITIAL_DOCUMENT_ACTIVITY_LAUNCH_TIMEOUT_MS); |
| 368 } |
| 369 |
| 370 /** |
| 371 * If necessary, attempts to clobber the current DocumentActivity's tab with
the given URL. |
| 372 * @param url URL to display. |
| 373 * @return Whether or not the clobber was successful. |
| 374 */ |
| 375 private boolean clobberCurrentDocument(String url) { |
| 376 boolean shouldOpenNewTab = IntentUtils.safeGetBooleanExtra( |
| 377 getIntent(), Browser.EXTRA_CREATE_NEW_TAB, false); |
| 378 String applicationId = |
| 379 IntentUtils.safeGetStringExtra(getIntent(), Browser.EXTRA_APPLIC
ATION_ID); |
| 380 if (shouldOpenNewTab || !getPackageName().equals(applicationId)) return
false; |
| 381 |
| 382 // Check if there's a Tab that can be clobbered. |
| 383 int tabId = ChromeMobileApplication.getDocumentTabModelSelector().getCur
rentTabId(); |
| 384 if (tabId == Tab.INVALID_TAB_ID) return false; |
| 385 |
| 386 // Try to clobber the page. |
| 387 PendingDocumentData data = new PendingDocumentData(); |
| 388 data.url = url; |
| 389 data.originalIntent = new Intent(getIntent()); |
| 390 ChromeMobileApplication.getDocumentTabModelSelector().addPendingDocument
Data(tabId, data); |
| 391 if (!relaunchTask(tabId)) { |
| 392 // Were not able to clobber, will fall through to handle in a new do
cument. |
| 393 ChromeMobileApplication.getDocumentTabModelSelector().removePendingD
ocumentData(tabId); |
| 394 return false; |
| 395 } |
| 396 |
| 397 return true; |
| 398 } |
| 399 |
| 400 @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| 401 private boolean launchLastViewedActivity() { |
| 402 int tabId = ChromeMobileApplication.getDocumentTabModelSelector().getCur
rentTabId(); |
| 403 DocumentTabModel model = |
| 404 ChromeMobileApplication.getDocumentTabModelSelector().getModelFo
rTabId(tabId); |
| 405 if (tabId != Tab.INVALID_TAB_ID && model != null && !model.isCoveredByCh
ildActivity(tabId) |
| 406 && relaunchTask(tabId)) { |
| 407 return true; |
| 408 } |
| 409 |
| 410 // Everything above failed, try to launch the last viewed activity based
on app tasks list. |
| 411 ActivityManager am = (ActivityManager) getSystemService(Activity.ACTIVIT
Y_SERVICE); |
| 412 PackageManager pm = getPackageManager(); |
| 413 for (AppTask task : am.getAppTasks()) { |
| 414 String className = DocumentUtils.getTaskClassName(task, pm); |
| 415 if (className == null || !DocumentActivity.isDocumentActivity(classN
ame)) continue; |
| 416 |
| 417 int id = ActivityDelegate.getTabIdFromIntent(task.getTaskInfo().base
Intent); |
| 418 model = ChromeMobileApplication.getDocumentTabModelSelector().getMod
elForTabId(id); |
| 419 if (model != null && model.isCoveredByChildActivity(id)) continue; |
| 420 |
| 421 if (!moveToFront(task)) continue; |
| 422 return true; |
| 423 } |
| 424 return false; |
| 425 } |
| 426 |
| 427 /** |
| 428 * Starts a Document for the given URL. |
| 429 * |
| 430 * NOTE: this method adds trusted intent extra to authenticate that Chrome s
et the |
| 431 * EXTRA_PAGE_TRANSITION_TYPE extra which we only want Chrome to do. |
| 432 * This should never be exposed to non-Chrome callers. |
| 433 * @param activity Activity launching the new instance. May be null. |
| 434 * @param incognito Whether the created document should be incognito. |
| 435 * @param launchMode See LAUNCH_MODE_* above. |
| 436 * @param url URL to load. |
| 437 * @param intentSource What is causing the Intent to be fired. |
| 438 * See DocumentUma.DOCUMENT_ACTIVITY_STARTED_BY_ |
| 439 * @param pageTransitionType The page transition we will do on loading the g
iven URL. |
| 440 * @param useDesktopUserAgent Whether to use a desktop user agent. |
| 441 * @param pendingUrlParams PendingUrlParams to store internally and use late
r once an intent is |
| 442 * received to launch the URL. May be null. |
| 443 */ |
| 444 @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| 445 public static void launchDocumentInstance(Activity activity, boolean incogni
to, int launchMode, |
| 446 String url, int intentSource, int pageTransitionType, |
| 447 boolean useDesktopUserAgent, PendingDocumentData pendingUrlParams) { |
| 448 // If we weren't given an initial URL, check the pending parameters. |
| 449 if (url == null && pendingUrlParams != null) { |
| 450 if (pendingUrlParams.url != null) { |
| 451 url = pendingUrlParams.url; |
| 452 } else if (pendingUrlParams.webContents != null) { |
| 453 url = pendingUrlParams.webContents.getUrl(); |
| 454 } |
| 455 } |
| 456 |
| 457 // Try to retarget an existing task. Make sure there is no pending data
to go with the load |
| 458 // because relaunching an Activity won't send the parameters over. |
| 459 if (launchMode == LAUNCH_MODE_RETARGET) { |
| 460 assert pendingUrlParams == null; |
| 461 if (relaunchTask(incognito, url)) return; |
| 462 } |
| 463 |
| 464 // If the new tab is spawned by another tab, record the parent. |
| 465 int parentId = activity != null && (launchMode == LAUNCH_MODE_AFFILIATED |
| 466 || intentSource == DocumentMetricIds.STARTED_BY_WINDOW_OPEN |
| 467 || intentSource == DocumentMetricIds.STARTED_BY_CONTEXTUAL_SEARC
H) |
| 468 ? ActivityDelegate.getTabIdFromIntent(activity.getIntent()) |
| 469 : Tab.INVALID_TAB_ID; |
| 470 |
| 471 // Fire an Intent to start a DocumentActivity instance. |
| 472 Context context = ApplicationStatus.getApplicationContext(); |
| 473 Intent intent = createLaunchIntent(context, null, url, incognito, parent
Id); |
| 474 setRecentsFlagsOnIntent(intent, Intent.FLAG_ACTIVITY_NEW_DOCUMENT, incog
nito); |
| 475 intent.putExtra(IntentHandler.EXTRA_OPEN_NEW_INCOGNITO_TAB, incognito); |
| 476 intent.putExtra(IntentHandler.EXTRA_PAGE_TRANSITION_TYPE, pageTransition
Type); |
| 477 intent.putExtra(IntentHandler.EXTRA_STARTED_BY, intentSource); |
| 478 intent.putExtra(IntentHandler.EXTRA_USE_DESKTOP_USER_AGENT, useDesktopUs
erAgent); |
| 479 intent.putExtra(EXTRA_LAUNCH_MODE, launchMode); |
| 480 IntentHandler.addTrustedIntentExtras(intent, context); |
| 481 |
| 482 boolean affiliated = launchMode == LAUNCH_MODE_AFFILIATED; |
| 483 if (activity == null) { |
| 484 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| 485 fireDocumentIntent(context, intent, incognito, url, affiliated, pend
ingUrlParams); |
| 486 } else { |
| 487 fireDocumentIntent(activity, intent, incognito, url, affiliated, pen
dingUrlParams); |
| 488 } |
| 489 } |
| 490 |
| 491 /** |
| 492 * Starts the document activity specified by the intent and options. Potenti
ally first runs |
| 493 * {@link CipherKeyActivity} in order to restore cipher keys. |
| 494 * |
| 495 * Note that Android has a mechanism for retargeting existing tasks via Inte
nts, which involves |
| 496 * firing an Intent to the same class with the same URI data. Firing an Int
ent via this method |
| 497 * may therefore _not_ create a new DocumentActivity instance. |
| 498 */ |
| 499 @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| 500 private static void fireDocumentIntent(Context context, Intent intent, boole
an incognito, |
| 501 String url, boolean affiliated, PendingDocumentData pendingUrlParams
) { |
| 502 assert url != null; |
| 503 assert incognito || TextUtils.equals(IntentHandler.getUrlFromIntent(inte
nt), url); |
| 504 assert !affiliated || !incognito; |
| 505 |
| 506 // Remove any flags from the Intent that would prevent a second instance
of Chrome from |
| 507 // appearing. |
| 508 if (context instanceof ChromeLauncherActivity |
| 509 && ((ChromeLauncherActivity) context).mIsInMultiInstanceMode) { |
| 510 MultiWindowUtils.getInstance().makeMultiInstanceIntent((ChromeLaunch
erActivity) context, |
| 511 intent); |
| 512 } |
| 513 |
| 514 // Incognito URLs are not passed through the Intent for privacy reasons.
Instead, store it |
| 515 // as a parameter that gets retrieved when the IncognitoDocumentActivity
starts. |
| 516 if (incognito) { |
| 517 if (pendingUrlParams == null) pendingUrlParams = new PendingDocument
Data(); |
| 518 assert pendingUrlParams.url == null; |
| 519 pendingUrlParams.url = url; |
| 520 } |
| 521 |
| 522 // Store parameters for the new DocumentActivity, which are retrieved im
mediately after the |
| 523 // new Activity starts. This structure is used to avoid passing things
like pointers to |
| 524 // native WebContents in the Intent, which are strictly under Android's
control and is |
| 525 // re-delivered when a Chrome Activity is restarted. |
| 526 boolean isWebContentsPending = false; |
| 527 if (pendingUrlParams != null) { |
| 528 int tabId = ActivityDelegate.getTabIdFromIntent(intent); |
| 529 ChromeMobileApplication.getDocumentTabModelSelector().addPendingDocu
mentData( |
| 530 tabId, pendingUrlParams); |
| 531 isWebContentsPending = pendingUrlParams.webContents != null; |
| 532 } |
| 533 |
| 534 Bundle options = affiliated && !isWebContentsPending |
| 535 ? ActivityOptions.makeTaskLaunchBehind().toBundle() : null; |
| 536 if (incognito && !CipherFactory.getInstance().hasCipher() |
| 537 && ChromeMobileApplication.getDocumentTabModelSelector().getMode
l(true) |
| 538 .getCount() > 0) { |
| 539 // The CipherKeyActivity needs to be run to restore the Incognito de
cryption key. |
| 540 Intent cipherIntent = CipherKeyActivity.createIntent(context, intent
, options); |
| 541 context.startActivity(cipherIntent); |
| 542 } else { |
| 543 context.startActivity(intent, options); |
| 544 } |
| 545 } |
| 546 |
| 547 /** |
| 548 * Get an intent that will close all incognito tabs through {@link ChromeLau
ncherActivity}. |
| 549 * @param context The context to use for creating the {@link PendingIntent}. |
| 550 * @return {@link PendingIntent} to use for closing all incognito tabs. |
| 551 */ |
| 552 @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| 553 public static PendingIntent getRemoveAllIncognitoTabsIntent(Context context)
{ |
| 554 Intent intent = new Intent( |
| 555 ACTION_CLOSE_ALL_INCOGNITO, null, context, ChromeLauncherActivit
y.class); |
| 556 return PendingIntent.getActivity(context, 0, intent, 0); |
| 557 } |
| 558 |
| 559 static String getDocumentClassName(boolean isIncognito) { |
| 560 return isIncognito ? IncognitoDocumentActivity.class.getName() : |
| 561 DocumentActivity.class.getName(); |
| 562 } |
| 563 |
| 564 @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| 565 private static Intent createLaunchIntent( |
| 566 Context context, Intent oldIntent, String url, boolean incognito, in
t parentId) { |
| 567 int newTabId = ChromeMobileApplication.getDocumentTabModelSelector().gen
erateValidTabId(); |
| 568 |
| 569 // Copy the old Intent so that the extras carry over. |
| 570 Intent intent = oldIntent == null ? new Intent() : new Intent(oldIntent)
; |
| 571 intent.setAction(Intent.ACTION_VIEW); |
| 572 intent.setClassName(context, getDocumentClassName(incognito)); |
| 573 |
| 574 if (incognito) { |
| 575 // Incognito Intents don't pass URLs in their data. |
| 576 intent.setData(DocumentTabModelSelector.createDocumentDataString(new
TabId, "")); |
| 577 } else { |
| 578 intent.setData(DocumentTabModelSelector.createDocumentDataString(new
TabId, url)); |
| 579 } |
| 580 |
| 581 // For content URIs, because intent.getData().getScheme() begins with "d
ocument://, |
| 582 // we need to pass a ClipData so DocumentActivity can access the content
. |
| 583 if (url != null && url.startsWith("content://")) { |
| 584 intent.setClipData(ClipData.newUri( |
| 585 context.getContentResolver(), "content", Uri.parse(url))); |
| 586 intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); |
| 587 } |
| 588 intent.putExtra(IntentHandler.EXTRA_PARENT_TAB_ID, parentId); |
| 589 if (oldIntent != null && Intent.ACTION_VIEW.equals(oldIntent.getAction()
)) { |
| 590 intent.putExtra(IntentHandler.EXTRA_ORIGINAL_INTENT, oldIntent); |
| 591 } |
| 592 |
| 593 return intent; |
| 594 } |
| 595 |
| 596 @SuppressLint("InlinedApi") |
| 597 private void launchTabbedMode() { |
| 598 maybePrefetchDnsInBackground(); |
| 599 |
| 600 Intent newIntent = new Intent(getIntent()); |
| 601 newIntent.setClassName(getApplicationContext().getPackageName(), |
| 602 ChromeTabbedActivity.class.getName()); |
| 603 newIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY
_NEW_TASK); |
| 604 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { |
| 605 newIntent.addFlags(Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS); |
| 606 } |
| 607 Uri uri = newIntent.getData(); |
| 608 if (uri != null && "content".equals(uri.getScheme())) { |
| 609 newIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); |
| 610 } |
| 611 if (mIsInMultiInstanceMode) { |
| 612 MultiWindowUtils.getInstance().makeMultiInstanceIntent(this, newInte
nt); |
| 613 } |
| 614 startActivity(newIntent); |
| 615 } |
| 616 |
| 617 /** |
| 618 * Bring the task matching the given tab ID to the front. |
| 619 * @param tabId tab ID to search for. |
| 620 * @return Whether the task was successfully brought back. |
| 621 */ |
| 622 @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| 623 private static boolean relaunchTask(int tabId) { |
| 624 if (tabId == Tab.INVALID_TAB_ID) return false; |
| 625 |
| 626 Context context = ApplicationStatus.getApplicationContext(); |
| 627 ActivityManager manager = |
| 628 (ActivityManager) context.getSystemService(Context.ACTIVITY_SERV
ICE); |
| 629 for (AppTask task : manager.getAppTasks()) { |
| 630 RecentTaskInfo info = DocumentUtils.getTaskInfoFromTask(task); |
| 631 if (info == null) continue; |
| 632 |
| 633 int id = ActivityDelegate.getTabIdFromIntent(info.baseIntent); |
| 634 if (id != tabId) continue; |
| 635 |
| 636 DocumentTabModelSelector.setPrioritizedTabId(id); |
| 637 if (!moveToFront(task)) continue; |
| 638 |
| 639 return true; |
| 640 } |
| 641 |
| 642 return false; |
| 643 } |
| 644 |
| 645 /** |
| 646 * Bring the task matching the given URL to the front if the task is retarge
table. |
| 647 * @param incognito Whether or not the tab is incognito. |
| 648 * @param url URL that the tab would have been created for. If null, this pa
ram is ignored. |
| 649 * @return Whether the task was successfully brought back. |
| 650 */ |
| 651 @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| 652 private static boolean relaunchTask(boolean incognito, String url) { |
| 653 if (TextUtils.isEmpty(url)) return false; |
| 654 |
| 655 Context context = ApplicationStatus.getApplicationContext(); |
| 656 ActivityManager manager = |
| 657 (ActivityManager) context.getSystemService(Context.ACTIVITY_SERV
ICE); |
| 658 for (AppTask task : manager.getAppTasks()) { |
| 659 RecentTaskInfo info = DocumentUtils.getTaskInfoFromTask(task); |
| 660 if (info == null) continue; |
| 661 |
| 662 String initialUrl = ActivityDelegate.getInitialUrlForDocument(info.b
aseIntent); |
| 663 if (TextUtils.isEmpty(initialUrl) || !TextUtils.equals(initialUrl, u
rl)) continue; |
| 664 |
| 665 int id = ActivityDelegate.getTabIdFromIntent(info.baseIntent); |
| 666 DocumentTabModelSelector.setPrioritizedTabId(id); |
| 667 if (!ChromeMobileApplication.getDocumentTabModelSelector().getModel(
incognito) |
| 668 .isRetargetable(id)) { |
| 669 continue; |
| 670 } |
| 671 |
| 672 if (!moveToFront(task)) continue; |
| 673 return true; |
| 674 } |
| 675 |
| 676 return false; |
| 677 } |
| 678 |
| 679 /** |
| 680 * On opting out, remove all the old tasks from the recents. |
| 681 * @param fromDocument Whether any possible migration was from document mode
to classic. |
| 682 */ |
| 683 @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| 684 private void cleanUpChromeRecents(boolean fromDocument) { |
| 685 ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY
_SERVICE); |
| 686 List<ActivityManager.AppTask> taskList = am.getAppTasks(); |
| 687 PackageManager pm = getPackageManager(); |
| 688 for (int i = 0; i < taskList.size(); i++) { |
| 689 AppTask task = taskList.get(i); |
| 690 String className = DocumentUtils.getTaskClassName(task, pm); |
| 691 if (className == null) continue; |
| 692 |
| 693 RecentTaskInfo taskInfo = DocumentUtils.getTaskInfoFromTask(task); |
| 694 if (taskInfo == null) continue; |
| 695 |
| 696 // Skip the document activities if we are migrating from classic to
document. |
| 697 boolean skip = !fromDocument && DocumentActivity.isDocumentActivity(
className); |
| 698 if (!skip && (taskInfo.id != getTaskId())) { |
| 699 taskList.get(i).finishAndRemoveTask(); |
| 700 } |
| 701 } |
| 702 } |
| 703 |
| 704 /** |
| 705 * Set flags that ensure that we control when our Activities disappear from
Recents. |
| 706 * @param intent Intent to set the flags on. |
| 707 * @param extraFlags Other flags to add to the Intent, 0 if there's nothing
to add. |
| 708 * @param incognito Whether we are launching an incognito document. |
| 709 */ |
| 710 @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| 711 private static void setRecentsFlagsOnIntent(Intent intent, int extraFlags, b
oolean incognito) { |
| 712 intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_R
ECENTS); |
| 713 if (!incognito) intent.addFlags(Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS); |
| 714 if (extraFlags != 0) intent.addFlags(extraFlags); |
| 715 } |
| 716 |
| 717 /** |
| 718 * @return Whether there is already an browser instance of Chrome already ru
nning. |
| 719 */ |
| 720 public boolean isChromeBrowserActivityRunning() { |
| 721 for (WeakReference<Activity> reference : ApplicationStatus.getRunningAct
ivities()) { |
| 722 Activity activity = reference.get(); |
| 723 if (activity == null) continue; |
| 724 |
| 725 String className = activity.getClass().getName(); |
| 726 if (DocumentActivity.isDocumentActivity(className) |
| 727 || TextUtils.equals(className, ChromeTabbedActivity.class.ge
tName())) { |
| 728 return true; |
| 729 } |
| 730 } |
| 731 return false; |
| 732 } |
| 733 |
| 734 /** |
| 735 * Attempt to move a task back to the front. This can FAIL for some reason
because the UID |
| 736 * of the DocumentActivity we try to bring back to the front doesn't match t
he |
| 737 * ChromeLauncherActivities. |
| 738 * @param task Task to attempt to bring back to the foreground. |
| 739 * @return Whether or not this succeeded. |
| 740 */ |
| 741 @TargetApi(Build.VERSION_CODES.LOLLIPOP) |
| 742 private static boolean moveToFront(AppTask task) { |
| 743 try { |
| 744 task.moveToFront(); |
| 745 return true; |
| 746 } catch (SecurityException e) { |
| 747 sMoveToFrontExceptionHistogram.recordHit(); |
| 748 } |
| 749 return false; |
| 750 } |
| 751 |
| 752 /** |
| 753 * Tries to launch a WebappActivity for the given Intent. |
| 754 * @return Intent to fire if the webapp Intent failed to launch because of s
ecurity checks, |
| 755 * null otherwise. |
| 756 */ |
| 757 private Intent launchWebapp(Intent intent) { |
| 758 String webappId = IntentUtils.safeGetStringExtra(intent, ShortcutHelper.
EXTRA_ID); |
| 759 String webappUrl = IntentUtils.safeGetStringExtra(intent, ShortcutHelper
.EXTRA_URL); |
| 760 String webappTitle = IntentUtils.safeGetStringExtra(intent, ShortcutHelp
er.EXTRA_TITLE); |
| 761 String webappIcon = IntentUtils.safeGetStringExtra(intent, ShortcutHelpe
r.EXTRA_ICON); |
| 762 int webappOrientation = IntentUtils.safeGetIntExtra(intent, |
| 763 ShortcutHelper.EXTRA_ORIENTATION, ScreenOrientationValues.DEFAUL
T); |
| 764 |
| 765 if (webappId != null && webappUrl != null) { |
| 766 String webappMacString = IntentUtils.safeGetStringExtra( |
| 767 intent, ShortcutHelper.EXTRA_MAC); |
| 768 byte[] webappMac = |
| 769 webappMacString == null ? null : Base64.decode(webappMacStri
ng, Base64.DEFAULT); |
| 770 |
| 771 if (webappMac != null && WebappAuthenticator.isUrlValid(this, webapp
Url, webappMac)) { |
| 772 if (TextUtils.equals(ACTION_START_WEBAPP, intent.getAction())) { |
| 773 LaunchMetrics.recordHomeScreenLaunchIntoStandaloneActivity(w
ebappUrl); |
| 774 } |
| 775 |
| 776 WebappActivity.launchInstance( |
| 777 this, webappId, webappUrl, webappIcon, webappTitle, weba
ppOrientation); |
| 778 } else { |
| 779 Log.e(TAG, "Shortcut (" + webappUrl + ") opened in Chrome."); |
| 780 |
| 781 // Tried and failed. Change the intent action and try the URL w
ith a VIEW Intent. |
| 782 Intent fallbackIntent = new Intent(intent); |
| 783 fallbackIntent.setAction(Intent.ACTION_VIEW); |
| 784 fallbackIntent.setData(Uri.parse(webappUrl)); |
| 785 fallbackIntent.putExtra(BookmarkUtils.REUSE_URL_MATCHING_TAB_ELS
E_NEW_TAB, true); |
| 786 return fallbackIntent; |
| 787 } |
| 788 } |
| 789 return null; |
| 790 } |
| 791 |
| 792 /** |
| 793 * Tries to launch the First Run Experience. If ChromeLauncherActivity is r
unning with the |
| 794 * wrong Intent flags, we instead relaunch ChromeLauncherActivity to make su
re it runs in its |
| 795 * own task, which then triggers First Run. |
| 796 * @return Whether or not the First Run Experience needed to be shown. |
| 797 */ |
| 798 private boolean launchFirstRunExperience() { |
| 799 final boolean isIntentActionMain = getIntent() != null |
| 800 && TextUtils.equals(getIntent().getAction(), Intent.ACTION_MAIN)
; |
| 801 final Intent freIntent = FirstRunFlowSequencer.checkIfFirstRunIsNecessar
y( |
| 802 this, getIntent(), isIntentActionMain); |
| 803 if (freIntent == null) return false; |
| 804 |
| 805 if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0) { |
| 806 startActivityForResult(freIntent, FIRST_RUN_EXPERIENCE_REQUEST_CODE)
; |
| 807 } else { |
| 808 Intent newIntent = new Intent(getIntent()); |
| 809 newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| 810 startActivity(newIntent); |
| 811 finish(); |
| 812 } |
| 813 return true; |
| 814 } |
| 815 |
| 816 /** |
| 817 * Send the number of times an exception was caught when trying to move a ta
sk back to front. |
| 818 */ |
| 819 public static void sendExceptionCount() { |
| 820 sMoveToFrontExceptionHistogram.commitHistogram(); |
| 821 } |
| 822 } |
OLD | NEW |