[go: nahoru, domu]

blob: 369aa529a3ac192a23c85c0a74c0ad1d0f8cbf42 [file] [log] [blame]
// Copyright 2014 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.customtabs;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.ContextUtils;
import org.chromium.base.IntentUtils;
import org.chromium.base.Log;
import org.chromium.base.PackageManagerUtils;
import org.chromium.base.supplier.Supplier;
import org.chromium.cc.input.BrowserControlsState;
import org.chromium.chrome.browser.AppHooks;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.ShortcutHelper;
import org.chromium.chrome.browser.app.ChromeActivity;
import org.chromium.chrome.browser.app.tab_activity_glue.ActivityTabWebContentsDelegateAndroid;
import org.chromium.chrome.browser.browserservices.BrowserServicesIntentDataProvider;
import org.chromium.chrome.browser.browserservices.permissiondelegation.TrustedWebActivityPermissionManager;
import org.chromium.chrome.browser.browserservices.ui.controller.Verifier;
import org.chromium.chrome.browser.compositor.bottombar.ephemeraltab.EphemeralTabCoordinator;
import org.chromium.chrome.browser.contextmenu.ChromeContextMenuPopulator;
import org.chromium.chrome.browser.contextmenu.ChromeContextMenuPopulatorFactory;
import org.chromium.chrome.browser.contextmenu.ContextMenuPopulatorFactory;
import org.chromium.chrome.browser.customtabs.features.toolbar.CustomTabBrowserControlsVisibilityDelegate;
import org.chromium.chrome.browser.dependency_injection.ActivityScope;
import org.chromium.chrome.browser.externalauth.ExternalAuthUtils;
import org.chromium.chrome.browser.externalnav.ExternalNavigationDelegateImpl;
import org.chromium.chrome.browser.flags.ActivityType;
import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
import org.chromium.chrome.browser.share.ShareDelegate;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabAssociatedApp;
import org.chromium.chrome.browser.tab.TabContextMenuItemDelegate;
import org.chromium.chrome.browser.tab.TabDelegateFactory;
import org.chromium.chrome.browser.tab.TabLaunchType;
import org.chromium.chrome.browser.tab.TabStateBrowserControlsVisibilityDelegate;
import org.chromium.chrome.browser.tab.TabWebContentsDelegateAndroid;
import org.chromium.chrome.browser.tabmodel.AsyncTabCreationParams;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.document.TabDelegate;
import org.chromium.chrome.browser.ui.native_page.NativePage;
import org.chromium.chrome.browser.webapps.WebDisplayMode;
import org.chromium.chrome.browser.webapps.WebappActivity;
import org.chromium.chrome.browser.webapps.WebappExtras;
import org.chromium.chrome.browser.webapps.WebappIntentUtils;
import org.chromium.chrome.browser.webapps.WebappLauncherActivity;
import org.chromium.components.browser_ui.util.BrowserControlsVisibilityDelegate;
import org.chromium.components.browser_ui.util.ComposedBrowserControlsVisibilityDelegate;
import org.chromium.components.embedder_support.delegate.WebContentsDelegateAndroid;
import org.chromium.components.embedder_support.util.UrlUtilities;
import org.chromium.components.external_intents.ExternalNavigationHandler;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.common.ResourceRequestBody;
import org.chromium.ui.mojom.WindowOpenDisposition;
import javax.inject.Inject;
import dagger.Lazy;
/**
* A {@link TabDelegateFactory} class to be used in all {@link Tab} owned
* by a {@link CustomTabActivity}.
*/
@ActivityScope
public class CustomTabDelegateFactory implements TabDelegateFactory {
/** Action for do-nothing activity for activating WebAPK. */
private static final String ACTION_ACTIVATE_WEBAPK =
"org.chromium.chrome.browser.webapps.ActivateWebApkActivity.ACTIVATE";
/**
* A custom external navigation delegate that forbids the intent picker from showing up.
*/
static class CustomTabNavigationDelegate extends ExternalNavigationDelegateImpl {
private static final String TAG = "customtabs";
private final String mClientPackageName;
private final ExternalAuthUtils mExternalAuthUtils;
private final Verifier mVerifier;
private final @ActivityType int mActivityType;
private boolean mHasActivityStarted;
/**
* Constructs a new instance of {@link CustomTabNavigationDelegate}.
*/
CustomTabNavigationDelegate(Tab tab, ExternalAuthUtils authUtils, Verifier verifier,
@ActivityType int activityType) {
super(tab);
mClientPackageName = TabAssociatedApp.from(tab).getAppId();
mExternalAuthUtils = authUtils;
mVerifier = verifier;
mActivityType = activityType;
}
@Override
public void didStartActivity(Intent intent) {
mHasActivityStarted = true;
}
@Override
public @StartActivityIfNeededResult int maybeHandleStartActivityIfNeeded(
Intent intent, boolean proxy) {
// Note: This method will not be called if shouldDisableExternalIntentRequestsForUrl()
// returns false.
boolean isExternalProtocol = !UrlUtilities.isAcceptedScheme(intent.toUri(0));
boolean hasDefaultHandler = hasDefaultHandler(intent);
try {
// For a URL chrome can handle and there is no default set, handle it ourselves.
if (!hasDefaultHandler) {
if (!TextUtils.isEmpty(mClientPackageName)
&& isPackageSpecializedHandler(mClientPackageName, intent)) {
intent.setPackage(mClientPackageName);
} else if (!isExternalProtocol) {
return StartActivityIfNeededResult.HANDLED_WITHOUT_ACTIVITY_START;
}
}
if (proxy) {
dispatchAuthenticatedIntent(intent);
mHasActivityStarted = true;
return StartActivityIfNeededResult.HANDLED_WITH_ACTIVITY_START;
} else {
// If android fails to find a handler, handle it ourselves.
Context context = getAvailableContext();
if (context instanceof Activity
&& ((Activity) context).startActivityIfNeeded(intent, -1)) {
mHasActivityStarted = true;
return StartActivityIfNeededResult.HANDLED_WITH_ACTIVITY_START;
}
}
return StartActivityIfNeededResult.HANDLED_WITHOUT_ACTIVITY_START;
} catch (SecurityException e) {
// https://crbug.com/808494: Handle the URL in Chrome if dispatching to another
// application fails with a SecurityException. This happens due to malformed
// manifests in another app.
return StartActivityIfNeededResult.HANDLED_WITHOUT_ACTIVITY_START;
} catch (RuntimeException e) {
IntentUtils.logTransactionTooLargeOrRethrow(e, intent);
return StartActivityIfNeededResult.HANDLED_WITHOUT_ACTIVITY_START;
}
}
/**
* Resolve the default external handler of an intent.
* @return Whether the default external handler is found: if chrome turns out to be the
* default handler, this method will return false.
*/
private boolean hasDefaultHandler(Intent intent) {
ResolveInfo info = PackageManagerUtils.resolveActivity(intent, 0);
if (info == null) return false;
final String chromePackage = mApplicationContext.getPackageName();
// If a default handler is found and it is not chrome itself, fire the intent.
return info.match != 0 && !chromePackage.equals(info.activityInfo.packageName);
}
@Override
public boolean isIntentForTrustedCallingApp(Intent intent) {
if (TextUtils.isEmpty(mClientPackageName)) return false;
if (!mExternalAuthUtils.isGoogleSigned(mClientPackageName)) return false;
return isPackageSpecializedHandler(mClientPackageName, intent);
}
/**
* @return Whether an external activity has started to handle a url. For testing only.
*/
@VisibleForTesting
public boolean hasExternalActivityStarted() {
return mHasActivityStarted;
}
@Override
public boolean shouldDisableExternalIntentRequestsForUrl(String url) {
// http://crbug.com/647569 : Do not forward URL requests to external intents for URLs
// within the Webapp/TWA's scope.
return mVerifier != null && mVerifier.shouldIgnoreExternalIntentHandlers(url);
}
}
private static class CustomTabWebContentsDelegate
extends ActivityTabWebContentsDelegateAndroid {
private static final String TAG = "CustomTabWebContentsDelegate";
private final ChromeActivity mActivity;
private final @ActivityType int mActivityType;
private final @Nullable String mWebApkScopeUrl;
private final @WebDisplayMode int mDisplayMode;
private final MultiWindowUtils mMultiWindowUtils;
private final boolean mShouldEnableEmbeddedMediaExperience;
private final @Nullable PendingIntent mFocusIntent;
/**
* See {@link TabWebContentsDelegateAndroid}.
*/
public CustomTabWebContentsDelegate(Tab tab, ChromeActivity activity,
@ActivityType int activityType, @Nullable String webApkScopeUrl,
@WebDisplayMode int displayMode, MultiWindowUtils multiWindowUtils,
boolean shouldEnableEmbeddedMediaExperience, @Nullable PendingIntent focusIntent) {
super(tab, activity);
mActivity = activity;
mActivityType = activityType;
mWebApkScopeUrl = webApkScopeUrl;
mDisplayMode = displayMode;
mMultiWindowUtils = multiWindowUtils;
mShouldEnableEmbeddedMediaExperience = shouldEnableEmbeddedMediaExperience;
mFocusIntent = focusIntent;
}
@Override
public boolean shouldResumeRequestsForCreatedWindow() {
return true;
}
@Override
protected void bringActivityToForeground() {
// TODO: Unify WebAPK and CCT behavior.
if (isWebappOrWebApk(mActivityType)) {
bringActivityToForegroundWebapp();
} else {
bringActivityToForegroundCustomTab();
}
}
private void bringActivityToForegroundCustomTab() {
// super.bringActivityToForeground creates an Intent to ChromeLauncherActivity for the
// current Tab. This will then bring to the foreground the Chrome Task that contains
// the Chrome Activity displaying the tab (this will usually be a ChromeTabbedActivity).
// Since Custom Tabs will be launched as part of the client's Task, we can't launch a
// Chrome Intent specifically to the Chrome Activity in that Task. Therefore we must
// use an Intent the client has provided to bring its Task to the foreground.
// If we've not been provided with an Intent to focus the client's Task, we can't do
// anything.
if (mFocusIntent == null) return;
try {
mFocusIntent.send();
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "CanceledException when sending pending intent.");
}
}
private void bringActivityToForegroundWebapp() {
WebappActivity webappActivity = (WebappActivity) mActivity;
BrowserServicesIntentDataProvider intentDataProvider =
webappActivity.getIntentDataProvider();
// Create an Intent that will be fired toward the WebappLauncherActivity, which in turn
// will fire an Intent to launch the correct WebappActivity. On L+ this could probably
// be changed to call AppTask.moveToFront(), but for backwards compatibility we relaunch
// it the hard way.
String startUrl = intentDataProvider.getUrlToLoad();
if (intentDataProvider.isWebApkActivity()) {
Intent activateIntent = null;
activateIntent = new Intent(ACTION_ACTIVATE_WEBAPK);
activateIntent.setPackage(ContextUtils.getApplicationContext().getPackageName());
IntentUtils.safeStartActivity(mActivity, activateIntent);
return;
}
Intent intent = new Intent();
intent.setAction(WebappLauncherActivity.ACTION_START_WEBAPP);
intent.setPackage(mActivity.getPackageName());
WebappIntentUtils.copyWebappLaunchIntentExtras(mActivity.getIntent(), intent);
intent.putExtra(ShortcutHelper.EXTRA_MAC, ShortcutHelper.getEncodedMac(startUrl));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
IntentUtils.safeStartActivity(ContextUtils.getApplicationContext(), intent);
}
@Override
protected boolean shouldEnableEmbeddedMediaExperience() {
return mShouldEnableEmbeddedMediaExperience;
}
@Override
public void openNewTab(String url, String extraHeaders, ResourceRequestBody postData,
int disposition, boolean isRendererInitiated) {
// If attempting to open an incognito tab, always send the user to tabbed mode.
if (disposition == WindowOpenDisposition.OFF_THE_RECORD) {
if (isRendererInitiated) {
throw new IllegalStateException(
"Invalid attempt to open an incognito tab from the renderer");
}
LoadUrlParams loadUrlParams = new LoadUrlParams(url);
loadUrlParams.setVerbatimHeaders(extraHeaders);
loadUrlParams.setPostData(postData);
loadUrlParams.setIsRendererInitiated(isRendererInitiated);
Class<? extends ChromeTabbedActivity> tabbedClass =
mMultiWindowUtils.getTabbedActivityForIntent(
null, ContextUtils.getApplicationContext());
AsyncTabCreationParams tabParams = new AsyncTabCreationParams(loadUrlParams,
new ComponentName(ContextUtils.getApplicationContext(), tabbedClass));
new TabDelegate(true).createNewTab(tabParams,
TabLaunchType.FROM_LONGPRESS_FOREGROUND, TabModel.INVALID_TAB_INDEX);
return;
}
super.openNewTab(url, extraHeaders, postData, disposition, isRendererInitiated);
}
@Override
public @WebDisplayMode int getDisplayMode() {
return mDisplayMode;
}
@Override
protected String getManifestScope() {
return mWebApkScopeUrl;
}
@Override
public boolean canShowAppBanners() {
return mActivityType == ActivityType.CUSTOM_TAB;
}
@Override
protected boolean isInstalledWebappDelegateGeolocation() {
if ((mActivity instanceof CustomTabActivity)
&& ((CustomTabActivity) mActivity).isInTwaMode()) {
// Whether the corresponding TWA client app enrolled in location delegation.
return TrustedWebActivityPermissionManager.hasAndroidLocationPermission(
((CustomTabActivity) mActivity).getTwaPackage())
!= null;
}
return false;
}
}
private final ChromeActivity<?> mActivity;
private final boolean mShouldHideBrowserControls;
private final boolean mIsOpenedByChrome;
private final @ActivityType int mActivityType;
@Nullable
private final String mWebApkScopeUrl;
private final @WebDisplayMode int mDisplayMode;
private final boolean mShouldEnableEmbeddedMediaExperience;
private final BrowserControlsVisibilityDelegate mBrowserStateVisibilityDelegate;
private final ExternalAuthUtils mExternalAuthUtils;
private final MultiWindowUtils mMultiWindowUtils;
private final PendingIntent mFocusIntent;
private final Verifier mVerifier;
private TabWebContentsDelegateAndroid mWebContentsDelegateAndroid;
private ExternalNavigationDelegateImpl mNavigationDelegate;
private Lazy<EphemeralTabCoordinator> mEphemeralTabCoordinator;
/**
* @param activity {@link ChromeActivity} instance.
* @param shouldHideBrowserControls Whether or not the browser controls may auto-hide.
* @param isOpenedByChrome Whether the CustomTab was originally opened by Chrome.
* @param webApkScopeUrl The URL of the WebAPK web manifest scope. Null if the delegate is not
* for a WebAPK.
* @param displayMode The activity's display mode.
* @param shouldEnableEmbeddedMediaExperience Whether embedded media experience is enabled.
* @param visibilityDelegate The delegate that handles browser control visibility associated
* with browser actions (as opposed to tab state).
* @param authUtils To determine whether apps are Google signed.
* @param multiWindowUtils To use to determine which ChromeTabbedActivity to open new tabs in.
* @param focusIntent A PendingIntent to launch to focus the client.
* @param verifier Decides how to handle navigation to a new URL.
* @param ephemeralTabCoordinatorSupplier A provider of {@link EphemeralTabCoordinator} that
* shows preview tab.
*/
private CustomTabDelegateFactory(ChromeActivity<?> activity, boolean shouldHideBrowserControls,
boolean isOpenedByChrome, @Nullable String webApkScopeUrl,
@WebDisplayMode int displayMode, boolean shouldEnableEmbeddedMediaExperience,
BrowserControlsVisibilityDelegate visibilityDelegate, ExternalAuthUtils authUtils,
MultiWindowUtils multiWindowUtils, @Nullable PendingIntent focusIntent,
Verifier verifier, Lazy<EphemeralTabCoordinator> ephemeralTabCoordinator) {
mActivity = activity;
mShouldHideBrowserControls = shouldHideBrowserControls;
mIsOpenedByChrome = isOpenedByChrome;
mActivityType = (activity == null) ? ActivityType.CUSTOM_TAB : activity.getActivityType();
mWebApkScopeUrl = webApkScopeUrl;
mDisplayMode = displayMode;
mShouldEnableEmbeddedMediaExperience = shouldEnableEmbeddedMediaExperience;
mBrowserStateVisibilityDelegate = visibilityDelegate;
mExternalAuthUtils = authUtils;
mMultiWindowUtils = multiWindowUtils;
mFocusIntent = focusIntent;
mVerifier = verifier;
mEphemeralTabCoordinator = ephemeralTabCoordinator;
}
@Inject
public CustomTabDelegateFactory(ChromeActivity<?> activity,
BrowserServicesIntentDataProvider intentDataProvider,
CustomTabBrowserControlsVisibilityDelegate visibilityDelegate,
ExternalAuthUtils authUtils, MultiWindowUtils multiWindowUtils, Verifier verifier,
Lazy<EphemeralTabCoordinator> ephemeralTabCoordinator) {
this(activity, intentDataProvider.shouldEnableUrlBarHiding(),
intentDataProvider.isOpenedByChrome(), getWebApkScopeUrl(intentDataProvider),
getDisplayMode(intentDataProvider),
intentDataProvider.shouldEnableEmbeddedMediaExperience(), visibilityDelegate,
authUtils, multiWindowUtils, intentDataProvider.getFocusIntent(), verifier,
ephemeralTabCoordinator);
}
/**
* Creates a basic/empty CustomTabDelegateFactory for use when creating a hidden tab. It will
* be replaced when the hidden Tab becomes shown.
*/
static CustomTabDelegateFactory createDummy() {
return new CustomTabDelegateFactory(null, false, false, null, WebDisplayMode.BROWSER, false,
null, null, null, null, null, () -> null);
}
@Override
public BrowserControlsVisibilityDelegate createBrowserControlsVisibilityDelegate(Tab tab) {
TabStateBrowserControlsVisibilityDelegate tabDelegate =
new TabStateBrowserControlsVisibilityDelegate(tab) {
@Override
protected int calculateVisibilityConstraints() {
@BrowserControlsState
int constraints = super.calculateVisibilityConstraints();
if (constraints == BrowserControlsState.BOTH
&& !mShouldHideBrowserControls) {
return BrowserControlsState.SHOWN;
}
return constraints;
}
};
// mBrowserStateVisibilityDelegate == null for background tabs for which
// fullscreen state info from BrowserStateVisibilityDelegate is not available.
return mBrowserStateVisibilityDelegate == null
? tabDelegate
: new ComposedBrowserControlsVisibilityDelegate(
tabDelegate, mBrowserStateVisibilityDelegate);
}
@Override
public TabWebContentsDelegateAndroid createWebContentsDelegate(Tab tab) {
mWebContentsDelegateAndroid = new CustomTabWebContentsDelegate(tab, mActivity,
mActivityType, mWebApkScopeUrl, mDisplayMode, mMultiWindowUtils,
mShouldEnableEmbeddedMediaExperience, mFocusIntent);
return mWebContentsDelegateAndroid;
}
@Override
public ExternalNavigationHandler createExternalNavigationHandler(Tab tab) {
if (mIsOpenedByChrome) {
mNavigationDelegate = new ExternalNavigationDelegateImpl(tab);
} else {
mNavigationDelegate = new CustomTabNavigationDelegate(
tab, mExternalAuthUtils, mVerifier, mActivityType);
}
return new ExternalNavigationHandler(mNavigationDelegate);
}
@Override
public ContextMenuPopulatorFactory createContextMenuPopulatorFactory(Tab tab) {
@ChromeContextMenuPopulator.ContextMenuMode
int contextMenuMode = getContextMenuMode(mActivityType);
Supplier<ShareDelegate> shareDelegateSupplier =
mActivity == null ? null : mActivity.getShareDelegateSupplier();
TabModelSelector tabModelSelector =
mActivity != null ? mActivity.getTabModelSelector() : null;
return new ChromeContextMenuPopulatorFactory(
new TabContextMenuItemDelegate(tab, tabModelSelector,
EphemeralTabCoordinator.isSupported() ? mEphemeralTabCoordinator::get : ()
-> null,
() -> {}, mActivity == null ? null : mActivity::getSnackbarManager),
shareDelegateSupplier, contextMenuMode, AppHooks.get().getExternalAuthUtils());
}
@Override
public NativePage createNativePage(String url, NativePage candidatePage, Tab tab) {
// Custom tab does not create native pages.
return null;
}
/**
* @return The {@link CustomTabNavigationDelegate} in this tab. For test purpose only.
*/
@VisibleForTesting
ExternalNavigationDelegateImpl getExternalNavigationDelegate() {
return mNavigationDelegate;
}
public WebContentsDelegateAndroid getWebContentsDelegate() {
return mWebContentsDelegateAndroid;
}
/**
* Gets the scope URL if the {@link BrowserServicesIntentDataProvider} is for a WebAPK. Returns
* null otherwise.
*/
private static @Nullable String getWebApkScopeUrl(
BrowserServicesIntentDataProvider intentDataProvider) {
if (!intentDataProvider.isWebApkActivity()) return null;
WebappExtras webappExtras = intentDataProvider.getWebappExtras();
return (webappExtras != null) ? webappExtras.scopeUrl : null;
}
/**
* Returns the WebDisplayMode for the passed-in {@link BrowserServicesIntentDataProvider}.
*/
public static @WebDisplayMode int getDisplayMode(
BrowserServicesIntentDataProvider intentDataProvider) {
WebappExtras webappExtras = intentDataProvider.getWebappExtras();
if (webappExtras != null) {
return webappExtras.displayMode;
}
return intentDataProvider.isTrustedWebActivity() ? WebDisplayMode.STANDALONE
: WebDisplayMode.BROWSER;
}
private static boolean isWebappOrWebApk(@ActivityType int activityType) {
return activityType == ActivityType.WEBAPP || activityType == ActivityType.WEB_APK;
}
private static @ChromeContextMenuPopulator.ContextMenuMode int getContextMenuMode(
@ActivityType int activityType) {
return isWebappOrWebApk(activityType)
? ChromeContextMenuPopulator.ContextMenuMode.WEB_APP
: ChromeContextMenuPopulator.ContextMenuMode.CUSTOM_TAB;
}
}