// Copyright 2016 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.instantapps; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.StrictMode; import android.os.SystemClock; import android.provider.Browser; import org.chromium.base.CommandLine; import org.chromium.base.ContextUtils; import org.chromium.base.FieldTrialList; import org.chromium.base.Log; import org.chromium.chrome.browser.ChromeApplication; import org.chromium.chrome.browser.ChromeSwitches; import org.chromium.chrome.browser.IntentHandler; import org.chromium.chrome.browser.ShortcutHelper; import org.chromium.chrome.browser.externalnav.ExternalNavigationDelegateImpl; import org.chromium.chrome.browser.metrics.LaunchMetrics.TimesHistogramSample; import org.chromium.chrome.browser.preferences.ChromePreferenceManager; import org.chromium.chrome.browser.util.IntentUtils; import org.chromium.content_public.browser.WebContents; import java.util.concurrent.TimeUnit; /** A launcher for Instant Apps. */ public class InstantAppsHandler { private static final String TAG = "InstantAppsHandler"; private static final Object INSTANCE_LOCK = new Object(); private static InstantAppsHandler sInstance; private static final String CUSTOM_APPS_INSTANT_APP_EXTRA = "android.support.customtabs.extra.EXTRA_ENABLE_INSTANT_APPS"; private static final String INSTANT_APP_START_TIME_EXTRA = "org.chromium.chrome.INSTANT_APP_START_TIME"; // TODO(mariakhomenko): Depend directly on the constants once we roll to v8 libraries. private static final String DO_NOT_LAUNCH_EXTRA = "com.google.android.gms.instantapps.DO_NOT_LAUNCH_INSTANT_APP"; protected static final String IS_REFERRER_TRUSTED_EXTRA = "com.google.android.gms.instantapps.IS_REFERRER_TRUSTED"; protected static final String IS_USER_CONFIRMED_LAUNCH_EXTRA = "com.google.android.gms.instantapps.IS_USER_CONFIRMED_LAUNCH"; protected static final String TRUSTED_REFERRER_PKG_EXTRA = "com.google.android.gms.instantapps.TRUSTED_REFERRER_PKG"; public static final String IS_GOOGLE_SEARCH_REFERRER = "com.google.android.gms.instantapps.IS_GOOGLE_SEARCH_REFERRER"; /** Finch experiment name. */ private static final String INSTANT_APPS_EXPERIMENT_NAME = "InstantApps"; /** Finch experiment group which is enabled for instant apps. */ private static final String INSTANT_APPS_ENABLED_ARM = "InstantAppsEnabled"; /** Finch experiment group which is disabled for instant apps. */ private static final String INSTANT_APPS_DISABLED_ARM = "InstantAppsDisabled"; /** A histogram to record how long each handleIntent() call took. */ private static final TimesHistogramSample sHandleIntentDuration = new TimesHistogramSample( "Android.InstantApps.HandleIntentDuration", TimeUnit.MILLISECONDS); /** A histogram to record how long the fallback intent roundtrip was. */ private static final TimesHistogramSample sFallbackIntentTimes = new TimesHistogramSample( "Android.InstantApps.FallbackDuration", TimeUnit.MILLISECONDS); /** A histogram to record how long the GMS Core API call took. */ private static final TimesHistogramSample sInstantAppsApiCallTimes = new TimesHistogramSample( "Android.InstantApps.ApiCallDuration", TimeUnit.MILLISECONDS); /** @return The singleton instance of {@link InstantAppsHandler}. */ public static InstantAppsHandler getInstance() { synchronized (INSTANCE_LOCK) { if (sInstance == null) { Context appContext = ContextUtils.getApplicationContext(); sInstance = ((ChromeApplication) appContext).createInstantAppsHandler(); } } return sInstance; } /** * Check the cached value to figure out if the feature is enabled. We have to use the cached * value because native library hasn't yet been loaded. * @param context The application context. * @return Whether the feature is enabled. */ protected boolean isEnabled(Context context) { // Will go away once the feature is enabled for everyone by default. StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); try { return ChromePreferenceManager.getInstance(context).getCachedInstantAppsEnabled(); } finally { StrictMode.setThreadPolicy(oldPolicy); } } /** * Record how long the handleIntent() method took. * @param startTime The timestamp for handleIntent start time. */ private void recordHandleIntentDuration(long startTime) { sHandleIntentDuration.record(SystemClock.elapsedRealtime() - startTime); } /** * Record the amount of time spent in the instant apps API call. * @param startTime The time at which we started doing computations. */ protected void recordInstantAppsApiCallTime(long startTime) { sInstantAppsApiCallTimes.record(SystemClock.elapsedRealtime() - startTime); } /** * In the case Chrome is called through the fallback mechanism from Instant Apps, record the * amount of time the whole trip took. * @param intent The current intent. */ private void maybeRecordFallbackDuration(Intent intent) { if (intent.hasExtra(INSTANT_APP_START_TIME_EXTRA)) { Long startTime = intent.getLongExtra(INSTANT_APP_START_TIME_EXTRA, 0); if (startTime != 0) { sFallbackIntentTimes.record(SystemClock.elapsedRealtime() - startTime); } } } /** * Cache whether the Instant Apps feature is enabled. * This should only be called with the native library loaded. */ public void cacheInstantAppsEnabled() { Context context = ContextUtils.getApplicationContext(); boolean isEnabled = false; boolean wasEnabled = isEnabled(context); CommandLine instance = CommandLine.getInstance(); if (instance.hasSwitch(ChromeSwitches.DISABLE_APP_LINK)) { isEnabled = false; } else if (instance.hasSwitch(ChromeSwitches.ENABLE_APP_LINK)) { isEnabled = true; } else { String experiment = FieldTrialList.findFullName(INSTANT_APPS_EXPERIMENT_NAME); if (INSTANT_APPS_DISABLED_ARM.equals(experiment)) { isEnabled = false; } else if (INSTANT_APPS_ENABLED_ARM.equals(experiment)) { isEnabled = true; } } if (isEnabled != wasEnabled) { ChromePreferenceManager.getInstance(context).setCachedInstantAppsEnabled(isEnabled); } } /** Handle incoming intent. */ public boolean handleIncomingIntent(Context context, Intent intent, boolean isCustomTabsIntent) { long startTimeStamp = SystemClock.elapsedRealtime(); boolean result = handleIncomingIntentInternal(context, intent, isCustomTabsIntent, startTimeStamp); recordHandleIntentDuration(startTimeStamp); return result; } private boolean handleIncomingIntentInternal( Context context, Intent intent, boolean isCustomTabsIntent, long startTime) { if (!isEnabled(context) || IntentUtils.safeGetBooleanExtra(intent, DO_NOT_LAUNCH_EXTRA, false) || IntentUtils.safeGetBooleanExtra( intent, IntentHandler.EXTRA_OPEN_NEW_INCOGNITO_TAB, false) || IntentUtils.safeHasExtra(intent, ShortcutHelper.EXTRA_SOURCE) || (isCustomTabsIntent && !IntentUtils.safeGetBooleanExtra( intent, CUSTOM_APPS_INSTANT_APP_EXTRA, false)) || isIntentFromChrome(context, intent) || (IntentHandler.getUrlFromIntent(intent) == null)) { Log.i(TAG, "Not handling with Instant Apps"); return false; } maybeRecordFallbackDuration(intent); // Used to search for the intent handlers. Needs null component to return correct results. Intent intentCopy = new Intent(intent); intentCopy.setComponent(null); Intent selector = intentCopy.getSelector(); if (selector != null) selector.setComponent(null); if (!(isCustomTabsIntent || isChromeDefaultHandler(context)) || ExternalNavigationDelegateImpl.isPackageSpecializedHandler( context, null, intentCopy)) { // Chrome is not the default browser or a specialized handler exists. Log.i(TAG, "Not handling with Instant Apps because Chrome is not default or " + "there's a specialized handler"); return false; } Intent callbackIntent = new Intent(intent); callbackIntent.putExtra(DO_NOT_LAUNCH_EXTRA, true); callbackIntent.putExtra(INSTANT_APP_START_TIME_EXTRA, startTime); return tryLaunchingInstantApp(context, intent, isCustomTabsIntent, callbackIntent); } /** * Attempts to launch an Instant App, if possible. * @param context The activity context. * @param intent The incoming intent. * @param isCustomTabsIntent Whether the intent is for a CustomTab. * @param fallbackIntent The intent that will be launched by Instant Apps in case of failure to * load. * @return Whether an Instant App was launched. */ protected boolean tryLaunchingInstantApp( Context context, Intent intent, boolean isCustomTabsIntent, Intent fallbackIntent) { return false; } /** * Evaluate a navigation for whether it should launch an Instant App or show the Instant * App banner. * @return Whether an Instant App intent was started. */ public boolean handleNavigation(Context context, String url, Uri referrer, WebContents webContents) { if (!isEnabled(context)) return false; if (InstantAppsSettings.isInstantAppDefault(webContents, url)) { return launchInstantAppForNavigation(context, url, referrer); } return startCheckForInstantApps(context, url, referrer, webContents); } /** * Checks if an Instant App banner should be shown for the page we are loading. */ protected boolean startCheckForInstantApps(Context context, String url, Uri referrer, WebContents webContents) { return false; } /** * Launches an Instant App immediately, if possible. */ protected boolean launchInstantAppForNavigation(Context context, String url, Uri referrer) { return false; } /** * @return Whether the intent was fired from Chrome. This happens when the user gets a * disambiguation dialog and chooses to stay in Chrome. */ private boolean isIntentFromChrome(Context context, Intent intent) { return context.getPackageName().equals(IntentUtils.safeGetStringExtra( intent, Browser.EXTRA_APPLICATION_ID)) // We shouldn't leak internal intents with authentication tokens || IntentHandler.wasIntentSenderChrome(intent, context); } /** @return Whether Chrome is the default browser on the device. */ private boolean isChromeDefaultHandler(Context context) { StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); try { return ChromePreferenceManager.getInstance(context).getCachedChromeDefaultBrowser(); } finally { StrictMode.setThreadPolicy(oldPolicy); } } /** * Launches the Instant App from the infobar banner. */ public void launchFromBanner(InstantAppsBannerData data) { if (data.getIntent() == null) return; Intent iaIntent = data.getIntent(); if (data.getReferrer() != null) { iaIntent.putExtra(Intent.EXTRA_REFERRER, data.getReferrer()); iaIntent.putExtra(IS_REFERRER_TRUSTED_EXTRA, true); } Context appContext = ContextUtils.getApplicationContext(); iaIntent.putExtra(TRUSTED_REFERRER_PKG_EXTRA, appContext.getPackageName()); iaIntent.putExtra(IS_USER_CONFIRMED_LAUNCH_EXTRA, true); try { appContext.startActivity(iaIntent); InstantAppsSettings.setInstantAppDefault(data.getWebContents(), data.getUrl()); } catch (Exception e) { Log.e(TAG, "Could not launch instant app intent", e); } } /** * Gets the instant app intent for the given URL if one exists. * * @param url The URL whose instant app this is associated with. * @return An instant app intent for the URL if one exists. */ public Intent getInstantAppIntentForUrl(String url) { return null; } }