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[];