[Android] Enable multidex for release builds of chrome_public_test_apk. (RELAND)

BUG=712852

Review-Url: https://codereview.chromium.org/2839983002
Cr-Commit-Position: refs/heads/master@{#467462}
diff --git a/DEPS b/DEPS
index b811f3ff..be08e7a4 100644
--- a/DEPS
+++ b/DEPS
@@ -466,7 +466,7 @@
       Var('chromium_git') + '/external/android_protobuf.git' + '@' + '999188d0dc72e97f7fe08bb756958a2cf090f4e7',
 
     'src/third_party/android_tools':
-      Var('chromium_git') + '/android_tools.git' + '@' + 'b65c4776dac2cf1b80e969b3b2d4e081b9c84f29',
+      Var('chromium_git') + '/android_tools.git' + '@' + 'cb6bc21107001e2f2eeee2707b482b2b755baf51',
 
     'src/third_party/apache-portable-runtime/src':
       Var('chromium_git') + '/external/apache-portable-runtime.git' + '@' + 'c76a8c4277e09a82eaa229e35246edea1ee0a6a1',
diff --git a/base/BUILD.gn b/base/BUILD.gn
index b284bec..43897be 100644
--- a/base/BUILD.gn
+++ b/base/BUILD.gn
@@ -2585,6 +2585,7 @@
       "//testing/android/reporter:reporter_java",
       "//third_party/android_support_test_runner:exposed_instrumentation_api_publish_java",
       "//third_party/android_support_test_runner:runner_java",
+      "//third_party/android_tools:android_support_chromium_java",
       "//third_party/hamcrest:hamcrest_core_java",
       "//third_party/junit",
     ]
@@ -2594,6 +2595,7 @@
       "test/android/javatests/src/org/chromium/base/test/BaseInstrumentationTestRunner.java",
       "test/android/javatests/src/org/chromium/base/test/BaseJUnit4ClassRunner.java",
       "test/android/javatests/src/org/chromium/base/test/BaseChromiumAndroidJUnitRunner.java",
+      "test/android/javatests/src/org/chromium/base/test/BaseChromiumRunnerCommon.java",
       "test/android/javatests/src/org/chromium/base/test/BaseTestResult.java",
       "test/android/javatests/src/org/chromium/base/test/SetUpTestRule.java",
       "test/android/javatests/src/org/chromium/base/test/SetUpStatement.java",
diff --git a/base/android/java/src/org/chromium/base/BaseChromiumApplication.java b/base/android/java/src/org/chromium/base/BaseChromiumApplication.java
index 6b97bdd..61109f1 100644
--- a/base/android/java/src/org/chromium/base/BaseChromiumApplication.java
+++ b/base/android/java/src/org/chromium/base/BaseChromiumApplication.java
@@ -44,7 +44,9 @@
         super.attachBaseContext(base);
         assert getBaseContext() != null;
         checkAppBeingReplaced();
