summaryrefslogtreecommitdiffstats
path: root/third_party/libwebrtc/build/android/incremental_install
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/libwebrtc/build/android/incremental_install')
-rw-r--r--third_party/libwebrtc/build/android/incremental_install/BUILD.gn23
-rw-r--r--third_party/libwebrtc/build/android/incremental_install/README.md83
-rw-r--r--third_party/libwebrtc/build/android/incremental_install/__init__.py3
-rwxr-xr-xthird_party/libwebrtc/build/android/incremental_install/generate_android_manifest.py141
-rw-r--r--third_party/libwebrtc/build/android/incremental_install/generate_android_manifest.pydeps32
-rwxr-xr-xthird_party/libwebrtc/build/android/incremental_install/installer.py372
-rw-r--r--third_party/libwebrtc/build/android/incremental_install/java/org/chromium/incrementalinstall/BootstrapApplication.java297
-rw-r--r--third_party/libwebrtc/build/android/incremental_install/java/org/chromium/incrementalinstall/BootstrapInstrumentation.java25
-rw-r--r--third_party/libwebrtc/build/android/incremental_install/java/org/chromium/incrementalinstall/ClassLoaderPatcher.java312
-rw-r--r--third_party/libwebrtc/build/android/incremental_install/java/org/chromium/incrementalinstall/LockFile.java129
-rw-r--r--third_party/libwebrtc/build/android/incremental_install/java/org/chromium/incrementalinstall/Reflect.java142
-rw-r--r--third_party/libwebrtc/build/android/incremental_install/java/org/chromium/incrementalinstall/SecondInstrumentation.java12
-rwxr-xr-xthird_party/libwebrtc/build/android/incremental_install/write_installer_json.py68
-rw-r--r--third_party/libwebrtc/build/android/incremental_install/write_installer_json.pydeps6
14 files changed, 1645 insertions, 0 deletions
diff --git a/third_party/libwebrtc/build/android/incremental_install/BUILD.gn b/third_party/libwebrtc/build/android/incremental_install/BUILD.gn
new file mode 100644
index 0000000000..8d26e9622b
--- /dev/null
+++ b/third_party/libwebrtc/build/android/incremental_install/BUILD.gn
@@ -0,0 +1,23 @@
+# Copyright 2015 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.
+
+import("//build/config/android/rules.gni")
+
+android_library("bootstrap_java") {
+ sources = [
+ "java/org/chromium/incrementalinstall/BootstrapApplication.java",
+ "java/org/chromium/incrementalinstall/BootstrapInstrumentation.java",
+ "java/org/chromium/incrementalinstall/ClassLoaderPatcher.java",
+ "java/org/chromium/incrementalinstall/LockFile.java",
+ "java/org/chromium/incrementalinstall/Reflect.java",
+ "java/org/chromium/incrementalinstall/SecondInstrumentation.java",
+ ]
+ jacoco_never_instrument = true
+ no_build_hooks = true
+}
+
+dist_dex("apk_dex") {
+ output = "$target_out_dir/apk.dex"
+ deps = [ ":bootstrap_java" ]
+}
diff --git a/third_party/libwebrtc/build/android/incremental_install/README.md b/third_party/libwebrtc/build/android/incremental_install/README.md
new file mode 100644
index 0000000000..9a27b8c5a6
--- /dev/null
+++ b/third_party/libwebrtc/build/android/incremental_install/README.md
@@ -0,0 +1,83 @@
+# Incremental Install
+
+Incremental Install is a way of building & deploying an APK that tries to
+minimize the time it takes to make a change and see that change running on
+device. They work best with `is_component_build=true`, and do *not* require a
+rooted device.
+
+## Building
+
+Add the gn arg:
+
+ incremental_install = true
+
+This causes all apks to be built as incremental except for denylisted ones.
+
+## Running
+
+It is not enough to `adb install` them. You must use the generated wrapper
+script:
+
+ out/Debug/bin/your_apk run
+ out/Debug/bin/run_chrome_public_test_apk # Automatically sets --fast-local-dev
+
+# How it Works
+
+## Overview
+
+The basic idea is to sideload .dex and .so files to `/data/local/tmp` rather
+than bundling them in the .apk. Then, when making a change, only the changed
+.dex / .so needs to be pushed to the device.
+
+Faster Builds:
+
+ * No `final_dex` step (where all .dex files are merged into one)
+ * No need to rebuild .apk for code-only changes (but required for resources)
+ * Apks sign faster because they are smaller.
+
+Faster Installs:
+
+ * The .apk is smaller, and so faster to verify.
+ * No need to run `adb install` for code-only changes.
+ * Only changed .so / .dex files are pushed. MD5s of existing on-device files
+ are cached on host computer.
+
+Slower Initial Runs:
+
+ * The first time you run an incremental .apk, the `DexOpt` needs to run on all
+ .dex files. This step is normally done during `adb install`, but is done on
+ start-up for incremental apks.
+ * DexOpt results are cached, so subsequent runs are faster.
+ * The slowdown varies significantly based on the Android version. Android O+
+ has almost no visible slow-down.
+
+Caveats:
+ * Isolated processes (on L+) are incompatible with incremental install. As a
+ work-around, isolated processes are disabled when building incremental apks.
+ * Android resources, assets, and `loadable_modules` are not sideloaded (they
+ remain in the apk), so builds & installs that modify any of these are not as
+ fast as those that modify only .java / .cc.
+ * Since files are sideloaded to `/data/local/tmp`, you need to use the wrapper
+ scripts to uninstall them fully. E.g.:
+ ```shell
+ out/Default/bin/chrome_public_apk uninstall
+ ```
+
+## The Code
+
+All incremental apks have the same classes.dex, which is built from:
+
+ //build/android/incremental_install:bootstrap_java
+
+They also have a transformed `AndroidManifest.xml`, which overrides the the
+main application class and any instrumentation classes so that they instead
+point to `BootstrapApplication`. This is built by:
+
+ //build/android/incremental_install/generate_android_manifest.py
+
+Wrapper scripts and install logic is contained in:
+
+ //build/android/incremental_install/create_install_script.py
+ //build/android/incremental_install/installer.py
+
+Finally, GN logic for incremental apks is sprinkled throughout.
diff --git a/third_party/libwebrtc/build/android/incremental_install/__init__.py b/third_party/libwebrtc/build/android/incremental_install/__init__.py
new file mode 100644
index 0000000000..50b23dff63
--- /dev/null
+++ b/third_party/libwebrtc/build/android/incremental_install/__init__.py
@@ -0,0 +1,3 @@
+# Copyright 2015 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.
diff --git a/third_party/libwebrtc/build/android/incremental_install/generate_android_manifest.py b/third_party/libwebrtc/build/android/incremental_install/generate_android_manifest.py
new file mode 100755
index 0000000000..67feaa5a6f
--- /dev/null
+++ b/third_party/libwebrtc/build/android/incremental_install/generate_android_manifest.py
@@ -0,0 +1,141 @@
+#!/usr/bin/env python3
+#
+# Copyright 2015 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.
+"""Creates an AndroidManifest.xml for an incremental APK.
+
+Given the manifest file for the real APK, generates an AndroidManifest.xml with
+the application class changed to IncrementalApplication.
+"""
+
+import argparse
+import os
+import subprocess
+import sys
+import tempfile
+import zipfile
+from xml.etree import ElementTree
+
+sys.path.append(os.path.join(os.path.dirname(__file__), os.path.pardir, 'gyp'))
+from util import build_utils
+from util import manifest_utils
+from util import resource_utils
+
+_INCREMENTAL_APP_NAME = 'org.chromium.incrementalinstall.BootstrapApplication'
+_META_DATA_APP_NAME = 'incremental-install-real-app'
+_DEFAULT_APPLICATION_CLASS = 'android.app.Application'
+_META_DATA_INSTRUMENTATION_NAMES = [
+ 'incremental-install-real-instrumentation-0',
+ 'incremental-install-real-instrumentation-1',
+]
+_INCREMENTAL_INSTRUMENTATION_CLASSES = [
+ 'android.app.Instrumentation',
+ 'org.chromium.incrementalinstall.SecondInstrumentation',
+]
+
+
+def _AddNamespace(name):
+ """Adds the android namespace prefix to the given identifier."""
+ return '{%s}%s' % (manifest_utils.ANDROID_NAMESPACE, name)
+
+
+def _ParseArgs(args):
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ '--src-manifest', required=True, help='The main manifest of the app')
+ parser.add_argument('--disable-isolated-processes',
+ help='Changes all android:isolatedProcess to false. '
+ 'This is required on Android M+',
+ action='store_true')
+ parser.add_argument(
+ '--out-apk', required=True, help='Path to output .ap_ file')
+ parser.add_argument(
+ '--in-apk', required=True, help='Path to non-incremental .ap_ file')
+ parser.add_argument(
+ '--aapt2-path', required=True, help='Path to the Android aapt tool')
+ parser.add_argument(
+ '--android-sdk-jars', help='GN List of resource apks to include.')
+
+ ret = parser.parse_args(build_utils.ExpandFileArgs(args))
+ ret.android_sdk_jars = build_utils.ParseGnList(ret.android_sdk_jars)
+ return ret
+
+
+def _CreateMetaData(parent, name, value):
+ meta_data_node = ElementTree.SubElement(parent, 'meta-data')
+ meta_data_node.set(_AddNamespace('name'), name)
+ meta_data_node.set(_AddNamespace('value'), value)
+
+
+def _ProcessManifest(path, arsc_package_name, disable_isolated_processes):
+ doc, manifest_node, app_node = manifest_utils.ParseManifest(path)
+
+ # Ensure the manifest package matches that of the apk's arsc package
+ # So that resource references resolve correctly. The actual manifest
+ # package name is set via --rename-manifest-package.
+ manifest_node.set('package', arsc_package_name)
+
+ # Pylint for some reason things app_node is an int.
+ # pylint: disable=no-member
+ real_app_class = app_node.get(_AddNamespace('name'),
+ _DEFAULT_APPLICATION_CLASS)
+ app_node.set(_AddNamespace('name'), _INCREMENTAL_APP_NAME)
+ # pylint: enable=no-member
+ _CreateMetaData(app_node, _META_DATA_APP_NAME, real_app_class)
+
+ # Seems to be a bug in ElementTree, as doc.find() doesn't work here.
+ instrumentation_nodes = doc.findall('instrumentation')
+ assert len(instrumentation_nodes) <= 2, (
+ 'Need to update incremental install to support >2 <instrumentation> tags')
+ for i, instrumentation_node in enumerate(instrumentation_nodes):
+ real_instrumentation_class = instrumentation_node.get(_AddNamespace('name'))
+ instrumentation_node.set(_AddNamespace('name'),
+ _INCREMENTAL_INSTRUMENTATION_CLASSES[i])
+ _CreateMetaData(app_node, _META_DATA_INSTRUMENTATION_NAMES[i],
+ real_instrumentation_class)
+
+ ret = ElementTree.tostring(doc.getroot(), encoding='UTF-8')
+ # Disable check for page-aligned native libraries.
+ ret = ret.replace(b'extractNativeLibs="false"', b'extractNativeLibs="true"')
+ if disable_isolated_processes:
+ ret = ret.replace(b'isolatedProcess="true"', b'isolatedProcess="false"')
+ return ret
+
+
+def main(raw_args):
+ options = _ParseArgs(raw_args)
+
+ arsc_package, _ = resource_utils.ExtractArscPackage(options.aapt2_path,
+ options.in_apk)
+ assert arsc_package is not None, 'The apk does not have a valid package.'
+ # Extract version from the compiled manifest since it might have been set
+ # via aapt, and not exist in the manifest's text form.
+ version_code, version_name, manifest_package = (
+ resource_utils.ExtractBinaryManifestValues(options.aapt2_path,
+ options.in_apk))
+
+ new_manifest_data = _ProcessManifest(options.src_manifest, arsc_package,
+ options.disable_isolated_processes)
+ with tempfile.NamedTemporaryFile() as tmp_manifest, \
+ tempfile.NamedTemporaryFile() as tmp_apk:
+ tmp_manifest.write(new_manifest_data)
+ tmp_manifest.flush()
+ cmd = [
+ options.aapt2_path, 'link', '-o', tmp_apk.name, '--manifest',
+ tmp_manifest.name, '-I', options.in_apk, '--replace-version',
+ '--version-code', version_code, '--version-name', version_name,
+ '--rename-manifest-package', manifest_package, '--debug-mode'
+ ]
+ for j in options.android_sdk_jars:
+ cmd += ['-I', j]
+ subprocess.check_call(cmd)
+ with zipfile.ZipFile(options.out_apk, 'w') as z:
+ path_transform = lambda p: None if p != 'AndroidManifest.xml' else p
+ build_utils.MergeZips(z, [tmp_apk.name], path_transform=path_transform)
+ path_transform = lambda p: None if p == 'AndroidManifest.xml' else p
+ build_utils.MergeZips(z, [options.in_apk], path_transform=path_transform)
+
+
+if __name__ == '__main__':
+ main(sys.argv[1:])
diff --git a/third_party/libwebrtc/build/android/incremental_install/generate_android_manifest.pydeps b/third_party/libwebrtc/build/android/incremental_install/generate_android_manifest.pydeps
new file mode 100644
index 0000000000..b28c3070d8
--- /dev/null
+++ b/third_party/libwebrtc/build/android/incremental_install/generate_android_manifest.pydeps
@@ -0,0 +1,32 @@
+# Generated by running:
+# build/print_python_deps.py --root build/android/incremental_install --output build/android/incremental_install/generate_android_manifest.pydeps build/android/incremental_install/generate_android_manifest.py
+../../../third_party/jinja2/__init__.py
+../../../third_party/jinja2/_compat.py
+../../../third_party/jinja2/_identifier.py
+../../../third_party/jinja2/asyncfilters.py
+../../../third_party/jinja2/asyncsupport.py
+../../../third_party/jinja2/bccache.py
+../../../third_party/jinja2/compiler.py
+../../../third_party/jinja2/defaults.py
+../../../third_party/jinja2/environment.py
+../../../third_party/jinja2/exceptions.py
+../../../third_party/jinja2/filters.py
+../../../third_party/jinja2/idtracking.py
+../../../third_party/jinja2/lexer.py
+../../../third_party/jinja2/loaders.py
+../../../third_party/jinja2/nodes.py
+../../../third_party/jinja2/optimizer.py
+../../../third_party/jinja2/parser.py
+../../../third_party/jinja2/runtime.py
+../../../third_party/jinja2/tests.py
+../../../third_party/jinja2/utils.py
+../../../third_party/jinja2/visitor.py
+../../../third_party/markupsafe/__init__.py
+../../../third_party/markupsafe/_compat.py
+../../../third_party/markupsafe/_native.py
+../../gn_helpers.py
+../gyp/util/__init__.py
+../gyp/util/build_utils.py
+../gyp/util/manifest_utils.py
+../gyp/util/resource_utils.py
+generate_android_manifest.py
diff --git a/third_party/libwebrtc/build/android/incremental_install/installer.py b/third_party/libwebrtc/build/android/incremental_install/installer.py
new file mode 100755
index 0000000000..55e578884e
--- /dev/null
+++ b/third_party/libwebrtc/build/android/incremental_install/installer.py
@@ -0,0 +1,372 @@
+#!/usr/bin/env vpython3
+#
+# Copyright 2015 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.
+
+"""Install *_incremental.apk targets as well as their dependent files."""
+
+import argparse
+import collections
+import functools
+import glob
+import json
+import logging
+import os
+import posixpath
+import shutil
+import sys
+
+sys.path.append(
+ os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)))
+import devil_chromium
+from devil.android import apk_helper
+from devil.android import device_utils
+from devil.utils import reraiser_thread
+from devil.utils import run_tests_helper
+from pylib import constants
+from pylib.utils import time_profile
+
+prev_sys_path = list(sys.path)
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir, 'gyp'))
+import dex
+from util import build_utils
+sys.path = prev_sys_path
+
+
+_R8_PATH = os.path.join(build_utils.DIR_SOURCE_ROOT, 'third_party', 'r8', 'lib',
+ 'r8.jar')
+
+
+def _DeviceCachePath(device):
+ file_name = 'device_cache_%s.json' % device.adb.GetDeviceSerial()
+ return os.path.join(constants.GetOutDirectory(), file_name)
+
+
+def _Execute(concurrently, *funcs):
+ """Calls all functions in |funcs| concurrently or in sequence."""
+ timer = time_profile.TimeProfile()
+ if concurrently:
+ reraiser_thread.RunAsync(funcs)
+ else:
+ for f in funcs:
+ f()
+ timer.Stop(log=False)
+ return timer
+
+
+def _GetDeviceIncrementalDir(package):
+ """Returns the device path to put incremental files for the given package."""
+ return '/data/local/tmp/incremental-app-%s' % package
+
+
+def _IsStale(src_paths, dest):
+ """Returns if |dest| is older than any of |src_paths|, or missing."""
+ if not os.path.exists(dest):
+ return True
+ dest_time = os.path.getmtime(dest)
+ for path in src_paths:
+ if os.path.getmtime(path) > dest_time:
+ return True
+ return False
+
+
+def _AllocateDexShards(dex_files):
+ """Divides input dex files into buckets."""
+ # Goals:
+ # * Make shards small enough that they are fast to merge.
+ # * Minimize the number of shards so they load quickly on device.
+ # * Partition files into shards such that a change in one file results in only
+ # one shard having to be re-created.
+ shards = collections.defaultdict(list)
+ # As of Oct 2019, 10 shards results in a min/max size of 582K/2.6M.
+ NUM_CORE_SHARDS = 10
+ # As of Oct 2019, 17 dex files are larger than 1M.
+ SHARD_THRESHOLD = 2**20
+ for src_path in dex_files:
+ if os.path.getsize(src_path) >= SHARD_THRESHOLD:
+ # Use the path as the name rather than an incrementing number to ensure
+ # that it shards to the same name every time.
+ name = os.path.relpath(src_path, constants.GetOutDirectory()).replace(
+ os.sep, '.')
+ shards[name].append(src_path)
+ else:
+ name = 'shard{}.dex.jar'.format(hash(src_path) % NUM_CORE_SHARDS)
+ shards[name].append(src_path)
+ logging.info('Sharding %d dex files into %d buckets', len(dex_files),
+ len(shards))
+ return shards
+
+
+def _CreateDexFiles(shards, dex_staging_dir, min_api, use_concurrency):
+ """Creates dex files within |dex_staging_dir| defined by |shards|."""
+ tasks = []
+ for name, src_paths in shards.items():
+ dest_path = os.path.join(dex_staging_dir, name)
+ if _IsStale(src_paths, dest_path):
+ tasks.append(
+ functools.partial(dex.MergeDexForIncrementalInstall, _R8_PATH,
+ src_paths, dest_path, min_api))
+
+ # TODO(agrieve): It would be more performant to write a custom d8.jar
+ # wrapper in java that would process these in bulk, rather than spinning
+ # up a new process for each one.
+ _Execute(use_concurrency, *tasks)
+
+ # Remove any stale shards.
+ for name in os.listdir(dex_staging_dir):
+ if name not in shards:
+ os.unlink(os.path.join(dex_staging_dir, name))
+
+
+def Uninstall(device, package, enable_device_cache=False):
+ """Uninstalls and removes all incremental files for the given package."""
+ main_timer = time_profile.TimeProfile()
+ device.Uninstall(package)
+ if enable_device_cache:
+ # Uninstall is rare, so just wipe the cache in this case.
+ cache_path = _DeviceCachePath(device)
+ if os.path.exists(cache_path):
+ os.unlink(cache_path)
+ device.RunShellCommand(['rm', '-rf', _GetDeviceIncrementalDir(package)],
+ check_return=True)
+ logging.info('Uninstall took %s seconds.', main_timer.GetDelta())
+
+
+def Install(device, install_json, apk=None, enable_device_cache=False,
+ use_concurrency=True, permissions=()):
+ """Installs the given incremental apk and all required supporting files.
+
+ Args:
+ device: A DeviceUtils instance (to install to).
+ install_json: Path to .json file or already parsed .json object.
+ apk: An existing ApkHelper instance for the apk (optional).
+ enable_device_cache: Whether to enable on-device caching of checksums.
+ use_concurrency: Whether to speed things up using multiple threads.
+ permissions: A list of the permissions to grant, or None to grant all
+ non-denylisted permissions in the manifest.
+ """
+ if isinstance(install_json, str):
+ with open(install_json) as f:
+ install_dict = json.load(f)
+ else:
+ install_dict = install_json
+
+ main_timer = time_profile.TimeProfile()
+ install_timer = time_profile.TimeProfile()
+ push_native_timer = time_profile.TimeProfile()
+ merge_dex_timer = time_profile.TimeProfile()
+ push_dex_timer = time_profile.TimeProfile()
+
+ def fix_path(p):
+ return os.path.normpath(os.path.join(constants.GetOutDirectory(), p))
+
+ if not apk:
+ apk = apk_helper.ToHelper(fix_path(install_dict['apk_path']))
+ split_globs = [fix_path(p) for p in install_dict['split_globs']]
+ native_libs = [fix_path(p) for p in install_dict['native_libs']]
+ dex_files = [fix_path(p) for p in install_dict['dex_files']]
+ show_proguard_warning = install_dict.get('show_proguard_warning')
+
+ apk_package = apk.GetPackageName()
+ device_incremental_dir = _GetDeviceIncrementalDir(apk_package)
+ dex_staging_dir = os.path.join(constants.GetOutDirectory(),
+ 'incremental-install',
+ install_dict['apk_path'])
+ device_dex_dir = posixpath.join(device_incremental_dir, 'dex')
+
+ # Install .apk(s) if any of them have changed.
+ def do_install():
+ install_timer.Start()
+ if split_globs:
+ splits = []
+ for split_glob in split_globs:
+ splits.extend((f for f in glob.glob(split_glob)))
+ device.InstallSplitApk(
+ apk,
+ splits,
+ allow_downgrade=True,
+ reinstall=True,
+ allow_cached_props=True,
+ permissions=permissions)
+ else:
+ device.Install(
+ apk, allow_downgrade=True, reinstall=True, permissions=permissions)
+ install_timer.Stop(log=False)
+
+ # Push .so and .dex files to the device (if they have changed).
+ def do_push_files():
+
+ def do_push_native():
+ push_native_timer.Start()
+ if native_libs:
+ with build_utils.TempDir() as temp_dir:
+ device_lib_dir = posixpath.join(device_incremental_dir, 'lib')
+ for path in native_libs:
+ # Note: Can't use symlinks as they don't work when
+ # "adb push parent_dir" is used (like we do here).
+ shutil.copy(path, os.path.join(temp_dir, os.path.basename(path)))
+ device.PushChangedFiles([(temp_dir, device_lib_dir)],
+ delete_device_stale=True)
+ push_native_timer.Stop(log=False)
+
+ def do_merge_dex():
+ merge_dex_timer.Start()
+ shards = _AllocateDexShards(dex_files)
+ build_utils.MakeDirectory(dex_staging_dir)
+ _CreateDexFiles(shards, dex_staging_dir, apk.GetMinSdkVersion(),
+ use_concurrency)
+ merge_dex_timer.Stop(log=False)
+
+ def do_push_dex():
+ push_dex_timer.Start()
+ device.PushChangedFiles([(dex_staging_dir, device_dex_dir)],
+ delete_device_stale=True)
+ push_dex_timer.Stop(log=False)
+
+ _Execute(use_concurrency, do_push_native, do_merge_dex)
+ do_push_dex()
+
+ def check_device_configured():
+ target_sdk_version = int(apk.GetTargetSdkVersion())
+ # Beta Q builds apply allowlist to targetSdk=28 as well.
+ if target_sdk_version >= 28 and device.build_version_sdk >= 28:
+ # In P, there are two settings:
+ # * hidden_api_policy_p_apps
+ # * hidden_api_policy_pre_p_apps
+ # In Q, there is just one:
+ # * hidden_api_policy
+ if device.build_version_sdk == 28:
+ setting_name = 'hidden_api_policy_p_apps'
+ else:
+ setting_name = 'hidden_api_policy'
+ apis_allowed = ''.join(
+ device.RunShellCommand(['settings', 'get', 'global', setting_name],
+ check_return=True))
+ if apis_allowed.strip() not in '01':
+ msg = """\
+Cannot use incremental installs on Android P+ without first enabling access to
+non-SDK interfaces (https://developer.android.com/preview/non-sdk-q).
+
+To enable access:
+ adb -s {0} shell settings put global {1} 0
+To restore back to default:
+ adb -s {0} shell settings delete global {1}"""
+ raise Exception(msg.format(device.serial, setting_name))
+
+ cache_path = _DeviceCachePath(device)
+ def restore_cache():
+ if not enable_device_cache:
+ return
+ if os.path.exists(cache_path):
+ logging.info('Using device cache: %s', cache_path)
+ with open(cache_path) as f:
+ device.LoadCacheData(f.read())
+ # Delete the cached file so that any exceptions cause it to be cleared.
+ os.unlink(cache_path)
+ else:
+ logging.info('No device cache present: %s', cache_path)
+
+ def save_cache():
+ if not enable_device_cache:
+ return
+ with open(cache_path, 'w') as f:
+ f.write(device.DumpCacheData())
+ logging.info('Wrote device cache: %s', cache_path)
+
+ # Create 2 lock files:
+ # * install.lock tells the app to pause on start-up (until we release it).
+ # * firstrun.lock is used by the app to pause all secondary processes until
+ # the primary process finishes loading the .dex / .so files.
+ def create_lock_files():
+ # Creates or zeros out lock files.
+ cmd = ('D="%s";'
+ 'mkdir -p $D &&'
+ 'echo -n >$D/install.lock 2>$D/firstrun.lock')
+ device.RunShellCommand(
+ cmd % device_incremental_dir, shell=True, check_return=True)
+
+ # The firstrun.lock is released by the app itself.
+ def release_installer_lock():
+ device.RunShellCommand('echo > %s/install.lock' % device_incremental_dir,
+ check_return=True, shell=True)
+
+ # Concurrency here speeds things up quite a bit, but DeviceUtils hasn't
+ # been designed for multi-threading. Enabling only because this is a
+ # developer-only tool.
+ setup_timer = _Execute(use_concurrency, create_lock_files, restore_cache,
+ check_device_configured)
+
+ _Execute(use_concurrency, do_install, do_push_files)
+
+ finalize_timer = _Execute(use_concurrency, release_installer_lock, save_cache)
+
+ logging.info(
+ 'Install of %s took %s seconds (setup=%s, install=%s, lib_push=%s, '
+ 'dex_merge=%s dex_push=%s, finalize=%s)', os.path.basename(apk.path),
+ main_timer.GetDelta(), setup_timer.GetDelta(), install_timer.GetDelta(),
+ push_native_timer.GetDelta(), merge_dex_timer.GetDelta(),
+ push_dex_timer.GetDelta(), finalize_timer.GetDelta())
+ if show_proguard_warning:
+ logging.warning('Target had proguard enabled, but incremental install uses '
+ 'non-proguarded .dex files. Performance characteristics '
+ 'may differ.')
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('json_path',
+ help='The path to the generated incremental apk .json.')
+ parser.add_argument('-d', '--device', dest='device',
+ help='Target device for apk to install on.')
+ parser.add_argument('--uninstall',
+ action='store_true',
+ default=False,
+ help='Remove the app and all side-loaded files.')
+ parser.add_argument('--output-directory',
+ help='Path to the root build directory.')
+ parser.add_argument('--no-threading',
+ action='store_false',
+ default=True,
+ dest='threading',
+ help='Do not install and push concurrently')
+ parser.add_argument('--no-cache',
+ action='store_false',
+ default=True,
+ dest='cache',
+ help='Do not use cached information about what files are '
+ 'currently on the target device.')
+ parser.add_argument('-v',
+ '--verbose',
+ dest='verbose_count',
+ default=0,
+ action='count',
+ help='Verbose level (multiple times for more)')
+
+ args = parser.parse_args()
+
+ run_tests_helper.SetLogLevel(args.verbose_count)
+ if args.output_directory:
+ constants.SetOutputDirectory(args.output_directory)
+
+ devil_chromium.Initialize(output_directory=constants.GetOutDirectory())
+
+ # Retries are annoying when commands fail for legitimate reasons. Might want
+ # to enable them if this is ever used on bots though.
+ device = device_utils.DeviceUtils.HealthyDevices(
+ device_arg=args.device,
+ default_retries=0,
+ enable_device_files_cache=True)[0]
+
+ if args.uninstall:
+ with open(args.json_path) as f:
+ install_dict = json.load(f)
+ apk = apk_helper.ToHelper(install_dict['apk_path'])
+ Uninstall(device, apk.GetPackageName(), enable_device_cache=args.cache)
+ else:
+ Install(device, args.json_path, enable_device_cache=args.cache,
+ use_concurrency=args.threading)
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/third_party/libwebrtc/build/android/incremental_install/java/org/chromium/incrementalinstall/BootstrapApplication.java b/third_party/libwebrtc/build/android/incremental_install/java/org/chromium/incrementalinstall/BootstrapApplication.java
new file mode 100644
index 0000000000..f7003f27ea
--- /dev/null
+++ b/third_party/libwebrtc/build/android/incremental_install/java/org/chromium/incrementalinstall/BootstrapApplication.java
@@ -0,0 +1,297 @@
+// Copyright 2015 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.incrementalinstall;
+
+import android.app.Application;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.util.Log;
+
+import dalvik.system.DexFile;
+
+import java.io.File;
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An Application that replaces itself with another Application (as defined in
+ * an AndroidManifext.xml meta-data tag). It loads the other application only
+ * after side-loading its .so and .dex files from /data/local/tmp.
+ *
+ * This class is highly dependent on the private implementation details of
+ * Android's ActivityThread.java. However, it has been tested to work with
+ * JellyBean through Marshmallow.
+ */
+public final class BootstrapApplication extends Application {
+ private static final String TAG = "incrementalinstall";
+ private static final String MANAGED_DIR_PREFIX = "/data/local/tmp/incremental-app-";
+ private static final String REAL_APP_META_DATA_NAME = "incremental-install-real-app";
+ private static final String REAL_INSTRUMENTATION_META_DATA_NAME0 =
+ "incremental-install-real-instrumentation-0";
+ private static final String REAL_INSTRUMENTATION_META_DATA_NAME1 =
+ "incremental-install-real-instrumentation-1";
+
+ private ClassLoaderPatcher mClassLoaderPatcher;
+ private Application mRealApplication;
+ private Instrumentation mOrigInstrumentation;
+ private Instrumentation mRealInstrumentation;
+ private Object mStashedProviderList;
+ private Object mActivityThread;
+ public static DexFile[] sIncrementalDexFiles; // Needed by junit test runner.
+
+ @Override
+ protected void attachBaseContext(Context context) {
+ super.attachBaseContext(context);
+ try {
+ mActivityThread = Reflect.invokeMethod(Class.forName("android.app.ActivityThread"),
+ "currentActivityThread");
+ mClassLoaderPatcher = new ClassLoaderPatcher(context);
+
+ mOrigInstrumentation =
+ (Instrumentation) Reflect.getField(mActivityThread, "mInstrumentation");
+ Context instContext = mOrigInstrumentation.getContext();
+ if (instContext == null) {
+ instContext = context;
+ }
+
+ // When running with an instrumentation that lives in a different package from the
+ // application, we must load the dex files and native libraries from both pacakges.
+ // This logic likely won't work when the instrumentation is incremental, but the app is
+ // non-incremental. This configuration isn't used right now though.
+ String appPackageName = getPackageName();
+ String instPackageName = instContext.getPackageName();
+ boolean instPackageNameDiffers = !appPackageName.equals(instPackageName);
+ Log.i(TAG, "App PackageName: " + appPackageName);
+ if (instPackageNameDiffers) {
+ Log.i(TAG, "Inst PackageName: " + instPackageName);
+ }
+
+ File appIncrementalRootDir = new File(MANAGED_DIR_PREFIX + appPackageName);
+ File appLibDir = new File(appIncrementalRootDir, "lib");
+ File appDexDir = new File(appIncrementalRootDir, "dex");
+ File appInstallLockFile = new File(appIncrementalRootDir, "install.lock");
+ File appFirstRunLockFile = new File(appIncrementalRootDir, "firstrun.lock");
+ File instIncrementalRootDir = new File(MANAGED_DIR_PREFIX + instPackageName);
+ File instLibDir = new File(instIncrementalRootDir, "lib");
+ File instDexDir = new File(instIncrementalRootDir, "dex");
+ File instInstallLockFile = new File(instIncrementalRootDir, "install.lock");
+ File instFirstRunLockFile = new File(instIncrementalRootDir, "firstrun.lock");
+
+ boolean isFirstRun = LockFile.installerLockExists(appFirstRunLockFile)
+ || (instPackageNameDiffers
+ && LockFile.installerLockExists(instFirstRunLockFile));
+ if (isFirstRun) {
+ if (mClassLoaderPatcher.mIsPrimaryProcess) {
+ // Wait for incremental_install.py to finish.
+ LockFile.waitForInstallerLock(appInstallLockFile, 30 * 1000);
+ LockFile.waitForInstallerLock(instInstallLockFile, 30 * 1000);
+ } else {
+ // Wait for the browser process to create the optimized dex files
+ // and copy the library files.
+ LockFile.waitForInstallerLock(appFirstRunLockFile, 60 * 1000);
+ LockFile.waitForInstallerLock(instFirstRunLockFile, 60 * 1000);
+ }
+ }
+
+ mClassLoaderPatcher.importNativeLibs(instLibDir);
+ sIncrementalDexFiles = mClassLoaderPatcher.loadDexFiles(instDexDir, instPackageName);
+ if (instPackageNameDiffers) {
+ mClassLoaderPatcher.importNativeLibs(appLibDir);
+ mClassLoaderPatcher.loadDexFiles(appDexDir, appPackageName);
+ }
+
+ if (isFirstRun && mClassLoaderPatcher.mIsPrimaryProcess) {
+ LockFile.clearInstallerLock(appFirstRunLockFile);
+ if (instPackageNameDiffers) {
+ LockFile.clearInstallerLock(instFirstRunLockFile);
+ }
+ }
+
+ // mInstrumentationAppDir is one of a set of fields that is initialized only when
+ // instrumentation is active.
+ if (Reflect.getField(mActivityThread, "mInstrumentationAppDir") != null) {
+ String metaDataName = REAL_INSTRUMENTATION_META_DATA_NAME0;
+ if (mOrigInstrumentation instanceof SecondInstrumentation) {
+ metaDataName = REAL_INSTRUMENTATION_META_DATA_NAME1;
+ }
+ mRealInstrumentation =
+ initInstrumentation(getClassNameFromMetadata(metaDataName, instContext));
+ } else {
+ Log.i(TAG, "No instrumentation active.");
+ }
+
+ // Even when instrumentation is not enabled, ActivityThread uses a default
+ // Instrumentation instance internally. We hook it here in order to hook into the
+ // call to Instrumentation.onCreate().
+ BootstrapInstrumentation bootstrapInstrumentation = new BootstrapInstrumentation(this);
+ populateInstrumenationFields(bootstrapInstrumentation);
+ Reflect.setField(mActivityThread, "mInstrumentation", bootstrapInstrumentation);
+
+ // attachBaseContext() is called from ActivityThread#handleBindApplication() and
+ // Application#mApplication is changed right after we return. Thus, we cannot swap
+ // the Application instances until onCreate() is called.
+ String realApplicationName = getClassNameFromMetadata(REAL_APP_META_DATA_NAME, context);
+ Log.i(TAG, "Instantiating " + realApplicationName);
+ Instrumentation anyInstrumentation =
+ mRealInstrumentation != null ? mRealInstrumentation : mOrigInstrumentation;
+ mRealApplication = anyInstrumentation.newApplication(
+ getClassLoader(), realApplicationName, context);
+
+ // Between attachBaseContext() and onCreate(), ActivityThread tries to instantiate
+ // all ContentProviders. The ContentProviders break without the correct Application
+ // class being installed, so temporarily pretend there are no providers, and then
+ // instantiate them explicitly within onCreate().
+ disableContentProviders();
+ Log.i(TAG, "Waiting for Instrumentation.onCreate");
+ } catch (Exception e) {
+ throw new RuntimeException("Incremental install failed.", e);
+ }
+ }
+
+ /**
+ * Returns the fully-qualified class name for the given key, stored in a
+ * &lt;meta&gt; witin the manifest.
+ */
+ private static String getClassNameFromMetadata(String key, Context context)
+ throws NameNotFoundException {
+ String pkgName = context.getPackageName();
+ ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(pkgName,
+ PackageManager.GET_META_DATA);
+ String value = appInfo.metaData.getString(key);
+ if (value != null && !value.contains(".")) {
+ value = pkgName + "." + value;
+ }
+ return value;
+ }
+
+ /**
+ * Instantiates and initializes mRealInstrumentation (the real Instrumentation class).
+ */
+ private Instrumentation initInstrumentation(String realInstrumentationName)
+ throws ReflectiveOperationException {
+ if (realInstrumentationName == null) {
+ // This is the case when an incremental app is used as a target for an instrumentation
+ // test. In this case, ActivityThread can instantiate the proper class just fine since
+ // it exists within the test apk (as opposed to the incremental apk-under-test).
+ Log.i(TAG, "Running with external instrumentation");
+ return null;
+ }
+ // For unit tests, the instrumentation class is replaced in the manifest by a build step
+ // because ActivityThread tries to instantiate it before we get a chance to load the
+ // incremental dex files.
+ Log.i(TAG, "Instantiating instrumentation " + realInstrumentationName);
+ Instrumentation ret =
+ (Instrumentation) Reflect.newInstance(Class.forName(realInstrumentationName));
+ populateInstrumenationFields(ret);
+ return ret;
+ }
+
+ /**
+ * Sets important fields on a newly created Instrumentation object by copying them from the
+ * original Instrumentation instance.
+ */
+ private void populateInstrumenationFields(Instrumentation target)
+ throws ReflectiveOperationException {
+ // Initialize the fields that are set by Instrumentation.init().
+ String[] initFields = {"mAppContext", "mComponent", "mInstrContext", "mMessageQueue",
+ "mThread", "mUiAutomationConnection", "mWatcher"};
+ for (String fieldName : initFields) {
+ Reflect.setField(target, fieldName, Reflect.getField(mOrigInstrumentation, fieldName));
+ }
+ }
+
+ /**
+ * Called by BootstrapInstrumentation from Instrumentation.onCreate().
+ * This happens regardless of whether or not instrumentation is enabled.
+ */
+ void onInstrumentationCreate(Bundle arguments) {
+ Log.i(TAG, "Instrumentation.onCreate() called. Swapping references.");
+ try {
+ swapApplicationReferences();
+ enableContentProviders();
+ if (mRealInstrumentation != null) {
+ Reflect.setField(mActivityThread, "mInstrumentation", mRealInstrumentation);
+ mRealInstrumentation.onCreate(arguments);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Incremental install failed.", e);
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ try {
+ Log.i(TAG, "Application.onCreate() called.");
+ mRealApplication.onCreate();
+ } catch (Exception e) {
+ throw new RuntimeException("Incremental install failed.", e);
+ }
+ }
+
+ /**
+ * Nulls out ActivityThread.mBoundApplication.providers.
+ */
+ private void disableContentProviders() throws ReflectiveOperationException {
+ Object data = Reflect.getField(mActivityThread, "mBoundApplication");
+ mStashedProviderList = Reflect.getField(data, "providers");
+ Reflect.setField(data, "providers", null);
+ }
+
+ /**
+ * Restores the value of ActivityThread.mBoundApplication.providers, and invokes
+ * ActivityThread#installContentProviders().
+ */
+ private void enableContentProviders() throws ReflectiveOperationException {
+ Object data = Reflect.getField(mActivityThread, "mBoundApplication");
+ Reflect.setField(data, "providers", mStashedProviderList);
+ if (mStashedProviderList != null && mClassLoaderPatcher.mIsPrimaryProcess) {
+ Log.i(TAG, "Instantiating content providers");
+ Reflect.invokeMethod(mActivityThread, "installContentProviders", mRealApplication,
+ mStashedProviderList);
+ }
+ mStashedProviderList = null;
+ }
+
+ /**
+ * Changes all fields within framework classes that have stored an reference to this
+ * BootstrapApplication to instead store references to mRealApplication.
+ */
+ @SuppressWarnings("unchecked")
+ private void swapApplicationReferences() throws ReflectiveOperationException {
+ if (Reflect.getField(mActivityThread, "mInitialApplication") == this) {
+ Reflect.setField(mActivityThread, "mInitialApplication", mRealApplication);
+ }
+
+ List<Application> allApplications =
+ (List<Application>) Reflect.getField(mActivityThread, "mAllApplications");
+ for (int i = 0; i < allApplications.size(); i++) {
+ if (allApplications.get(i) == this) {
+ allApplications.set(i, mRealApplication);
+ }
+ }
+
+ // Contains a reference to BootstrapApplication and will cause BroadCastReceivers to fail
+ // if not replaced.
+ Context contextImpl = mRealApplication.getBaseContext();
+ Reflect.setField(contextImpl, "mOuterContext", mRealApplication);
+
+ for (String fieldName : new String[] {"mPackages", "mResourcePackages"}) {
+ Map<String, WeakReference<?>> packageMap =
+ (Map<String, WeakReference<?>>) Reflect.getField(mActivityThread, fieldName);
+ for (Map.Entry<String, WeakReference<?>> entry : packageMap.entrySet()) {
+ Object loadedApk = entry.getValue().get();
+ if (loadedApk != null && Reflect.getField(loadedApk, "mApplication") == this) {
+ Reflect.setField(loadedApk, "mApplication", mRealApplication);
+ }
+ }
+ }
+ }
+}
diff --git a/third_party/libwebrtc/build/android/incremental_install/java/org/chromium/incrementalinstall/BootstrapInstrumentation.java b/third_party/libwebrtc/build/android/incremental_install/java/org/chromium/incrementalinstall/BootstrapInstrumentation.java
new file mode 100644
index 0000000000..f197406499
--- /dev/null
+++ b/third_party/libwebrtc/build/android/incremental_install/java/org/chromium/incrementalinstall/BootstrapInstrumentation.java
@@ -0,0 +1,25 @@
+// Copyright 2015 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.incrementalinstall;
+
+import android.app.Instrumentation;
+import android.os.Bundle;
+
+/**
+ * Notifies BootstrapApplication of the call to Instrumentation.onCreate().
+ */
+public final class BootstrapInstrumentation extends Instrumentation {
+ private final BootstrapApplication mApp;
+
+ BootstrapInstrumentation(BootstrapApplication app) {
+ mApp = app;
+ }
+
+ @Override
+ public void onCreate(Bundle arguments) {
+ super.onCreate(arguments);
+ mApp.onInstrumentationCreate(arguments);
+ }
+}
diff --git a/third_party/libwebrtc/build/android/incremental_install/java/org/chromium/incrementalinstall/ClassLoaderPatcher.java b/third_party/libwebrtc/build/android/incremental_install/java/org/chromium/incrementalinstall/ClassLoaderPatcher.java
new file mode 100644
index 0000000000..b6d752247b
--- /dev/null
+++ b/third_party/libwebrtc/build/android/incremental_install/java/org/chromium/incrementalinstall/ClassLoaderPatcher.java
@@ -0,0 +1,312 @@
+// Copyright 2015 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.incrementalinstall;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.Build;
+import android.os.Process;
+import android.util.Log;
+
+import dalvik.system.DexFile;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Provides the ability to add native libraries and .dex files to an existing class loader.
+ * Tested with Jellybean MR2 - Marshmellow.
+ */
+final class ClassLoaderPatcher {
+ private static final String TAG = "incrementalinstall";
+ private final File mAppFilesSubDir;
+ private final ClassLoader mClassLoader;
+ private final Object mLibcoreOs;
+ private final int mProcessUid;
+ final boolean mIsPrimaryProcess;
+
+ ClassLoaderPatcher(Context context) throws ReflectiveOperationException {
+ mAppFilesSubDir =
+ new File(context.getApplicationInfo().dataDir, "incremental-install-files");
+ mClassLoader = context.getClassLoader();
+ mLibcoreOs = Reflect.getField(Class.forName("libcore.io.Libcore"), "os");
+ mProcessUid = Process.myUid();
+ mIsPrimaryProcess = context.getApplicationInfo().uid == mProcessUid;
+ Log.i(TAG, "uid=" + mProcessUid + " (isPrimary=" + mIsPrimaryProcess + ")");
+ }
+
+ /**
+ * Loads all dex files within |dexDir| into the app's ClassLoader.
+ */
+ @SuppressLint({
+ "SetWorldReadable",
+ "SetWorldWritable",
+ })
+ DexFile[] loadDexFiles(File dexDir, String packageName)
+ throws ReflectiveOperationException, IOException {
+ Log.i(TAG, "Installing dex files from: " + dexDir);
+
+ File optimizedDir = null;
+ boolean isAtLeastOreo = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
+
+ if (isAtLeastOreo) {
+ // In O, optimizedDirectory is ignored, and the files are always put in an "oat"
+ // directory that is a sibling to the dex files themselves. SELinux policies
+ // prevent using odex files from /data/local/tmp, so we must first copy them
+ // into the app's data directory in order to get the odex files to live there.
+ // Use a package-name subdirectory to prevent name collisions when apk-under-test is
+ // used.
+ File newDexDir = new File(mAppFilesSubDir, packageName + "-dexes");
+ if (mIsPrimaryProcess) {
+ safeCopyAllFiles(dexDir, newDexDir);
+ }
+ dexDir = newDexDir;
+ } else {
+ // The optimized dex files will be owned by this process' user.
+ // Store them within the app's data dir rather than on /data/local/tmp
+ // so that they are still deleted (by the OS) when we uninstall
+ // (even on a non-rooted device).
+ File incrementalDexesDir = new File(mAppFilesSubDir, "optimized-dexes");
+ File isolatedDexesDir = new File(mAppFilesSubDir, "isolated-dexes");
+
+ if (mIsPrimaryProcess) {
+ ensureAppFilesSubDirExists();
+ // Allows isolated processes to access the same files.
+ incrementalDexesDir.mkdir();
+ incrementalDexesDir.setReadable(true, false);
+ incrementalDexesDir.setExecutable(true, false);
+ // Create a directory for isolated processes to create directories in.
+ isolatedDexesDir.mkdir();
+ isolatedDexesDir.setWritable(true, false);
+ isolatedDexesDir.setExecutable(true, false);
+
+ optimizedDir = incrementalDexesDir;
+ } else {
+ // There is a UID check of the directory in dalvik.system.DexFile():
+ // https://android.googlesource.com/platform/libcore/+/45e0260/dalvik/src/main/java/dalvik/system/DexFile.java#101
+ // Rather than have each isolated process run DexOpt though, we use
+ // symlinks within the directory to point at the browser process'
+ // optimized dex files.
+ optimizedDir = new File(isolatedDexesDir, "isolated-" + mProcessUid);
+ optimizedDir.mkdir();
+ // Always wipe it out and re-create for simplicity.
+ Log.i(TAG, "Creating dex file symlinks for isolated process");
+ for (File f : optimizedDir.listFiles()) {
+ f.delete();
+ }
+ for (File f : incrementalDexesDir.listFiles()) {
+ String to = "../../" + incrementalDexesDir.getName() + "/" + f.getName();
+ File from = new File(optimizedDir, f.getName());
+ createSymlink(to, from);
+ }
+ }
+ Log.i(TAG, "Code cache dir: " + optimizedDir);
+ }
+
+ // Ignore "oat" directory.
+ // Also ignore files that sometimes show up (e.g. .jar.arm.flock).
+ File[] dexFilesArr = dexDir.listFiles(f -> f.getName().endsWith(".jar"));
+ if (dexFilesArr == null) {
+ throw new FileNotFoundException("Dex dir does not exist: " + dexDir);
+ }
+
+ Log.i(TAG, "Loading " + dexFilesArr.length + " dex files");
+
+ Object dexPathList = Reflect.getField(mClassLoader, "pathList");
+ Object[] dexElements = (Object[]) Reflect.getField(dexPathList, "dexElements");
+ dexElements = addDexElements(dexFilesArr, optimizedDir, dexElements);
+ Reflect.setField(dexPathList, "dexElements", dexElements);
+
+ // Return the list of new DexFile instances for the .jars in dexPathList.
+ DexFile[] ret = new DexFile[dexFilesArr.length];
+ int startIndex = dexElements.length - dexFilesArr.length;
+ for (int i = 0; i < ret.length; ++i) {
+ ret[i] = (DexFile) Reflect.getField(dexElements[startIndex + i], "dexFile");
+ }
+ return ret;
+ }
+
+ /**
+ * Sets up all libraries within |libDir| to be loadable by System.loadLibrary().
+ */
+ @SuppressLint("SetWorldReadable")
+ void importNativeLibs(File libDir) throws ReflectiveOperationException, IOException {
+ Log.i(TAG, "Importing native libraries from: " + libDir);
+ if (!libDir.exists()) {
+ Log.i(TAG, "No native libs exist.");
+ return;
+ }
+ // The library copying is not necessary on older devices, but we do it anyways to
+ // simplify things (it's fast compared to dexing).
+ // https://code.google.com/p/android/issues/detail?id=79480
+ File localLibsDir = new File(mAppFilesSubDir, "lib");
+ safeCopyAllFiles(libDir, localLibsDir);
+ addNativeLibrarySearchPath(localLibsDir);
+ }
+
+ @SuppressLint("SetWorldReadable")
+ private void safeCopyAllFiles(File srcDir, File dstDir) throws IOException {
+ // The library copying is not necessary on older devices, but we do it anyways to
+ // simplify things (it's fast compared to dexing).
+ // https://code.google.com/p/android/issues/detail?id=79480
+ File lockFile = new File(mAppFilesSubDir, dstDir.getName() + ".lock");
+ if (mIsPrimaryProcess) {
+ ensureAppFilesSubDirExists();
+ LockFile lock = LockFile.acquireRuntimeLock(lockFile);
+ if (lock == null) {
+ LockFile.waitForRuntimeLock(lockFile, 10 * 1000);
+ } else {
+ try {
+ dstDir.mkdir();
+ dstDir.setReadable(true, false);
+ dstDir.setExecutable(true, false);
+ copyChangedFiles(srcDir, dstDir);
+ } finally {
+ lock.release();
+ }
+ }
+ } else {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ // TODO: Work around this issue by using APK splits to install each dex / lib.
+ throw new RuntimeException("Incremental install does not work on Android M+ "
+ + "with isolated processes. Build system should have removed this. "
+ + "Please file a bug.");
+ }
+ // Other processes: Waits for primary process to finish copying.
+ LockFile.waitForRuntimeLock(lockFile, 10 * 1000);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private void addNativeLibrarySearchPath(File nativeLibDir) throws ReflectiveOperationException {
+ Object dexPathList = Reflect.getField(mClassLoader, "pathList");
+ Object currentDirs = Reflect.getField(dexPathList, "nativeLibraryDirectories");
+ File[] newDirs = new File[] { nativeLibDir };
+ // Switched from an array to an ArrayList in Lollipop.
+ if (currentDirs instanceof List) {
+ List<File> dirsAsList = (List<File>) currentDirs;
+ dirsAsList.add(0, nativeLibDir);
+ } else {
+ File[] dirsAsArray = (File[]) currentDirs;
+ Reflect.setField(dexPathList, "nativeLibraryDirectories",
+ Reflect.concatArrays(newDirs, newDirs, dirsAsArray));
+ }
+
+ Object[] nativeLibraryPathElements;
+ try {
+ nativeLibraryPathElements =
+ (Object[]) Reflect.getField(dexPathList, "nativeLibraryPathElements");
+ } catch (NoSuchFieldException e) {
+ // This field doesn't exist pre-M.
+ return;
+ }
+ Object[] additionalElements = makeNativePathElements(newDirs);
+ Reflect.setField(dexPathList, "nativeLibraryPathElements",
+ Reflect.concatArrays(nativeLibraryPathElements, additionalElements,
+ nativeLibraryPathElements));
+ }
+
+ private static void copyChangedFiles(File srcDir, File dstDir) throws IOException {
+ int numUpdated = 0;
+ File[] srcFiles = srcDir.listFiles();
+ for (File f : srcFiles) {
+ // Note: Tried using hardlinks, but resulted in EACCES exceptions.
+ File dest = new File(dstDir, f.getName());
+ if (copyIfModified(f, dest)) {
+ numUpdated++;
+ }
+ }
+ // Delete stale files.
+ int numDeleted = 0;
+ for (File f : dstDir.listFiles()) {
+ File src = new File(srcDir, f.getName());
+ if (!src.exists()) {
+ numDeleted++;
+ f.delete();
+ }
+ }
+ String msg = String.format(Locale.US,
+ "copyChangedFiles: %d of %d updated. %d stale files removed.", numUpdated,
+ srcFiles.length, numDeleted);
+ Log.i(TAG, msg);
+ }
+
+ @SuppressLint("SetWorldReadable")
+ private static boolean copyIfModified(File src, File dest) throws IOException {
+ long lastModified = src.lastModified();
+ if (dest.exists() && dest.lastModified() == lastModified) {
+ return false;
+ }
+ Log.i(TAG, "Copying " + src + " -> " + dest);
+ FileInputStream istream = new FileInputStream(src);
+ FileOutputStream ostream = new FileOutputStream(dest);
+ ostream.getChannel().transferFrom(istream.getChannel(), 0, istream.getChannel().size());
+ istream.close();
+ ostream.close();
+ dest.setReadable(true, false);
+ dest.setExecutable(true, false);
+ dest.setLastModified(lastModified);
+ return true;
+ }
+
+ private void ensureAppFilesSubDirExists() {
+ mAppFilesSubDir.mkdir();
+ mAppFilesSubDir.setExecutable(true, false);
+ }
+
+ private void createSymlink(String to, File from) throws ReflectiveOperationException {
+ Reflect.invokeMethod(mLibcoreOs, "symlink", to, from.getAbsolutePath());
+ }
+
+ private static Object[] makeNativePathElements(File[] paths)
+ throws ReflectiveOperationException {
+ Object[] entries = new Object[paths.length];
+ if (Build.VERSION.SDK_INT >= 26) {
+ Class<?> entryClazz = Class.forName("dalvik.system.DexPathList$NativeLibraryElement");
+ for (int i = 0; i < paths.length; ++i) {
+ entries[i] = Reflect.newInstance(entryClazz, paths[i]);
+ }
+ } else {
+ Class<?> entryClazz = Class.forName("dalvik.system.DexPathList$Element");
+ for (int i = 0; i < paths.length; ++i) {
+ entries[i] = Reflect.newInstance(entryClazz, paths[i], true, null, null);
+ }
+ }
+ return entries;
+ }
+
+ private Object[] addDexElements(File[] files, File optimizedDirectory, Object[] curDexElements)
+ throws ReflectiveOperationException {
+ Class<?> entryClazz = Class.forName("dalvik.system.DexPathList$Element");
+ Class<?> clazz = Class.forName("dalvik.system.DexPathList");
+ Object[] ret =
+ Reflect.concatArrays(curDexElements, curDexElements, new Object[files.length]);
+ File emptyDir = new File("");
+ for (int i = 0; i < files.length; ++i) {
+ File file = files[i];
+ Object dexFile;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ // loadDexFile requires that ret contain all previously added elements.
+ dexFile = Reflect.invokeMethod(clazz, "loadDexFile", file, optimizedDirectory,
+ mClassLoader, ret);
+ } else {
+ dexFile = Reflect.invokeMethod(clazz, "loadDexFile", file, optimizedDirectory);
+ }
+ Object dexElement;
+ if (Build.VERSION.SDK_INT >= 26) {
+ dexElement = Reflect.newInstance(entryClazz, dexFile, file);
+ } else {
+ dexElement = Reflect.newInstance(entryClazz, emptyDir, false, file, dexFile);
+ }
+ ret[curDexElements.length + i] = dexElement;
+ }
+ return ret;
+ }
+}
diff --git a/third_party/libwebrtc/build/android/incremental_install/java/org/chromium/incrementalinstall/LockFile.java b/third_party/libwebrtc/build/android/incremental_install/java/org/chromium/incrementalinstall/LockFile.java
new file mode 100644
index 0000000000..19d1f7624e
--- /dev/null
+++ b/third_party/libwebrtc/build/android/incremental_install/java/org/chromium/incrementalinstall/LockFile.java
@@ -0,0 +1,129 @@
+// Copyright 2015 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.incrementalinstall;
+
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.channels.FileLock;
+import java.util.concurrent.Callable;
+
+/**
+ * Helpers for dealing with .lock files used during install / first run.
+ */
+final class LockFile {
+ private static final String TAG = "incrementalinstall";
+
+ private final File mFile;
+ private final FileOutputStream mOutputStream;
+ private final FileLock mFileLock;
+
+ private LockFile(File file, FileOutputStream outputStream, FileLock fileLock) {
+ mFile = file;
+ mOutputStream = outputStream;
+ mFileLock = fileLock;
+ }
+
+ /**
+ * Clears the lock file by writing to it (making it non-zero in length);
+ */
+ static void clearInstallerLock(File lockFile) throws IOException {
+ Log.i(TAG, "Clearing " + lockFile);
+ // On Android M+, we can't delete files in /data/local/tmp, so we write to it instead.
+ FileOutputStream os = new FileOutputStream(lockFile);
+ os.write(1);
+ os.close();
+ }
+
+ /**
+ * Waits for the given file to be non-zero in length.
+ */
+ static void waitForInstallerLock(final File file, long timeoutMs) {
+ pollingWait(new Callable<Boolean>() {
+ @Override public Boolean call() {
+ return !installerLockExists(file);
+ }
+ }, file, timeoutMs);
+ }
+
+ /**
+ * Waits for the given file to be non-zero in length.
+ */
+ private static void pollingWait(Callable<Boolean> func, File file, long timeoutMs) {
+ long pollIntervalMs = 200;
+ for (int i = 0; i < timeoutMs / pollIntervalMs; i++) {
+ try {
+ if (func.call()) {
+ if (i > 0) {
+ Log.i(TAG, "Finished waiting on lock file: " + file);
+ }
+ return;
+ } else if (i == 0) {
+ Log.i(TAG, "Waiting on lock file: " + file);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ try {
+ Thread.sleep(pollIntervalMs);
+ } catch (InterruptedException e) {
+ // Should never happen.
+ }
+ }
+ throw new RuntimeException("Timed out waiting for lock file: " + file);
+ }
+
+ /**
+ * Returns whether the given lock file is missing or is in the locked state.
+ */
+ static boolean installerLockExists(File file) {
+ return !file.exists() || file.length() == 0;
+ }
+
+ /**
+ * Attempts to acquire a lock for the given file.
+ * @return Returns the FileLock if it was acquired, or null otherwise.
+ */
+ static LockFile acquireRuntimeLock(File file) {
+ try {
+ FileOutputStream outputStream = new FileOutputStream(file);
+ FileLock lock = outputStream.getChannel().tryLock();
+ if (lock != null) {
+ Log.i(TAG, "Created lock file: " + file);
+ return new LockFile(file, outputStream, lock);
+ }
+ outputStream.close();
+ } catch (IOException e) {
+ // Do nothing. We didn't get the lock.
+ Log.w(TAG, "Exception trying to acquire lock " + file, e);
+ }
+ return null;
+ }
+
+ /**
+ * Waits for the given file to not exist.
+ */
+ static void waitForRuntimeLock(final File file, long timeoutMs) {
+ pollingWait(new Callable<Boolean>() {
+ @Override public Boolean call() {
+ return !file.exists();
+ }
+ }, file, timeoutMs);
+ }
+
+ /**
+ * Releases and deletes the lock file.
+ */
+ void release() throws IOException {
+ Log.i(TAG, "Deleting lock file: " + mFile);
+ mFileLock.release();
+ mOutputStream.close();
+ if (!mFile.delete()) {
+ throw new IOException("Failed to delete lock file: " + mFile);
+ }
+ }
+}
diff --git a/third_party/libwebrtc/build/android/incremental_install/java/org/chromium/incrementalinstall/Reflect.java b/third_party/libwebrtc/build/android/incremental_install/java/org/chromium/incrementalinstall/Reflect.java
new file mode 100644
index 0000000000..c64dc1e8a3
--- /dev/null
+++ b/third_party/libwebrtc/build/android/incremental_install/java/org/chromium/incrementalinstall/Reflect.java
@@ -0,0 +1,142 @@
+// Copyright 2015 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.incrementalinstall;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+
+/**
+ * Reflection helper methods.
+ */
+final class Reflect {
+ /**
+ * Sets the value of an object's field (even if it's not visible).
+ *
+ * @param instance The object containing the field to set.
+ * @param name The name of the field to set.
+ * @param value The new value for the field.
+ */
+ static void setField(Object instance, String name, Object value)
+ throws ReflectiveOperationException {
+ Field field = findField(instance, name);
+ field.setAccessible(true);
+ field.set(instance, value);
+ }
+
+ /**
+ * Retrieves the value of an object's field (even if it's not visible).
+ *
+ * @param instance The object containing the field to set.
+ * @param name The name of the field to set.
+ * @return The field's value. Primitive values are returned as their boxed
+ * type.
+ */
+ static Object getField(Object instance, String name) throws ReflectiveOperationException {
+ Field field = findField(instance, name);
+ field.setAccessible(true);
+ return field.get(instance);
+ }
+
+ /**
+ * Concatenates two arrays into a new array. The arrays must be of the same
+ * type.
+ */
+ static Object[] concatArrays(Object[] arrType, Object[] left, Object[] right) {
+ Object[] result = (Object[]) Array.newInstance(
+ arrType.getClass().getComponentType(), left.length + right.length);
+ System.arraycopy(left, 0, result, 0, left.length);
+ System.arraycopy(right, 0, result, left.length, right.length);
+ return result;
+ }
+
+ /**
+ * Invokes a method with zero or more parameters. For static methods, use the Class as the
+ * instance.
+ */
+ static Object invokeMethod(Object instance, String name, Object... params)
+ throws ReflectiveOperationException {
+ boolean isStatic = instance instanceof Class;
+ Class<?> clazz = isStatic ? (Class<?>) instance : instance.getClass();
+ Method method = findMethod(clazz, name, params);
+ method.setAccessible(true);
+ return method.invoke(instance, params);
+ }
+
+ /**
+ * Calls a constructor with zero or more parameters.
+ */
+ static Object newInstance(Class<?> clazz, Object... params)
+ throws ReflectiveOperationException {
+ Constructor<?> constructor = findConstructor(clazz, params);
+ constructor.setAccessible(true);
+ return constructor.newInstance(params);
+ }
+
+ private static Field findField(Object instance, String name) throws NoSuchFieldException {
+ boolean isStatic = instance instanceof Class;
+ Class<?> clazz = isStatic ? (Class<?>) instance : instance.getClass();
+ for (; clazz != null; clazz = clazz.getSuperclass()) {
+ try {
+ return clazz.getDeclaredField(name);
+ } catch (NoSuchFieldException e) {
+ // Need to look in the super class.
+ }
+ }
+ throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
+ }
+
+ private static Method findMethod(Class<?> clazz, String name, Object... params)
+ throws NoSuchMethodException {
+ for (; clazz != null; clazz = clazz.getSuperclass()) {
+ for (Method method : clazz.getDeclaredMethods()) {
+ if (method.getName().equals(name)
+ && areParametersCompatible(method.getParameterTypes(), params)) {
+ return method;
+ }
+ }
+ }
+ throw new NoSuchMethodException("Method " + name + " with parameters "
+ + Arrays.asList(params) + " not found in " + clazz);
+ }
+
+ private static Constructor<?> findConstructor(Class<?> clazz, Object... params)
+ throws NoSuchMethodException {
+ for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
+ if (areParametersCompatible(constructor.getParameterTypes(), params)) {
+ return constructor;
+ }
+ }
+ throw new NoSuchMethodException("Constructor with parameters " + Arrays.asList(params)
+ + " not found in " + clazz);
+ }
+
+ private static boolean areParametersCompatible(Class<?>[] paramTypes, Object... params) {
+ if (params.length != paramTypes.length) {
+ return false;
+ }
+ for (int i = 0; i < params.length; i++) {
+ if (!isAssignableFrom(paramTypes[i], params[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static boolean isAssignableFrom(Class<?> left, Object right) {
+ if (right == null) {
+ return !left.isPrimitive();
+ }
+ Class<?> rightClazz = right.getClass();
+ if (left.isPrimitive()) {
+ // TODO(agrieve): Fill in the rest as needed.
+ return left == boolean.class && rightClazz == Boolean.class
+ || left == int.class && rightClazz == Integer.class;
+ }
+ return left.isAssignableFrom(rightClazz);
+ }
+}
diff --git a/third_party/libwebrtc/build/android/incremental_install/java/org/chromium/incrementalinstall/SecondInstrumentation.java b/third_party/libwebrtc/build/android/incremental_install/java/org/chromium/incrementalinstall/SecondInstrumentation.java
new file mode 100644
index 0000000000..3e0df0521e
--- /dev/null
+++ b/third_party/libwebrtc/build/android/incremental_install/java/org/chromium/incrementalinstall/SecondInstrumentation.java
@@ -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.
+
+package org.chromium.incrementalinstall;
+
+import android.app.Instrumentation;
+
+/**
+ * Exists to support an app having multiple instrumentations.
+ */
+public final class SecondInstrumentation extends Instrumentation {}
diff --git a/third_party/libwebrtc/build/android/incremental_install/write_installer_json.py b/third_party/libwebrtc/build/android/incremental_install/write_installer_json.py
new file mode 100755
index 0000000000..ce88e8a036
--- /dev/null
+++ b/third_party/libwebrtc/build/android/incremental_install/write_installer_json.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+
+# 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.
+
+"""Writes a .json file with the per-apk details for an incremental install."""
+
+import argparse
+import json
+import os
+import sys
+
+sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir, 'gyp'))
+
+from util import build_utils
+
+
+def _ParseArgs(args):
+ args = build_utils.ExpandFileArgs(args)
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--output-path',
+ help='Output path for .json file.',
+ required=True)
+ parser.add_argument('--apk-path',
+ help='Path to .apk relative to output directory.',
+ required=True)
+ parser.add_argument('--split',
+ action='append',
+ dest='split_globs',
+ default=[],
+ help='A glob matching the apk splits. '
+ 'Can be specified multiple times.')
+ parser.add_argument(
+ '--native-libs',
+ action='append',
+ help='GN-list of paths to native libraries relative to '
+ 'output directory. Can be repeated.')
+ parser.add_argument(
+ '--dex-files', help='GN-list of dex paths relative to output directory.')
+ parser.add_argument('--show-proguard-warning',
+ action='store_true',
+ default=False,
+ help='Print a warning about proguard being disabled')
+
+ options = parser.parse_args(args)
+ options.dex_files = build_utils.ParseGnList(options.dex_files)
+ options.native_libs = build_utils.ParseGnList(options.native_libs)
+ return options
+
+
+def main(args):
+ options = _ParseArgs(args)
+
+ data = {
+ 'apk_path': options.apk_path,
+ 'native_libs': options.native_libs,
+ 'dex_files': options.dex_files,
+ 'show_proguard_warning': options.show_proguard_warning,
+ 'split_globs': options.split_globs,
+ }
+
+ with build_utils.AtomicOutput(options.output_path, mode='w+') as f:
+ json.dump(data, f, indent=2, sort_keys=True)
+
+
+if __name__ == '__main__':
+ main(sys.argv[1:])
diff --git a/third_party/libwebrtc/build/android/incremental_install/write_installer_json.pydeps b/third_party/libwebrtc/build/android/incremental_install/write_installer_json.pydeps
new file mode 100644
index 0000000000..11a263f4a8
--- /dev/null
+++ b/third_party/libwebrtc/build/android/incremental_install/write_installer_json.pydeps
@@ -0,0 +1,6 @@
+# Generated by running:
+# build/print_python_deps.py --root build/android/incremental_install --output build/android/incremental_install/write_installer_json.pydeps build/android/incremental_install/write_installer_json.py
+../../gn_helpers.py
+../gyp/util/__init__.py
+../gyp/util/build_utils.py
+write_installer_json.py