// 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.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.os.TransactionTooLargeException;
import android.text.TextUtils;
import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.contextmenu.ChromeContextMenuPopulator;
import org.chromium.chrome.browser.contextmenu.ContextMenuPopulator;
import org.chromium.chrome.browser.externalnav.ExternalNavigationDelegateImpl;
import org.chromium.chrome.browser.externalnav.ExternalNavigationHandler;
import org.chromium.chrome.browser.tab.InterceptNavigationDelegateImpl;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabContextMenuItemDelegate;
import org.chromium.chrome.browser.tab.TabDelegateFactory;
import org.chromium.chrome.browser.tab.TabWebContentsDelegateAndroid;
import org.chromium.chrome.browser.tab.TopControlsVisibilityDelegate;
import org.chromium.chrome.browser.util.UrlUtilities;
/**
* A {@link TabDelegateFactory} class to be used in all {@link Tab} owned
* by a {@link CustomTabActivity}.
*/
public class CustomTabDelegateFactory extends TabDelegateFactory {
/**
* 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 boolean mHasActivityStarted;
/**
* Constructs a new instance of {@link CustomTabNavigationDelegate}.
*/
public CustomTabNavigationDelegate(Tab tab, String clientPackageName) {
super(tab);
mClientPackageName = clientPackageName;
}
@Override
public void startActivity(Intent intent, boolean proxy) {
super.startActivity(intent, proxy);
mHasActivityStarted = true;
}
@Override
public boolean startActivityIfNeeded(Intent intent, boolean proxy) {
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(
mApplicationContext, mClientPackageName, intent)) {
intent.setPackage(mClientPackageName);
} else if (!isExternalProtocol) {
return false;
}
}
if (proxy) {
dispatchAuthenticatedIntent(intent);
mHasActivityStarted = true;
return true;
} 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 true;
}
}
return false;
} catch (RuntimeException e) {
logTransactionTooLargeOrRethrow(e, intent);
return false;
}
}
/**
* 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) {
try {
ResolveInfo info =
mApplicationContext.getPackageManager().resolveActivity(intent, 0);
if (info != null) {
final String chromePackage = mApplicationContext.getPackageName();
// If a default handler is found and it is not chrome itself, fire the intent.
if (info.match != 0 && !chromePackage.equals(info.activityInfo.packageName)) {
return true;
}
}
} catch (RuntimeException e) {
logTransactionTooLargeOrRethrow(e, intent);
}
return false;
}
/**
* @return Whether an external activity has started to handle a url. For testing only.
*/
@VisibleForTesting
public boolean hasExternalActivityStarted() {
return mHasActivityStarted;
}
private static void logTransactionTooLargeOrRethrow(RuntimeException e, Intent intent) {
// See http://crbug.com/369574.
if (e.getCause() instanceof TransactionTooLargeException) {
Log.e(TAG, "Could not resolve Activity for intent " + intent.toString(), e);
} else {
throw e;
}
}
}
private static class CustomTabWebContentsDelegate extends TabWebContentsDelegateAndroid {
/**
* See {@link TabWebContentsDelegateAndroid}.
*/
public CustomTabWebContentsDelegate(Tab tab) {
super(tab);
}
@Override
public boolean shouldResumeRequestsForCreatedWindow() {
return true;
}
@Override
protected void bringActivityToForeground() {
// No-op here. If client's task is in background Chrome is unable to foreground it.
}
}
private final boolean mShouldHideTopControls;
private final boolean mIsOpenedByChrome;
private ExternalNavigationDelegateImpl mNavigationDelegate;
private ExternalNavigationHandler mNavigationHandler;
/**
* @param shouldHideTopControls Whether or not the top controls may auto-hide.
*/
public CustomTabDelegateFactory(boolean shouldHideTopControls, boolean isOpenedByChrome) {
mShouldHideTopControls = shouldHideTopControls;
mIsOpenedByChrome = isOpenedByChrome;
}
@Override
public TopControlsVisibilityDelegate createTopControlsVisibilityDelegate(Tab tab) {
return new TopControlsVisibilityDelegate(tab) {
@Override
public boolean isHidingTopControlsEnabled() {
return mShouldHideTopControls && super.isHidingTopControlsEnabled();
}
};
}
@Override
public TabWebContentsDelegateAndroid createWebContentsDelegate(Tab tab) {
return new CustomTabWebContentsDelegate(tab);
}
@Override
public InterceptNavigationDelegateImpl createInterceptNavigationDelegate(Tab tab) {
if (mIsOpenedByChrome) {
mNavigationDelegate = new ExternalNavigationDelegateImpl(tab);
} else {
mNavigationDelegate = new CustomTabNavigationDelegate(tab, tab.getAppAssociatedWith());
}
mNavigationHandler = new ExternalNavigationHandler(mNavigationDelegate);
return new InterceptNavigationDelegateImpl(mNavigationHandler, tab);
}
@Override
public ContextMenuPopulator createContextMenuPopulator(Tab tab) {
return new ChromeContextMenuPopulator(new TabContextMenuItemDelegate(tab),
ChromeContextMenuPopulator.CUSTOM_TAB_MODE);
}
/**
* @return The {@link ExternalNavigationHandler} in this tab. For test purpose only.
*/
@VisibleForTesting
ExternalNavigationHandler getExternalNavigationHandler() {
return mNavigationHandler;
}
/**
* @return The {@link CustomTabNavigationDelegate} in this tab. For test purpose only.
*/
@VisibleForTesting
ExternalNavigationDelegateImpl getExternalNavigationDelegate() {
return mNavigationDelegate;
}
@Override
public boolean canShowAppBanners(Tab tab) {
return false;
}
}