-        ChromiumMultiDexInstaller.install(this);
+        if (BuildConfig.isMultidexEnabled()) {
+            ChromiumMultiDexInstaller.install(this);
+        }
     }
 
     /**
diff --git a/base/android/java/src/org/chromium/base/multidex/ChromiumMultiDexInstaller.java b/base/android/java/src/org/chromium/base/multidex/ChromiumMultiDexInstaller.java
index 3d036c2..9049cb0 100644
--- a/base/android/java/src/org/chromium/base/multidex/ChromiumMultiDexInstaller.java
+++ b/base/android/java/src/org/chromium/base/multidex/ChromiumMultiDexInstaller.java
@@ -12,7 +12,6 @@
 import android.os.Build;
 import android.support.multidex.MultiDex;
 
-import org.chromium.base.BuildConfig;
 import org.chromium.base.Log;
 import org.chromium.base.VisibleForTesting;
 
@@ -48,8 +47,6 @@
      */
     @VisibleForTesting
     public static void install(Context context) {
-        if (!BuildConfig.isMultidexEnabled()) return;
-
         // TODO(jbudorick): Back out this version check once support for K & below works.
         // http://crbug.com/512357
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP
diff --git a/base/test/android/javatests/src/org/chromium/base/test/BaseChromiumAndroidJUnitRunner.java b/base/test/android/javatests/src/org/chromium/base/test/BaseChromiumAndroidJUnitRunner.java
index fcd60869..a48c4ab 100644
--- a/base/test/android/javatests/src/org/chromium/base/test/BaseChromiumAndroidJUnitRunner.java
+++ b/base/test/android/javatests/src/org/chromium/base/test/BaseChromiumAndroidJUnitRunner.java
@@ -4,7 +4,8 @@
 
 package org.chromium.base.test;
 
-import android.os.Bundle;
+import android.app.Application;
+import android.content.Context;
 import android.support.test.runner.AndroidJUnitRunner;
 
 import org.chromium.base.multidex.ChromiumMultiDexInstaller;
@@ -18,8 +19,11 @@
  */
 public class BaseChromiumAndroidJUnitRunner extends AndroidJUnitRunner {
     @Override
-    public void onCreate(Bundle arguments) {
-        ChromiumMultiDexInstaller.install(getTargetContext());
-        super.onCreate(arguments);
+    public Application newApplication(ClassLoader cl, String className, Context context)
+            throws ClassNotFoundException, IllegalAccessException, InstantiationException {
+        ChromiumMultiDexInstaller.install(new BaseChromiumRunnerCommon.MultiDexContextWrapper(
+                getContext(), getTargetContext()));
+        BaseChromiumRunnerCommon.reorderDexPathElements(cl, getContext(), getTargetContext());
+        return super.newApplication(cl, className, context);
     }
 }
diff --git a/base/test/android/javatests/src/org/chromium/base/test/BaseChromiumInstrumentationTestRunner.java b/base/test/android/javatests/src/org/chromium/base/test/BaseChromiumInstrumentationTestRunner.java
index d44c9dd..693bb62 100644
--- a/base/test/android/javatests/src/org/chromium/base/test/BaseChromiumInstrumentationTestRunner.java
+++ b/base/test/android/javatests/src/org/chromium/base/test/BaseChromiumInstrumentationTestRunner.java
@@ -4,7 +4,8 @@
 
 package org.chromium.base.test;
 
-import android.os.Bundle;
+import android.app.Application;
+import android.content.Context;
 
 import org.chromium.base.multidex.ChromiumMultiDexInstaller;
 import org.chromium.base.test.util.CommandLineFlags;
@@ -16,9 +17,12 @@
  */
 public class BaseChromiumInstrumentationTestRunner extends BaseInstrumentationTestRunner {
     @Override
-    public void onCreate(Bundle arguments) {
-        ChromiumMultiDexInstaller.install(getTargetContext());
-        super.onCreate(arguments);
+    public Application newApplication(ClassLoader cl, String className, Context context)
+            throws ClassNotFoundException, IllegalAccessException, InstantiationException {
+        ChromiumMultiDexInstaller.install(new BaseChromiumRunnerCommon.MultiDexContextWrapper(
+                getContext(), getTargetContext()));
+        BaseChromiumRunnerCommon.reorderDexPathElements(cl, getContext(), getTargetContext());
+        return super.newApplication(cl, className, context);
     }
 
     /**
diff --git a/base/test/android/javatests/src/org/chromium/base/test/BaseChromiumRunnerCommon.java b/base/test/android/javatests/src/org/chromium/base/test/BaseChromiumRunnerCommon.java
new file mode 100644
index 0000000..fcda9103
--- /dev/null
+++ b/base/test/android/javatests/src/org/chromium/base/test/BaseChromiumRunnerCommon.java
@@ -0,0 +1,162 @@
+// Copyright 2017 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.base.test;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+
+import org.chromium.android.support.PackageManagerWrapper;
+import org.chromium.base.Log;
+import org.chromium.base.annotations.MainDex;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Serializable;
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Comparator;
+
+/**
+ *  Functionality common to the JUnit3 and JUnit4 runners.
+ */
+@MainDex
+class BaseChromiumRunnerCommon {
+    private static final String TAG = "base_test";
+
+    /**
+     *  A ContextWrapper that allows multidex test APKs to extract secondary dexes into
+     *  the APK under test's data directory.
+     */
+    @MainDex
+    static class MultiDexContextWrapper extends ContextWrapper {
+        private Context mAppContext;
+
+        MultiDexContextWrapper(Context instrContext, Context appContext) {
+            super(instrContext);
+            mAppContext = appContext;
+        }
+
+        @Override
+        public File getFilesDir() {
+            return mAppContext.getFilesDir();
+        }
+
+        @Override
+        public SharedPreferences getSharedPreferences(String name, int mode) {
+            return mAppContext.getSharedPreferences(name, mode);
+        }
+
+        @Override
+        public PackageManager getPackageManager() {
+            return new PackageManagerWrapper(super.getPackageManager()) {
+                @Override
+                public ApplicationInfo getApplicationInfo(String packageName, int flags) {
+                    try {
+                        ApplicationInfo ai = super.getApplicationInfo(packageName, flags);
+                        if (packageName.equals(getPackageName())) {
+                            ApplicationInfo appAi =
+                                    super.getApplicationInfo(mAppContext.getPackageName(), flags);
+                            File dataDir = new File(appAi.dataDir, "test-multidex");
+                            if (!dataDir.exists() && !dataDir.mkdirs()) {
+                                throw new IOException(String.format(
+                                        "Unable to create test multidex directory \"%s\"",
+                                        dataDir.getPath()));
+                            }
+                            ai.dataDir = dataDir.getPath();
+                        }
+                        return ai;
+                    } catch (Exception e) {
+                        Log.e(TAG, "Failed to get application info for %s", packageName, e);
+                    }
+                    return null;
+                }
+            };
+        }
+    }
+
+    /**
+     * Ensure all test dex entries precede app dex entries.
+     *
+     * @param cl ClassLoader to modify. Assumed to be a derivative of
+     *        {@link dalvik.system.BaseDexClassLoader}. If this isn't
+     *        the case, reordering will fail.
+     */
+    static void reorderDexPathElements(ClassLoader cl, Context context, Context targetContext) {
+        try {
+            Log.i(TAG,
+                    "Reordering dex files. If you're building a multidex test APK and see a "
+                            + "class resolving to an unexpected implementation, this may be why.");
+            Field pathListField = findField(cl, "pathList");
+            Object dexPathList = pathListField.get(cl);
+            Field dexElementsField = findField(dexPathList, "dexElements");
+            Object[] dexElementsList = (Object[]) dexElementsField.get(dexPathList);
+            Arrays.sort(dexElementsList,
+                    new DexListReorderingComparator(
+                            context.getPackageName(), targetContext.getPackageName()));
+            dexElementsField.set(dexPathList, dexElementsList);
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to reorder dex elements for testing.", e);
+        }
+    }
+
+    /**
+     *  Comparator for sorting dex list entries.
+     *
+     *  Using this to sort a list of dex list entries will result in the following order:
+     *   - Strings that contain neither the test package nor the app package in lexicographical
+     *     order.
+     *   - Strings that contain the test package in lexicographical order.
+     *   - Strings that contain the app package but not the test package in lexicographical order.
+     */
+    private static class DexListReorderingComparator implements Comparator<Object>, Serializable {
+        private String mTestPackage;
+        private String mAppPackage;
+
+        public DexListReorderingComparator(String testPackage, String appPackage) {
+            mTestPackage = testPackage;
+            mAppPackage = appPackage;
+        }
+
+        @Override
+        public int compare(Object o1, Object o2) {
+            String s1 = o1.toString();
+            String s2 = o2.toString();
+            if (s1.contains(mTestPackage)) {
+                if (!s2.contains(mTestPackage)) {
+                    if (s2.contains(mAppPackage)) {
+                        return -1;
+                    } else {
+                        return 1;
+                    }
+                }
+            } else if (s1.contains(mAppPackage)) {
+                if (s2.contains(mTestPackage)) {
+                    return 1;
+                } else if (!s2.contains(mAppPackage)) {
+                    return 1;
+                }
+            } else if (s2.contains(mTestPackage) || s2.contains(mAppPackage)) {
+                return -1;
+            }
+            return s1.compareTo(s2);
+        }
+    }
+
+    private static Field findField(Object instance, String name) throws NoSuchFieldException {
+        for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
+            try {
+                Field f = clazz.getDeclaredField(name);
+                f.setAccessible(true);
+                return f;
+            } catch (NoSuchFieldException e) {
+            }
+        }
+        throw new NoSuchFieldException(
+                "Unable to find field " + name + " in " + instance.getClass());
+    }
+}
diff --git a/build/android/main_dex_classes.flags b/build/android/main_dex_classes.flags
index 81152dc..9bb7977a 100644
--- a/build/android/main_dex_classes.flags
+++ b/build/android/main_dex_classes.flags
@@ -1,3 +1,10 @@
+# Copyright 2017 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.
+
+# Proguard flags for what should be kept in the main dex. Only used
+# during main dex list determination, not during actual proguarding.
+
 -keep @**.MainDex class * {
   *;
 }
diff --git a/build/android/multidex.flags b/build/android/multidex.flags
new file mode 100644
index 0000000..59e7e85
--- /dev/null
+++ b/build/android/multidex.flags
@@ -0,0 +1,12 @@
+# Copyright 2017 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.
+
+# Proguard flags for what to keep through proguarding when multidex is
+# enabled. Not used during main dex list determination.
+
+-keepattributes *Annotations*
+-keep @interface org.chromium.base.annotations.MainDex
+-keep @**.MainDex class * {
+  *;
+}
diff --git a/build/config/android/rules.gni b/build/config/android/rules.gni
index 4455732..eddbbf3 100644
--- a/build/config/android/rules.gni
+++ b/build/config/android/rules.gni
@@ -1935,6 +1935,9 @@
       if (defined(invoker.proguard_configs)) {
         _proguard_configs += invoker.proguard_configs
       }
+      if (enable_multidex) {
+        _proguard_configs += [ "//build/android/multidex.flags" ]
+      }
       assert(_proguard_configs != [])  # Mark as used.
       _proguard_target = "${_template_name}__proguard"
       proguard(_proguard_target) {
diff --git a/build/secondary/third_party/android_tools/BUILD.gn b/build/secondary/third_party/android_tools/BUILD.gn
index 853b040..3161db12 100644
--- a/build/secondary/third_party/android_tools/BUILD.gn
+++ b/build/secondary/third_party/android_tools/BUILD.gn
@@ -193,6 +193,11 @@
   aar_path = "$lib_path/$_lib_name/$lib_version/$_lib_name-$lib_version.aar"
 }
 
+android_library("android_support_chromium_java") {
+  testonly = true
+  java_files = [ "$android_sdk_root/extras/chromium/support/src/org/chromium/android/support/PackageManagerWrapper.java" ]
+}
+
 # TODO(dgn): Remove this once no other target has a dependency on it
 java_group("google_play_services_default_resources") {
   deps = []
diff --git a/chrome/android/BUILD.gn b/chrome/android/BUILD.gn
index 7f3c01c..d281862 100644
--- a/chrome/android/BUILD.gn
+++ b/chrome/android/BUILD.gn
@@ -884,6 +884,11 @@
     "//net/android:net_test_support_apk",
   ]
   proguard_enabled = !is_java_debug
+
+  # The test APK contains code from both the APK under test and the
+  # test APK when proguard is enabled. That causes this APK to exceed
+  # the dex limit.
+  enable_multidex = proguard_enabled
 }
 
 if (enable_vr) {