Dynamically load a CCT module from another package.
* Uses AIDL interfaces to work across class loaders
* Adds a bottom bar in CCT if the module is loaded
Bug: 843161
Change-Id: I8eaaaebf78cfb875f61deebca8df3e98a4f8c0dc
Reviewed-on: https://chromium-review.googlesource.com/1085060
Commit-Queue: Michael van Ouwerkerk <[email protected]>
Reviewed-by: Ted Choc <[email protected]>
Reviewed-by: Bernhard Bauer <[email protected]>
Cr-Commit-Position: refs/heads/master@{#568438}
diff --git a/chrome/android/BUILD.gn b/chrome/android/BUILD.gn
index 5e5da00e..5d327b9 100644
--- a/chrome/android/BUILD.gn
+++ b/chrome/android/BUILD.gn
@@ -189,6 +189,17 @@
]
}
+android_aidl("cct_dynamic_module_aidl") {
+ import_include = [ "java/src" ]
+ sources = [
+ "java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/IActivityDelegate.aidl",
+ "java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/IActivityHost.aidl",
+ "java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/IModuleEntryPoint.aidl",
+ "java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/IModuleHost.aidl",
+ "java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/IObjectWrapper.aidl",
+ ]
+}
+
android_library("chrome_java") {
deps = [
":chrome_java_resources",
@@ -292,6 +303,7 @@
":chrome_android_java_enums_srcjar",
":chrome_android_java_google_api_keys_srcjar",
":chrome_version_srcjar",
+ ":cct_dynamic_module_aidl",
":photo_picker_aidl",
":resource_id_javagen",
"//chrome:assist_ranker_prediction_enum_javagen",
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ChromeFeatureList.java b/chrome/android/java/src/org/chromium/chrome/browser/ChromeFeatureList.java
index 67821c1e..f5d4c87 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ChromeFeatureList.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ChromeFeatureList.java
@@ -157,6 +157,7 @@
public static final String CAF_MEDIA_ROUTER_IMPL = "CafMediaRouterImpl";
public static final String CAPTIVE_PORTAL_CERTIFICATE_LIST = "CaptivePortalCertificateList";
public static final String CCT_BACKGROUND_TAB = "CCTBackgroundTab";
+ public static final String CCT_MODULE = "CCTModule";
public static final String CCT_EXTERNAL_LINK_HANDLING = "CCTExternalLinkHandling";
public static final String CCT_PARALLEL_REQUEST = "CCTParallelRequest";
public static final String CCT_POST_MESSAGE_API = "CCTPostMessageAPI";
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabActivity.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabActivity.java
index ec08a2af1..87d494e 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabActivity.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabActivity.java
@@ -59,7 +59,11 @@
import org.chromium.chrome.browser.browserservices.BrowserSessionContentHandler;
import org.chromium.chrome.browser.browserservices.BrowserSessionContentUtils;
import org.chromium.chrome.browser.compositor.layouts.LayoutManager;
+import org.chromium.chrome.browser.customtabs.dynamicmodule.ActivityDelegate;
+import org.chromium.chrome.browser.customtabs.dynamicmodule.ActivityHostImpl;
+import org.chromium.chrome.browser.customtabs.dynamicmodule.ModuleEntryPoint;
import org.chromium.chrome.browser.datausage.DataUseTabUIManager;
+import org.chromium.chrome.browser.externalauth.ExternalAuthUtils;
import org.chromium.chrome.browser.externalnav.ExternalNavigationDelegateImpl;
import org.chromium.chrome.browser.firstrun.FirstRunSignInProcessor;
import org.chromium.chrome.browser.fullscreen.BrowserStateBrowserControlsVisibilityDelegate;
@@ -153,6 +157,8 @@
private WebappCustomTabTimeSpentLogger mWebappTimeSpentLogger;
+ @Nullable private ActivityDelegate mActivityDelegate;
+
private static class PageLoadMetricsObserver implements PageLoadMetrics.Observer {
private final CustomTabsConnection mConnection;
private final CustomTabsSessionToken mSession;
@@ -338,6 +344,39 @@
}
}
+ /**
+ * Dynamically loads a module using the package and class names specified in the intent, if it
+ * is not loaded yet.
+ */
+ private void maybeLoadModule() {
+ // TODO(https://crbug.com/853728): Load the module in the background.
+ if (!ChromeFeatureList.isEnabled(ChromeFeatureList.CCT_MODULE)) {
+ Log.w(TAG, "The %s feature is disabled.", ChromeFeatureList.CCT_MODULE);
+ return;
+ }
+
+ String packageName = mIntentDataProvider.getModulePackageName();
+ if (!ExternalAuthUtils.getInstance().isGoogleSigned(packageName)) {
+ Log.w(TAG, "The %s package is not Google-signed.", packageName);
+ return;
+ }
+
+ ModuleEntryPoint entryPoint =
+ mConnection.loadModule(packageName, mIntentDataProvider.getModuleClassName());
+ if (entryPoint == null) return;
+
+ mActivityDelegate = entryPoint.createActivityDelegate(new ActivityHostImpl(this));
+ mActivityDelegate.onCreate(getSavedInstanceState());
+ if (mBottomBarDelegate != null) {
+ mBottomBarDelegate.setBottomBarContentView(mActivityDelegate.getBottomBarView());
+ mBottomBarDelegate.showBottomBarIfNecessary();
+ }
+
+ ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+ addContentView(mActivityDelegate.getOverlayView(), layoutParams);
+ }
+
@Override
public boolean shouldAllocateChildConnection() {
return !mHasCreatedTabEarly && !mHasSpeculated
@@ -539,6 +578,9 @@
return entry != null ? entry.getUrl() : null;
}
};
+
+ maybeLoadModule();
+
recordClientPackageName();
mConnection.showSignInToastIfNecessary(mSession, getIntent());
String url = getUrlToLoad();
@@ -704,6 +746,11 @@
super.onStartWithNative();
BrowserSessionContentUtils.setActiveContentHandler(mBrowserSessionContentHandler);
if (mHasCreatedTabEarly && !mMainTab.isLoading()) postDeferredStartupIfNeeded();
+ if (mActivityDelegate != null) {
+ mActivityDelegate.onStart();
+ mActivityDelegate.onRestoreInstanceState(getSavedInstanceState());
+ mActivityDelegate.onPostCreate(getSavedInstanceState());
+ }
}
@Override
@@ -739,6 +786,7 @@
mWebappTimeSpentLogger = WebappCustomTabTimeSpentLogger.createInstanceAndStartTimer(
getIntent().getIntExtra(CustomTabIntentDataProvider.EXTRA_BROWSER_LAUNCH_SOURCE,
ACTIVITY_TYPE_OTHER));
+ if (mActivityDelegate != null) mActivityDelegate.onResume();
}
@Override
@@ -747,12 +795,14 @@
if (mWebappTimeSpentLogger != null) {
mWebappTimeSpentLogger.onPause();
}
+ if (mActivityDelegate != null) mActivityDelegate.onPause(isChangingConfigurations());
}
@Override
public void onStopWithNative() {
super.onStopWithNative();
BrowserSessionContentUtils.setActiveContentHandler(null);
+ if (mActivityDelegate != null) mActivityDelegate.onStop(isChangingConfigurations());
if (mIsClosing) {
getTabModelSelector().closeAllTabs(true);
mTabPersistencePolicy.deleteMetadataStateFileAsync();
@@ -761,6 +811,24 @@
}
}
+ @Override
+ protected void onDestroyInternal() {
+ super.onDestroyInternal();
+ if (mActivityDelegate != null) mActivityDelegate.onDestroy(isChangingConfigurations());
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (mActivityDelegate != null) mActivityDelegate.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ super.onWindowFocusChanged(hasFocus);
+ if (mActivityDelegate != null) mActivityDelegate.onWindowFocusChanged(hasFocus);
+ }
+
/**
* Loads the current tab with the given load params while taking client
* referrer and extra headers into account.
@@ -928,6 +996,8 @@
if (exitFullscreenIfShowing()) return true;
+ if (mActivityDelegate != null && mActivityDelegate.onBackPressed()) return true;
+
if (!getToolbarManager().back()) {
if (getCurrentTabModel().getCount() > 1) {
getCurrentTabModel().closeTab(getActivityTab(), false, false, false);
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabBottomBarDelegate.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabBottomBarDelegate.java
index 02176f4..fa8b78c 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabBottomBarDelegate.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabBottomBarDelegate.java
@@ -48,6 +48,7 @@
private ChromeActivity mActivity;
private ChromeFullscreenManager mFullscreenManager;
private ViewGroup mBottomBarView;
+ @Nullable private View mBottomBarContentView;
private CustomTabIntentDataProvider mDataProvider;
private PendingIntent mClickPendingIntent;
private int[] mClickableIDs;
@@ -74,7 +75,12 @@
* Makes the bottom bar area to show, if any.
*/
public void showBottomBarIfNecessary() {
- if (!mDataProvider.shouldShowBottomBar()) return;
+ if (!shouldShowBottomBar()) return;
+
+ if (mBottomBarContentView != null) {
+ getBottomBarView().addView(mBottomBarContentView);
+ return;
+ }
RemoteViews remoteViews = mDataProvider.getBottomBarRemoteViews();
if (remoteViews != null) {
@@ -140,10 +146,17 @@
}
/**
+ * Sets the content of the bottom bar.
+ */
+ public void setBottomBarContentView(View view) {
+ mBottomBarContentView = view;
+ }
+
+ /**
* @return The height of the bottom bar, excluding its top shadow.
*/
public int getBottomBarHeight() {
- if (!mDataProvider.shouldShowBottomBar() || mBottomBarView == null
+ if (!shouldShowBottomBar() || mBottomBarView == null
|| mBottomBarView.getChildCount() < 2) {
return 0;
}
@@ -244,6 +257,10 @@
}
}
+ private boolean shouldShowBottomBar() {
+ return mBottomBarContentView != null || mDataProvider.shouldShowBottomBar();
+ }
+
// FullscreenListener methods
@Override
public void onControlsOffsetChanged(float topOffset, float bottomOffset,
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabIntentDataProvider.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabIntentDataProvider.java
index e5816746..1c264dc 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabIntentDataProvider.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabIntentDataProvider.java
@@ -15,6 +15,7 @@
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
import android.support.customtabs.CustomTabsIntent;
import android.text.TextUtils;
import android.util.Pair;
@@ -106,6 +107,14 @@
public static final String EXTRA_SEND_TO_EXTERNAL_DEFAULT_HANDLER =
"android.support.customtabs.extra.SEND_TO_EXTERNAL_HANDLER";
+ /** The APK package to load the module from. */
+ private static final String EXTRA_MODULE_PACKAGE_NAME =
+ "org.chromium.chrome.browser.customtabs.EXTRA_MODULE_PACKAGE_NAME";
+
+ /** The class name of the module entry point. */
+ private static final String EXTRA_MODULE_CLASS_NAME =
+ "org.chromium.chrome.browser.customtabs.EXTRA_MODULE_CLASS_NAME";
+
private static final int MAX_CUSTOM_MENU_ITEMS = 5;
private static final int MAX_CUSTOM_TOOLBAR_ITEMS = 2;
@@ -122,6 +131,8 @@
private final int mInitialBackgroundColor;
private final boolean mDisableStar;
private final boolean mDisableDownload;
+ @Nullable private final String mModulePackageName;
+ @Nullable private final String mModuleClassName;
private int mToolbarColor;
private int mBottomBarColor;
@@ -232,6 +243,8 @@
mDisableStar = IntentUtils.safeGetBooleanExtra(intent, EXTRA_DISABLE_STAR_BUTTON, false);
mDisableDownload =
IntentUtils.safeGetBooleanExtra(intent, EXTRA_DISABLE_DOWNLOAD_BUTTON, false);
+ mModulePackageName = IntentUtils.safeGetStringExtra(intent, EXTRA_MODULE_PACKAGE_NAME);
+ mModuleClassName = IntentUtils.safeGetStringExtra(intent, EXTRA_MODULE_CLASS_NAME);
}
/**
@@ -594,4 +607,20 @@
// Only open custom tab in incognito mode for payment request.
return isTrustedIntent() && mIsOpenedByChrome && isForPaymentRequest() && mIsIncognito;
}
+
+ /**
+ * @return The APK package to load the module from, or null if not specified.
+ */
+ @Nullable
+ String getModulePackageName() {
+ return mModulePackageName;
+ }
+
+ /**
+ * @return The class name of the module entry point, or null if not specified.
+ */
+ @Nullable
+ String getModuleClassName() {
+ return mModuleClassName;
+ }
}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnection.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnection.java
index ba42faf..0c40540 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnection.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnection.java
@@ -23,6 +23,7 @@
import android.support.customtabs.CustomTabsService;
import android.support.customtabs.CustomTabsSessionToken;
import android.text.TextUtils;
+import android.util.Pair;
import android.widget.RemoteViews;
import org.json.JSONException;
@@ -52,6 +53,8 @@
import org.chromium.chrome.browser.browserservices.BrowserSessionContentUtils;
import org.chromium.chrome.browser.browserservices.Origin;
import org.chromium.chrome.browser.browserservices.PostMessageHandler;
+import org.chromium.chrome.browser.customtabs.dynamicmodule.ModuleEntryPoint;
+import org.chromium.chrome.browser.customtabs.dynamicmodule.ModuleLoader;
import org.chromium.chrome.browser.device.DeviceClassManager;
import org.chromium.chrome.browser.init.ChainedTasks;
import org.chromium.chrome.browser.init.ChromeBrowserInitializer;
@@ -230,6 +233,12 @@
private volatile ChainedTasks mWarmupTasks;
+ /** The module package name and class name. */
+ private Pair<String, String> mModuleNames;
+
+ /** The module entry point. */
+ private ModuleEntryPoint mModuleEntryPoint;
+
/**
* <strong>DO NOT CALL</strong>
* Public to be instanciable from {@link ChromeApplication}. This is however
@@ -1408,4 +1417,27 @@
private static native void nativeCreateAndStartDetachedResourceRequest(
Profile profile, String url, String origin, @WebReferrerPolicy int referrerPolicy);
+
+ /**
+ * Dynamically loads the class {@code className} from the application identified by
+ * {@code packageName} and wraps it in a {@link ModuleEntryPoint}.
+ * @param packageName The package name of the application to load form.
+ * @param className The fully-qualified name of the class to load.
+ * @return The loaded class, cast to an AIDL interface and wrapped in a more user friendly form.
+ */
+ @Nullable
+ public ModuleEntryPoint loadModule(String packageName, String className) {
+ if (mModuleEntryPoint != null && mModuleNames != null) {
+ if ((!mModuleNames.first.equals(packageName)
+ || !mModuleNames.second.equals(className))) {
+ throw new IllegalStateException("Only one module can be loaded at a time.");
+ }
+ return mModuleEntryPoint;
+ }
+
+ // TODO(https://crbug.com/853732): Add cleanup mechanism to unload the module.
+ mModuleNames = new Pair<>(packageName, className);
+ mModuleEntryPoint = ModuleLoader.loadModule(packageName, className);
+ return mModuleEntryPoint;
+ }
}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ActivityDelegate.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ActivityDelegate.java
new file mode 100644
index 0000000..5f3433f
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ActivityDelegate.java
@@ -0,0 +1,129 @@
+// Copyright 2018 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.dynamicmodule;
+
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.view.View;
+
+/**
+ * A wrapper around a {@link IActivityDelegate}.
+ *
+ * No {@link RemoteException} should ever be thrown as all of this code runs in the same process.
+ */
+public class ActivityDelegate {
+ private final IActivityDelegate mActivityDelegate;
+
+ public ActivityDelegate(IActivityDelegate activityDelegate) {
+ mActivityDelegate = activityDelegate;
+ }
+
+ public void onCreate(Bundle savedInstanceState) {
+ try {
+ mActivityDelegate.onCreate(savedInstanceState);
+ } catch (RemoteException e) {
+ assert false;
+ }
+ }
+
+ public void onPostCreate(Bundle savedInstanceState) {
+ try {
+ mActivityDelegate.onPostCreate(savedInstanceState);
+ } catch (RemoteException e) {
+ assert false;
+ }
+ }
+
+ public void onStart() {
+ try {
+ mActivityDelegate.onStart();
+ } catch (RemoteException e) {
+ assert false;
+ }
+ }
+
+ public void onStop(boolean isChangingConfigurations) {
+ try {
+ mActivityDelegate.onStop(isChangingConfigurations);
+ } catch (RemoteException e) {
+ assert false;
+ }
+ }
+
+ public void onWindowFocusChanged(boolean hasFocus) {
+ try {
+ mActivityDelegate.onWindowFocusChanged(hasFocus);
+ } catch (RemoteException e) {
+ assert false;
+ }
+ }
+
+ public void onSaveInstanceState(Bundle outState) {
+ try {
+ mActivityDelegate.onSaveInstanceState(outState);
+ } catch (RemoteException e) {
+ assert false;
+ }
+ }
+
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ try {
+ mActivityDelegate.onRestoreInstanceState(savedInstanceState);
+ } catch (RemoteException e) {
+ assert false;
+ }
+ }
+
+ public void onResume() {
+ try {
+ mActivityDelegate.onResume();
+ } catch (RemoteException e) {
+ assert false;
+ }
+ }
+
+ public void onPause(boolean isChangingConfigurations) {
+ try {
+ mActivityDelegate.onPause(isChangingConfigurations);
+ } catch (RemoteException e) {
+ assert false;
+ }
+ }
+
+ public void onDestroy(boolean isChangingConfigurations) {
+ try {
+ mActivityDelegate.onDestroy(isChangingConfigurations);
+ } catch (RemoteException e) {
+ assert false;
+ }
+ }
+
+ public boolean onBackPressed() {
+ try {
+ return mActivityDelegate.onBackPressed();
+ } catch (RemoteException e) {
+ assert false;
+ }
+ return false;
+ }
+
+ public View getBottomBarView() {
+ try {
+ return ObjectWrapper.unwrap(mActivityDelegate.getBottomBarView(), View.class);
+ } catch (RemoteException e) {
+ assert false;
+ }
+ return null;
+ }
+
+ public View getOverlayView() {
+ try {
+ return ObjectWrapper.unwrap(mActivityDelegate.getOverlayView(), View.class);
+ } catch (RemoteException e) {
+ assert false;
+ }
+ return null;
+ }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ActivityHostImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ActivityHostImpl.java
new file mode 100644
index 0000000..4e58178
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ActivityHostImpl.java
@@ -0,0 +1,23 @@
+// Copyright 2018 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.dynamicmodule;
+
+import android.content.Context;
+
+/**
+ * The implementation of {@link IActivityHost}.
+ */
+public class ActivityHostImpl extends IActivityHost.Stub {
+ private final Context mActivityContext;
+
+ public ActivityHostImpl(Context activityContext) {
+ mActivityContext = activityContext;
+ }
+
+ @Override
+ public IObjectWrapper getActivityContext() {
+ return ObjectWrapper.wrap(mActivityContext);
+ }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/IActivityDelegate.aidl b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/IActivityDelegate.aidl
new file mode 100644
index 0000000..0921ad7
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/IActivityDelegate.aidl
@@ -0,0 +1,35 @@
+// Copyright 2018 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.dynamicmodule;
+
+import org.chromium.chrome.browser.customtabs.dynamicmodule.IObjectWrapper;
+
+interface IActivityDelegate {
+ void onCreate(in Bundle savedInstanceState) = 0;
+
+ void onPostCreate(in Bundle savedInstanceState) = 1;
+
+ void onStart() = 2;
+
+ void onStop(boolean isChangingConfigurations) = 3;
+
+ void onWindowFocusChanged(boolean hasFocus) = 4;
+
+ void onSaveInstanceState(in Bundle outState) = 5;
+
+ void onRestoreInstanceState(in Bundle savedInstanceState) = 6;
+
+ void onResume() = 7;
+
+ void onPause(boolean isChangingConfigurations) = 8;
+
+ void onDestroy(boolean isChangingConfigurations) = 9;
+
+ boolean onBackPressed() = 10;
+
+ IObjectWrapper /* View */ getBottomBarView() = 11;
+
+ IObjectWrapper /* View */ getOverlayView() = 12;
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/IActivityHost.aidl b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/IActivityHost.aidl
new file mode 100644
index 0000000..ca84c93
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/IActivityHost.aidl
@@ -0,0 +1,11 @@
+// Copyright 2018 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.dynamicmodule;
+
+import org.chromium.chrome.browser.customtabs.dynamicmodule.IObjectWrapper;
+
+interface IActivityHost {
+ IObjectWrapper /* Context */ getActivityContext() = 0;
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/IModuleEntryPoint.aidl b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/IModuleEntryPoint.aidl
new file mode 100644
index 0000000..79516d37a
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/IModuleEntryPoint.aidl
@@ -0,0 +1,16 @@
+// Copyright 2018 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.dynamicmodule;
+
+import org.chromium.chrome.browser.customtabs.dynamicmodule.IActivityDelegate;
+import org.chromium.chrome.browser.customtabs.dynamicmodule.IActivityHost;
+import org.chromium.chrome.browser.customtabs.dynamicmodule.IModuleHost;
+
+interface IModuleEntryPoint {
+ void init(in IModuleHost moduleHost) = 0;
+ IActivityDelegate createActivityDelegate(in IActivityHost activityHost) = 1;
+ int getVersion() = 2;
+ int getMinimumHostVersion() = 3;
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/IModuleHost.aidl b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/IModuleHost.aidl
new file mode 100644
index 0000000..5c7afd09
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/IModuleHost.aidl
@@ -0,0 +1,14 @@
+// Copyright 2018 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.dynamicmodule;
+
+import org.chromium.chrome.browser.customtabs.dynamicmodule.IObjectWrapper;
+
+interface IModuleHost {
+ IObjectWrapper /* Context */ getHostApplicationContext() = 0;
+ IObjectWrapper /* Context */ getModuleContext() = 1;
+ int getVersion() = 2;
+ int getMinimumModuleVersion() = 3;
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/IObjectWrapper.aidl b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/IObjectWrapper.aidl
new file mode 100644
index 0000000..fc904ff
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/IObjectWrapper.aidl
@@ -0,0 +1,14 @@
+// Copyright 2018 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.dynamicmodule;
+
+/**
+ * This interface intentionally has no methods, and instances of this should
+ * be created from class ObjectWrapper only. This is used as a way of passing
+ * objects that descend from the system classes via AIDL across classloaders
+ * without serializing them.
+ */
+interface IObjectWrapper {
+}
\ No newline at end of file
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ModuleEntryPoint.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ModuleEntryPoint.java
new file mode 100644
index 0000000..d3041af0
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ModuleEntryPoint.java
@@ -0,0 +1,55 @@
+// Copyright 2018 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.dynamicmodule;
+
+import android.os.RemoteException;
+
+/**
+ * A wrapper around a {@link IModuleEntryPoint}.
+ *
+ * No {@link RemoteException} should ever be thrown as all of this code runs in the same process.
+ */
+public class ModuleEntryPoint {
+ private final IModuleEntryPoint mEntryPoint;
+
+ public ModuleEntryPoint(IModuleEntryPoint entryPoint) {
+ mEntryPoint = entryPoint;
+ }
+
+ public void init(ModuleHostImpl moduleHost) {
+ try {
+ mEntryPoint.init(moduleHost);
+ } catch (RemoteException e) {
+ assert false;
+ }
+ }
+
+ public ActivityDelegate createActivityDelegate(ActivityHostImpl activityHost) {
+ try {
+ return new ActivityDelegate(mEntryPoint.createActivityDelegate(activityHost));
+ } catch (RemoteException e) {
+ assert false;
+ }
+ return null;
+ }
+
+ public int getVersion() {
+ try {
+ return mEntryPoint.getVersion();
+ } catch (RemoteException e) {
+ assert false;
+ }
+ return -1;
+ }
+
+ public int getMinimumHostVersion() {
+ try {
+ return mEntryPoint.getMinimumHostVersion();
+ } catch (RemoteException e) {
+ assert false;
+ }
+ return -1;
+ }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ModuleHostImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ModuleHostImpl.java
new file mode 100644
index 0000000..5bfdb5b4d
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ModuleHostImpl.java
@@ -0,0 +1,43 @@
+// Copyright 2018 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.dynamicmodule;
+
+import android.content.Context;
+
+/**
+ * The implementation of {@link IModuleHost}.
+ */
+public class ModuleHostImpl extends IModuleHost.Stub {
+ private static final int VERSION = 1;
+ private static final int MINIMUM_MODULE_VERSION = 1;
+
+ private final Context mApplicationContext;
+ private final Context mModuleContext;
+
+ public ModuleHostImpl(Context applicationContext, Context moduleContext) {
+ mApplicationContext = applicationContext;
+ mModuleContext = moduleContext;
+ }
+
+ @Override
+ public IObjectWrapper getHostApplicationContext() {
+ return ObjectWrapper.wrap(mApplicationContext);
+ }
+
+ @Override
+ public IObjectWrapper getModuleContext() {
+ return ObjectWrapper.wrap(mModuleContext);
+ }
+
+ @Override
+ public int getVersion() {
+ return VERSION;
+ }
+
+ @Override
+ public int getMinimumModuleVersion() {
+ return MINIMUM_MODULE_VERSION;
+ }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ModuleLoader.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ModuleLoader.java
new file mode 100644
index 0000000..07db661
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ModuleLoader.java
@@ -0,0 +1,83 @@
+// Copyright 2018 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.dynamicmodule;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.IBinder;
+import android.support.annotation.Nullable;
+
+import org.chromium.base.ContextUtils;
+import org.chromium.base.Log;
+
+/**
+ * Dynamically loads a module from another apk.
+ */
+public final class ModuleLoader {
+ private static final String TAG = "ModuleLoader";
+
+ private ModuleLoader() {}
+
+ /**
+ * Dynamically loads the class {@code className} from the application identified by
+ * {@code packageName} and wraps it in a {@link ModuleEntryPoint}.
+ * @param packageName The package name of the application to load form.
+ * @param className The name of the class to load, in the form of a binary name (including the
+ * class package name).
+ * @return The loaded class, cast to an AIDL interface and wrapped in a more user friendly form.
+ */
+ @Nullable
+ public static ModuleEntryPoint loadModule(String packageName, String className) {
+ Context applicationContext = ContextUtils.getApplicationContext();
+ Context moduleContext = getModuleContext(applicationContext, packageName);
+
+ if (moduleContext == null) return null;
+
+ try {
+ Class<?> clazz = moduleContext.getClassLoader().loadClass(className);
+
+ IBinder binder = (IBinder) clazz.newInstance();
+
+ ModuleHostImpl moduleHost = new ModuleHostImpl(applicationContext, moduleContext);
+ ModuleEntryPoint entryPoint =
+ new ModuleEntryPoint(IModuleEntryPoint.Stub.asInterface(binder));
+
+ if (!isCompatible(moduleHost, entryPoint)) {
+ Log.w(TAG, "Incompatible module due to version mismatch: host version %s, "
+ + "minimum required host version %s, entry point version %s, minimum "
+ + "required entry point version %s.",
+ moduleHost.getVersion(), entryPoint.getMinimumHostVersion(),
+ entryPoint.getVersion(), moduleHost.getMinimumModuleVersion());
+ return null;
+ }
+
+ entryPoint.init(moduleHost);
+ return entryPoint;
+ } catch (Exception e) { // No multi-catch below API level 19 for reflection exceptions.
+ Log.e(TAG, "Could not create entry point using package name %s and class name %s",
+ packageName, className, e);
+ }
+ return null;
+ }
+
+ @Nullable
+ private static Context getModuleContext(Context applicationContext, String packageName) {
+ try {
+ // The flags Context.CONTEXT_INCLUDE_CODE and Context.CONTEXT_IGNORE_SECURITY are
+ // needed to be able to load classes via the classloader of the returned context.
+ Context moduleContext = applicationContext.createPackageContext(
+ packageName, Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
+ return moduleContext;
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Could not create package context for %s", packageName, e);
+ }
+ return null;
+ }
+
+ private static boolean isCompatible(ModuleHostImpl moduleHost, ModuleEntryPoint entryPoint) {
+ return entryPoint.getVersion() >= moduleHost.getMinimumModuleVersion()
+ && moduleHost.getVersion() >= entryPoint.getMinimumHostVersion();
+ }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/OWNERS b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/OWNERS
new file mode 100644
index 0000000..8f094e0
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/OWNERS
@@ -0,0 +1,2 @@
+per-file *.aidl=set noparent
+per-file *.aidl=file://ipc/SECURITY_OWNERS
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ObjectWrapper.java b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ObjectWrapper.java
new file mode 100644
index 0000000..7f7b4b2f6
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ObjectWrapper.java
@@ -0,0 +1,104 @@
+// Copyright 2018 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.dynamicmodule;
+
+import android.os.IBinder;
+import android.support.annotation.Nullable;
+
+import java.lang.reflect.Field;
+
+/**
+ * This wraps an object to be transferred across sibling classloaders in the same process via the
+ * IObjectWrapper AIDL interface. By using reflection to retrieve the object, no serialization needs
+ * to occur.
+ *
+ * @param <T> The type of the wrapped object.
+ */
+public final class ObjectWrapper<T> extends IObjectWrapper.Stub {
+ /**
+ * The wrapped object. You must not add another member in this class because the check for
+ * retrieving this member variable is that this is the ONLY member variable declared in this
+ * class and it is private. This is because ObjectWrapper can be obfuscated, so that this member
+ * variable can have an obfuscated name.
+ */
+ private final T mWrappedObject;
+
+ /* DO NOT ADD NEW CLASS MEMBERS (see above) */
+
+ /** Disable creating an object wrapper. Instead, use {@link #wrap(Object)}. */
+ private ObjectWrapper(T object) {
+ mWrappedObject = object;
+ }
+
+ /**
+ * Create the wrapped object.
+ *
+ * @param object The object instance to wrap.
+ * @return The wrapped object.
+ */
+ public static <T> IObjectWrapper wrap(T object) {
+ return new ObjectWrapper<T>(object);
+ }
+
+ /**
+ * Unwrap the object within the {@link IObjectWrapper} using reflection.
+ *
+ * @param remote The {@link IObjectWrapper} instance to unwrap.
+ * @param clazz The {@link Class} of the unwrapped object type.
+ * @return The unwrapped object.
+ */
+ @Nullable
+ public static <T> T unwrap(@Nullable IObjectWrapper remote, Class<T> clazz) {
+ if (remote == null) return null;
+
+ // Handle the case when not getting an IObjectWrapper from a sibling classloader
+ if (remote instanceof ObjectWrapper) {
+ @SuppressWarnings("unchecked")
+ ObjectWrapper<T> typedRemote = ((ObjectWrapper<T>) remote);
+ return typedRemote.mWrappedObject;
+ }
+
+ IBinder remoteBinder = remote.asBinder();
+
+ // It is possible that ObjectWrapper was obfuscated in which case wrappedObject
+ // would have a different name. The following checks that there is a single
+ // declared field that is private.
+ Class<?> remoteClazz = remoteBinder.getClass();
+
+ Field validField = null;
+ for (Field field : remoteClazz.getDeclaredFields()) {
+ if (field.isSynthetic()) continue;
+
+ // Only one valid, non-synthetic field is allowed on the class.
+ if (validField != null) {
+ validField = null;
+ break;
+ }
+ validField = field;
+ }
+
+ if (validField == null || validField.isAccessible()) {
+ throw new IllegalArgumentException("The concrete class implementing IObjectWrapper"
+ + " must have exactly *one* declared *private* field for the wrapped object. "
+ + " Preferably, this is an instance of the ObjectWrapper<T> class.");
+ }
+
+ validField.setAccessible(true);
+ try {
+ Object wrappedObject = validField.get(remoteBinder);
+ if (wrappedObject == null) return null;
+ if (!clazz.isInstance(wrappedObject)) {
+ throw new IllegalArgumentException("remoteBinder is the wrong class.");
+ }
+ return clazz.cast(wrappedObject);
+ } catch (NullPointerException e) {
+ throw new IllegalArgumentException("Binder object is null.", e);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("remoteBinder is the wrong class.", e);
+ } catch (IllegalAccessException e) {
+ throw new IllegalArgumentException("Could not access the field in remoteBinder.", e);
+ }
+ }
+}
diff --git a/chrome/android/java_sources.gni b/chrome/android/java_sources.gni
index 4695bb4..a9458da 100644
--- a/chrome/android/java_sources.gni
+++ b/chrome/android/java_sources.gni
@@ -364,8 +364,8 @@
"java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnection.java",
"java/src/org/chromium/chrome/browser/customtabs/CustomTabsConnectionService.java",
"java/src/org/chromium/chrome/browser/customtabs/CustomTabTabPersistencePolicy.java",
- "java/src/org/chromium/chrome/browser/customtabs/RequestThrottler.java",
"java/src/org/chromium/chrome/browser/customtabs/NavigationInfoCaptureTrigger.java",
+ "java/src/org/chromium/chrome/browser/customtabs/RequestThrottler.java",
"java/src/org/chromium/chrome/browser/customtabs/SeparateTaskCustomTabActivity.java",
"java/src/org/chromium/chrome/browser/customtabs/SeparateTaskCustomTabActivity0.java",
"java/src/org/chromium/chrome/browser/customtabs/SeparateTaskCustomTabActivity1.java",
@@ -378,6 +378,12 @@
"java/src/org/chromium/chrome/browser/customtabs/SeparateTaskCustomTabActivity8.java",
"java/src/org/chromium/chrome/browser/customtabs/SeparateTaskCustomTabActivity9.java",
"java/src/org/chromium/chrome/browser/customtabs/SeparateTaskManagedCustomTabActivity.java",
+ "java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ActivityDelegate.java",
+ "java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ActivityHostImpl.java",
+ "java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ModuleEntryPoint.java",
+ "java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ModuleHostImpl.java",
+ "java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ModuleLoader.java",
+ "java/src/org/chromium/chrome/browser/customtabs/dynamicmodule/ObjectWrapper.java",
"java/src/org/chromium/chrome/browser/database/SQLiteCursor.java",
"java/src/org/chromium/chrome/browser/datausage/DataUseTabUIManager.java",
"java/src/org/chromium/chrome/browser/datausage/ExternalDataUseObserver.java",
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index 25de418..365c1ea13 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -3934,6 +3934,12 @@
kOsAndroid, FEATURE_VALUE_TYPE(media::kMediaControlsExpandGesture)},
#endif
+#if defined(OS_ANDROID)
+ {"cct-module", flag_descriptions::kCCTModuleName,
+ flag_descriptions::kCCTModuleDescription, kOsAndroid,
+ FEATURE_VALUE_TYPE(chrome::android::kCCTModule)},
+#endif
+
// NOTE: Adding a new flag requires adding a corresponding entry to enum
// "LoginCustomFlags" in tools/metrics/histograms/enums.xml. See "Flag
// Histograms" in tools/metrics/histograms/README.md (run the
diff --git a/chrome/browser/android/chrome_feature_list.cc b/chrome/browser/android/chrome_feature_list.cc
index 2864bbed..f14aebc6 100644
--- a/chrome/browser/android/chrome_feature_list.cc
+++ b/chrome/browser/android/chrome_feature_list.cc
@@ -72,6 +72,7 @@
&kAndroidPaymentApps,
&kCCTBackgroundTab,
&kCCTExternalLinkHandling,
+ &kCCTModule,
&kCCTParallelRequest,
&kCCTPostMessageAPI,
&kCCTRedirectPreconnect,
@@ -189,6 +190,8 @@
const base::Feature kCCTExternalLinkHandling{"CCTExternalLinkHandling",
base::FEATURE_ENABLED_BY_DEFAULT};
+const base::Feature kCCTModule{"CCTModule", base::FEATURE_DISABLED_BY_DEFAULT};
+
const base::Feature kCCTParallelRequest{"CCTParallelRequest",
base::FEATURE_ENABLED_BY_DEFAULT};
diff --git a/chrome/browser/android/chrome_feature_list.h b/chrome/browser/android/chrome_feature_list.h
index 5c55385..756c0fd 100644
--- a/chrome/browser/android/chrome_feature_list.h
+++ b/chrome/browser/android/chrome_feature_list.h
@@ -19,6 +19,7 @@
extern const base::Feature kAndroidPaymentApps;
extern const base::Feature kCCTBackgroundTab;
extern const base::Feature kCCTExternalLinkHandling;
+extern const base::Feature kCCTModule;
extern const base::Feature kCCTParallelRequest;
extern const base::Feature kCCTPostMessageAPI;
extern const base::Feature kCCTRedirectPreconnect;
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index d27116ab..171a128 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -2008,6 +2008,10 @@
"Enables downloading pages in the background in case page is not yet "
"loaded in current tab.";
+const char kCCTModuleName[] = "Chrome Custom Tabs Module";
+const char kCCTModuleDescription[] =
+ "Enables a dynamically loaded module in Chrome Custom Tabs, on Android.";
+
const char kChromeDuplexName[] = "Chrome Duplex";
const char kChromeDuplexDescription[] =
"Enables Chrome Duplex, split toolbar Chrome Home, on Android.";
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index ee25e5b4..20174d6 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -1222,6 +1222,9 @@
extern const char kBackgroundLoaderForDownloadsName[];
extern const char kBackgroundLoaderForDownloadsDescription[];
+extern const char kCCTModuleName[];
+extern const char kCCTModuleDescription[];
+
extern const char kChromeDuplexName[];
extern const char kChromeDuplexDescription[];