blob: ff2c2281dddd7a84cbecb1671f01aabb263d3195 [file] [log] [blame] [view]
Andrew Grievef6069feb2021-04-29 18:29:171# App Bundles and Dynamic Feature Modules (DFMs)
Tibor Goldschwendt19364ba2019-04-10 15:59:552
3[TOC]
4
Andrew Grievef6069feb2021-04-29 18:29:175## About Bundles
6[Android App bundles] is a Play Store feature that allows packaging an app as
7multiple `.apk` files, known as "splits". Bundles are zip files with an `.aab`
8extension. See [android_build_instructions.md#multiple-chrome-targets] for a
9list of buildable bundle targets.
10
11Bundles provide three main advantages over monolithic `.apk` files:
121. Language resources are split into language-specific `.apk` files, known as
13 "resource splits". Delivering only the active languages reduces the overhead
14 of UI strings.
Andrew Grieve7e777abf2021-08-17 19:43:5915 * Resource splits can also be made on a per-screen-density basis (for drawables),
16 but Chrome has not taken advantage of this (yet).
Andrew Grievef6069feb2021-04-29 18:29:17172. Features can be packaged into lazily loaded `.apk` files, known as
18 "feature splits". Feature splits have no performance overhead until used.
19 * Except on versions prior to Android O, where support for
20 [android:isolatedSplits] was added. On prior versions, all installed splits
21 are loaded on application launch.
22 * E.g.: The `chrome` feature split makes renderers more efficient by having
23 them not load Java code that they don't need.
24 * E.g.: The `image_editor` feature split defers loading of Share-related code
25 until a Share action is performed.
26 * See also: [go/isolated-splits-dev-guide] (Googlers only).
273. Feature splits can be downloaded on-demand, saving disk space for users that
28 do not need the functionality they provide. These are known as
29 "Dynamic feature modules", or "DFMs".
30 * E.g. Chrome's VR support is packaged in this way, via the `vr` module.
31
32You can inspect which `.apk` files are produced by a bundle target via:
33```
34out/Default/bin/${target_name} build-bundle-apks --output-apks foo.apks
35unzip -l foo.apks
36```
37
Andrew Grieve23c828d2021-05-13 19:37:2938*** note
39Adding new features vis feature splits is highly encouraged when it makes sense
40to do so:
41 * Has a non-trivial amount of Dex (>50kb)
42 * Not needed on startup
Andrew Grieve7e777abf2021-08-17 19:43:5943 * Has a small integration surface (calls into it must be done with reflection)
44 * Not used by WebView (WebView does not support DFMs)
Andrew Grieve23c828d2021-05-13 19:37:2945***
46
Andrew Grievef6069feb2021-04-29 18:29:1747[android_build_instructions.md#multiple-chrome-targets]: android_build_instructions.md#multiple-chrome-targets
48[Android App Bundles]: https://developer.android.com/guide/app-bundle
49[android:isolatedSplits]: https://developer.android.com/reference/android/R.attr#isolatedSplits
50[go/isolated-splits-dev-guide]: http://go/isolated-splits-dev-guide
Tibor Goldschwendt19364ba2019-04-10 15:59:5551
Andrew Grieve7e777abf2021-08-17 19:43:5952### Declaring App Bundles with GN Templates
Tibor Goldschwendt19364ba2019-04-10 15:59:5553
Andrew Grieve7e777abf2021-08-17 19:43:5954Here's an example that shows how to declare a simple bundle that contains a
55single base module, which enables language-based splits:
Tibor Goldschwendt19364ba2019-04-10 15:59:5556
Andrew Grieve7e777abf2021-08-17 19:43:5957```gn
58 android_app_bundle_module("foo_base_module") {
59 # Declaration are similar to android_apk here.
60 ...
61 }
Tibor Goldschwendt19364ba2019-04-10 15:59:5562
Andrew Grieve7e777abf2021-08-17 19:43:5963 android_app_bundle("foo_bundle") {
64 base_module_target = ":foo_base_module"
65
66 # The name of our bundle file (without any suffix).
67 bundle_name = "FooBundle"
68
69 # Enable language-based splits for this bundle. Which means that
70 # resources and assets specific to a given language will be placed
71 # into their own split APK in the final .apks archive.
72 enable_language_splits = true
73
74 # Proguard settings must be passed at the bundle, not module, target.
75 proguard_enabled = !is_java_debug
76 }
77```
78
79When generating the `foo_bundle` target with Ninja, you will end up with
80the following:
81
82 * The bundle file under `out/Release/apks/FooBundle.aab`
83
84 * A helper script called `out/Release/bin/foo_bundle`, which can be used
85 to install / launch / uninstall the bundle on local devices.
86
87 This works like an APK wrapper script (e.g. `foo_apk`). Use `--help`
88 to see all possible commands supported by the script.
89
90
91The remainder of this doc focuses on DFMs.
92
93## Declaring Dynamic Feature Modules (DFMs)
Tibor Goldschwendt19364ba2019-04-10 15:59:5594
95This guide walks you through the steps to create a DFM called _Foo_ and add it
Tibor Goldschwendtaef8e392019-07-19 16:39:1096to the Chrome bundles.
Tibor Goldschwendt19364ba2019-04-10 15:59:5597
98*** note
99**Note:** To make your own module you'll essentially have to replace every
100instance of `foo`/`Foo`/`FOO` with `your_feature_name`/`YourFeatureName`/
101`YOUR_FEATURE_NAME`.
102***
103
Christopher Grantf649d282020-01-09 22:56:08104### Reference DFM
105
106In addition to this guide, the
107[Test Dummy](https://cs.chromium.org/chromium/src/chrome/android/modules/test_dummy/test_dummy_module.gni)
108module serves as an actively-maintained reference DFM. Test Dummy is used in
109automated bundle testing, and covers both Java and native code and resource
110usage.
Tibor Goldschwendt19364ba2019-04-10 15:59:55111
112### Create DFM target
113
114DFMs are APKs. They have a manifest and can contain Java and native code as well
115as resources. This section walks you through creating the module target in our
116build system.
117
Tibor Goldschwendt68c5f722019-08-01 15:10:15118First, create the file
Henrique Nakashimacfdcce32020-04-24 22:19:36119`//chrome/android/modules/foo/internal/java/AndroidManifest.xml` and add:
Tibor Goldschwendt19364ba2019-04-10 15:59:55120
121```xml
Tibor Goldschwendt68c5f722019-08-01 15:10:15122<?xml version="1.0" encoding="utf-8"?>
Tibor Goldschwendt19364ba2019-04-10 15:59:55123<manifest xmlns:android="http://schemas.android.com/apk/res/android"
124 xmlns:dist="http://schemas.android.com/apk/distribution"
Tibor Goldschwendt5172118f2019-06-24 21:57:47125 featureSplit="foo">
Tibor Goldschwendt19364ba2019-04-10 15:59:55126
Tibor Goldschwendt19364ba2019-04-10 15:59:55127 <!-- dist:onDemand="true" makes this a separately installed module.
128 dist:onDemand="false" would always install the module alongside the
129 rest of Chrome. -->
130 <dist:module
131 dist:onDemand="true"
132 dist:title="@string/foo_module_title">
Ben Masone571ea5a2019-09-06 18:29:37133 <!-- This will fuse the module into the base APK if a system image
134 APK is built from this bundle. -->
135 <dist:fusing dist:include="true" />
Tibor Goldschwendt19364ba2019-04-10 15:59:55136 </dist:module>
137
Samuel Huang39c7db632019-05-15 14:57:18138 <!-- Remove android:hasCode="false" when adding Java code. -->
139 <application android:hasCode="false" />
Tibor Goldschwendt19364ba2019-04-10 15:59:55140</manifest>
141```
142
Tibor Goldschwendtaef8e392019-07-19 16:39:10143Next, create a descriptor configuring the Foo module. To do this, create
Henrique Nakashimacfdcce32020-04-24 22:19:36144`//chrome/android/modules/foo/foo_module.gni` and add the following:
Tibor Goldschwendt19364ba2019-04-10 15:59:55145
146```gn
Tibor Goldschwendtaef8e392019-07-19 16:39:10147foo_module_desc = {
148 name = "foo"
Tibor Goldschwendt68c5f722019-08-01 15:10:15149 android_manifest =
Henrique Nakashimacfdcce32020-04-24 22:19:36150 "//chrome/android/modules/foo/internal/java/AndroidManifest.xml"
Tibor Goldschwendt19364ba2019-04-10 15:59:55151}
152```
153
Tibor Goldschwendtaef8e392019-07-19 16:39:10154Then, add the module descriptor to the appropriate descriptor list in
155//chrome/android/modules/chrome_feature_modules.gni, e.g. the Chrome Modern
156list:
Tibor Goldschwendt19364ba2019-04-10 15:59:55157
158```gn
Henrique Nakashimacfdcce32020-04-24 22:19:36159import("//chrome/android/modules/foo/foo_module.gni")
Tibor Goldschwendt19364ba2019-04-10 15:59:55160...
Tibor Goldschwendtaef8e392019-07-19 16:39:10161chrome_modern_module_descs += [ foo_module_desc ]
Tibor Goldschwendt19364ba2019-04-10 15:59:55162```
163
164The next step is to add Foo to the list of feature modules for UMA recording.
165For this, add `foo` to the `AndroidFeatureModuleName` in
166`//tools/metrics/histograms/histograms.xml`:
167
168```xml
169<histogram_suffixes name="AndroidFeatureModuleName" ...>
170 ...
171 <suffix name="foo" label="Super Duper Foo Module" />
172 ...
173</histogram_suffixes>
174```
175
Tibor Goldschwendtf430b272019-11-25 19:19:41176See [below](#metrics) for what metrics will be automatically collected after
177this step.
178
Tibor Goldschwendt19364ba2019-04-10 15:59:55179<!--- TODO(tiborg): Add info about install UI. -->
180Lastly, give your module a title that Chrome and Play can use for the install
181UI. To do this, add a string to
Adam Langley891ea2b2020-04-10 17:11:18182`//chrome/browser/ui/android/strings/android_chrome_strings.grd`:
Tibor Goldschwendt19364ba2019-04-10 15:59:55183
184```xml
185...
186<message name="IDS_FOO_MODULE_TITLE"
187 desc="Text shown when the Foo module is referenced in install start, success,
188 failure UI (e.g. in IDS_MODULE_INSTALL_START_TEXT, which will expand to
189 'Installing Foo for Chrome…').">
190 Foo
191</message>
192...
193```
194
Samuel Huang7f2b53752019-05-23 15:10:05195*** note
196**Note:** This is for module title only. Other strings specific to the module
197should go in the module, not here (in the base module).
198***
199
Tibor Goldschwendt19364ba2019-04-10 15:59:55200Congrats! You added the DFM Foo to Monochrome. That is a big step but not very
201useful so far. In the next sections you'll learn how to add code and resources
202to it.
203
204
205### Building and installing modules
206
207Before we are going to jump into adding content to Foo, let's take a look on how
208to build and deploy the Monochrome bundle with the Foo DFM. The remainder of
209this guide assumes the environment variable `OUTDIR` is set to a properly
210configured GN build directory (e.g. `out/Debug`).
211
212To build and install the Monochrome bundle to your connected device, run:
213
214```shell
215$ autoninja -C $OUTDIR monochrome_public_bundle
216$ $OUTDIR/bin/monochrome_public_bundle install -m base -m foo
217```
218
219This will install Foo alongside the rest of Chrome. The rest of Chrome is called
Tibor Goldschwendtf430b272019-11-25 19:19:41220_base_ module in the bundle world. The base module will always be put on the
Tibor Goldschwendt19364ba2019-04-10 15:59:55221device when initially installing Chrome.
222
223*** note
Tibor Goldschwendtf430b272019-11-25 19:19:41224**Note:** The install script may install more modules than you specify, e.g.
225when there are default or conditionally installed modules (see
226[below](#conditional-install) for details).
Tibor Goldschwendt19364ba2019-04-10 15:59:55227***
228
229You can then check that the install worked with:
230
231```shell
232$ adb shell dumpsys package org.chromium.chrome | grep splits
233> splits=[base, config.en, foo]
234```
235
236Then try installing the Monochrome bundle without your module and print the
237installed modules:
238
239```shell
240$ $OUTDIR/bin/monochrome_public_bundle install -m base
241$ adb shell dumpsys package org.chromium.chrome | grep splits
242> splits=[base, config.en]
243```
244
Andrew Grieve7e777abf2021-08-17 19:43:59245*** note
246The wrapper script's `install` command does approximately:
247```sh
248java -jar third_party/android_build_tools/bundletool/bundletool-all-$VERSION.jar build-apks --output tmp.apks ...
249java -jar third_party/android_build_tools/bundletool/bundletool-all-$VERSION.jar install-apks --apks tmp.apks
250```
251
252The `install-apks` command uses `adb install-multiple` under-the-hood.
253***
Tibor Goldschwendt19364ba2019-04-10 15:59:55254
Samuel Huang3dc9fce82020-02-26 18:09:57255### Adding Java code
Tibor Goldschwendt19364ba2019-04-10 15:59:55256
257To make Foo useful, let's add some Java code to it. This section will walk you
258through the required steps.
259
Tibor Goldschwendt573cf3022019-05-10 17:23:30260First, define a module interface for Foo. This is accomplished by adding the
261`@ModuleInterface` annotation to the Foo interface. This annotation
262automatically creates a `FooModule` class that can be used later to install and
263access the module. To do this, add the following in the new file
Henrique Nakashimacfdcce32020-04-24 22:19:36264`//chrome/browser/foo/android/java/src/org/chromium/chrome/browser/foo/Foo.java`:
Tibor Goldschwendt19364ba2019-04-10 15:59:55265
266```java
Henrique Nakashimacfdcce32020-04-24 22:19:36267package org.chromium.chrome.browser.foo;
Tibor Goldschwendt19364ba2019-04-10 15:59:55268
Fred Mello2623e052019-10-02 20:18:04269import org.chromium.components.module_installer.builder.ModuleInterface;
Tibor Goldschwendt573cf3022019-05-10 17:23:30270
Tibor Goldschwendt19364ba2019-04-10 15:59:55271/** Interface to call into Foo feature. */
Henrique Nakashimacfdcce32020-04-24 22:19:36272@ModuleInterface(module = "foo", impl = "org.chromium.chrome.browser.FooImpl")
Tibor Goldschwendt19364ba2019-04-10 15:59:55273public interface Foo {
274 /** Magical function. */
275 void bar();
276}
277```
278
Tibor Goldschwendt19364ba2019-04-10 15:59:55279Next, define an implementation that goes into the module in the new file
Henrique Nakashimacfdcce32020-04-24 22:19:36280`//chrome/browser/foo/internal/android/java/src/org/chromium/chrome/browser/foo/FooImpl.java`:
Tibor Goldschwendt19364ba2019-04-10 15:59:55281
282```java
Henrique Nakashimacfdcce32020-04-24 22:19:36283package org.chromium.chrome.browser.foo;
Tibor Goldschwendt19364ba2019-04-10 15:59:55284
285import org.chromium.base.Log;
Tibor Goldschwendt573cf3022019-05-10 17:23:30286import org.chromium.base.annotations.UsedByReflection;
Tibor Goldschwendt19364ba2019-04-10 15:59:55287
Tibor Goldschwendt573cf3022019-05-10 17:23:30288@UsedByReflection("FooModule")
Tibor Goldschwendt19364ba2019-04-10 15:59:55289public class FooImpl implements Foo {
290 @Override
291 public void bar() {
292 Log.i("FOO", "bar in module");
293 }
294}
295```
296
Tibor Goldschwendt19364ba2019-04-10 15:59:55297You can then use this provider to access the module if it is installed. To test
298that, instantiate Foo and call `bar()` somewhere in Chrome:
299
300```java
Tibor Goldschwendt573cf3022019-05-10 17:23:30301if (FooModule.isInstalled()) {
302 FooModule.getImpl().bar();
Tibor Goldschwendt19364ba2019-04-10 15:59:55303} else {
304 Log.i("FOO", "module not installed");
305}
306```
307
Tibor Goldschwendt573cf3022019-05-10 17:23:30308The interface has to be available regardless of whether the Foo DFM is present.
Henrique Nakashimacfdcce32020-04-24 22:19:36309Therefore, put those classes into the base module, creating a new public
310build target in: `//chrome/browser/foo/BUILD.gn`:
Tibor Goldschwendt19364ba2019-04-10 15:59:55311
312```gn
Henrique Nakashimacfdcce32020-04-24 22:19:36313import("//build/config/android/rules.gni")
314
315android_library("java") {
316 sources = [
317 "android/java/src/org/chromium/chrome/browser/foo/Foo.java",
318 ]
Mohamed Heikal826b4d52021-06-25 18:13:57319 deps = [
320 "//components/module_installer/android:module_installer_java",
321 "//components/module_installer/android:module_interface_java",
322 ]
323 annotation_processor_deps =
324 [ "//components/module_installer/android:module_interface_processor" ]
Henrique Nakashimacfdcce32020-04-24 22:19:36325}
Tibor Goldschwendt19364ba2019-04-10 15:59:55326```
327
Henrique Nakashimacfdcce32020-04-24 22:19:36328Then, depend on this target from where it is used as usual. For example, if the
329caller is in `chrome_java in //chrome/android/BUILD.gn`:
Tibor Goldschwendt19364ba2019-04-10 15:59:55330
331```gn
332...
Tibor Goldschwendt19364ba2019-04-10 15:59:55333android_library("chrome_java") {
Henrique Nakashimacfdcce32020-04-24 22:19:36334 deps =[
335 ...
336 "//chrome/browser/foo:java",
337 ...
338 ]
Tibor Goldschwendt19364ba2019-04-10 15:59:55339}
340...
341```
342
343The actual implementation, however, should go into the Foo DFM. For this
Henrique Nakashimacfdcce32020-04-24 22:19:36344purpose, create a new file `//chrome/browser/foo/internal/BUILD.gn` and
Tibor Goldschwendt68c5f722019-08-01 15:10:15345make a library with the module Java code in it:
Tibor Goldschwendt19364ba2019-04-10 15:59:55346
347```gn
348import("//build/config/android/rules.gni")
349
350android_library("java") {
351 # Define like ordinary Java Android library.
Natalie Chouinardcbdc6dc2019-12-24 00:02:35352 sources = [
Henrique Nakashimacfdcce32020-04-24 22:19:36353 "android/java/src/org/chromium/chrome/browser/foo/FooImpl.java",
Tibor Goldschwendt19364ba2019-04-10 15:59:55354 # Add other Java classes that should go into the Foo DFM here.
355 ]
Fred Mellob32b3022019-06-21 18:10:11356 deps = [
Tibor Goldschwendt19364ba2019-04-10 15:59:55357 "//base:base_java",
Henrique Nakashimacfdcce32020-04-24 22:19:36358 # Put other Chrome libs into the classpath so that you can call into them
359 # from the Foo DFM.
360 "//chrome/browser/bar:java",
361 # The module can depend even on `chrome_java` due to factory magic, but this
362 # is discouraged. Consider passing a delegate interface in instead.
Tibor Goldschwendt19364ba2019-04-10 15:59:55363 "//chrome/android:chrome_java",
Tibor Goldschwendt19364ba2019-04-10 15:59:55364 # Also, you'll need to depend on any //third_party or //components code you
365 # are using in the module code.
366 ]
367}
368```
369
Tibor Goldschwendtaef8e392019-07-19 16:39:10370Then, add this new library as a dependency of the Foo module descriptor in
Henrique Nakashimacfdcce32020-04-24 22:19:36371`//chrome/android/modules/foo/foo_module.gni`:
Tibor Goldschwendt19364ba2019-04-10 15:59:55372
373```gn
Tibor Goldschwendtaef8e392019-07-19 16:39:10374foo_module_desc = {
Tibor Goldschwendt19364ba2019-04-10 15:59:55375 ...
Tibor Goldschwendtaef8e392019-07-19 16:39:10376 java_deps = [
Henrique Nakashimacfdcce32020-04-24 22:19:36377 "//chrome/browser/foo/internal:java",
Tibor Goldschwendt19364ba2019-04-10 15:59:55378 ]
379}
380```
381
382Finally, tell Android that your module is now containing code. Do that by
Samuel Huang39c7db632019-05-15 14:57:18383removing the `android:hasCode="false"` attribute from the `<application>` tag in
Henrique Nakashimacfdcce32020-04-24 22:19:36384`//chrome/android/modules/foo/internal/java/AndroidManifest.xml`. You should be
Tibor Goldschwendt68c5f722019-08-01 15:10:15385left with an empty tag like so:
Tibor Goldschwendt19364ba2019-04-10 15:59:55386
387```xml
388...
389 <application />
390...
391```
392
393Rebuild and install `monochrome_public_bundle`. Start Chrome and run through a
394flow that tries to executes `bar()`. Depending on whether you installed your
395module (`-m foo`) "`bar in module`" or "`module not installed`" is printed to
396logcat. Yay!
397
Christopher Grantf649d282020-01-09 22:56:08398### Adding pre-built native libraries
Tibor Goldschwendt19364ba2019-04-10 15:59:55399
Christopher Grant8fea5a12019-07-31 19:12:31400You can add a third-party native library (or any standalone library that doesn't
401depend on Chrome code) by adding it as a loadable module to the module descriptor in
Henrique Nakashimacfdcce32020-04-24 22:19:36402`//chrome/android/moduiles/foo/foo_module.gni`:
Tibor Goldschwendt19364ba2019-04-10 15:59:55403
404```gn
Tibor Goldschwendtaef8e392019-07-19 16:39:10405foo_module_desc = {
Tibor Goldschwendt19364ba2019-04-10 15:59:55406 ...
Tibor Goldschwendtaef8e392019-07-19 16:39:10407 loadable_modules_32_bit = [ "//path/to/32/bit/lib.so" ]
408 loadable_modules_64_bit = [ "//path/to/64/bit/lib.so" ]
Tibor Goldschwendt19364ba2019-04-10 15:59:55409}
410```
411
Christopher Grant8fea5a12019-07-31 19:12:31412### Adding Chrome native code
Tibor Goldschwendt19364ba2019-04-10 15:59:55413
Christopher Grantf649d282020-01-09 22:56:08414Chrome native code may be placed in a DFM. The easiest way to access native
415feature code is by calling it from Java via JNI. When a module is first
416accessed, its native library (or potentially libraries, if using a component
417build), are automatically opened by the DFM framework, and a feature-specific
418JNI method (supplied by the feature's implementation) is invoked. Hence, a
419module's Java code may freely use JNI to call module native code.
Christopher Grant8fea5a12019-07-31 19:12:31420
Christopher Grantf649d282020-01-09 22:56:08421Using the module framework and JNI to access the native code eliminates concerns
422with DFM library file names (which vary across build variants),
423`android_dlopen_ext()` (needed to open feature libraries), and use of dlsym().
Christopher Grant8fea5a12019-07-31 19:12:31424
Christopher Grantf649d282020-01-09 22:56:08425This mechanism can be extended if necessary by DFM implementers to facilitate
426subsequent native-native calls, by having a JNI-called initialization method
427create instance of a object or factory, and register it through a call to the
428base module's native code (DFM native code can call base module code directly).
Christopher Grant8fea5a12019-07-31 19:12:31429
Eric Stevenson8c9ab26b2019-08-30 15:44:40430#### JNI
431
432Read the `jni_generator` [docs](../base/android/jni_generator/README.md) before
433reading this section.
434
435There are some subtleties to how JNI registration works with DFMs:
436
437* Generated wrapper `ClassNameJni` classes are packaged into the DFM's dex file
438* The class containing the actual native definitions, `GEN_JNI.java`, is always
439 stored in the base module
Christopher Grantf649d282020-01-09 22:56:08440* If the DFM is only included in bundles that use [implicit JNI
441 registration](android_native_libraries.md#JNI-Native-Methods-Resolution) (i.e.
442 Monochrome and newer), then no extra consideration is necessary
Eric Stevenson8c9ab26b2019-08-30 15:44:40443* Otherwise, the DFM will need to provide a `generate_jni_registration` target
444 that will generate all of the native registration functions
445
Christopher Grantf649d282020-01-09 22:56:08446#### Calling DFM native code via JNI
447
448A linker-assisted partitioning system automates the placement of code into
449either the main Chrome library or feature-specific .so libraries. Feature code
450may continue to make use of core Chrome code (eg. base::) without modification,
451but Chrome must call feature code through a virtual interface (any "direct"
452calls to the feature code from the main library will cause the feature code to
453be pulled back into the main library).
454
455Partitioning is explained in [Android Native
456Libraries](android_native_libraries.md#partitioned-libraries).
457
458First, build a module native interface. Supply a JNI method named
459`JNI_OnLoad_foo` for the module framework to call, in
460`//chrome/android/modules/foo/internal/entrypoints.cc`. This method is invoked
461on all Chrome build variants, including Monochrome (unlike base module JNI).
462
463```c++
464#include "base/android/jni_generator/jni_generator_helper.h"
465#include "base/android/jni_utils.h"
466#include "chrome/android/modules/foo/internal/jni_registration.h"
467
468extern "C" {
469// This JNI registration method is found and called by module framework code.
470JNI_GENERATOR_EXPORT bool JNI_OnLoad_foo(JNIEnv* env) {
471 if (!base::android::IsSelectiveJniRegistrationEnabled(env) &&
472 !foo::RegisterNonMainDexNatives(env)) {
473 return false;
474 }
475 if (!foo::RegisterMainDexNatives(env)) {
476 return false;
477 }
478 return true;
479}
480} // extern "C"
481```
482
483Next, include the module entrypoint and related pieces in the build config at
484`//chrome/android/modules/foo/internal/BUILD.gn`:
485
486```gn
487import("//build/config/android/rules.gni")
488import("//chrome/android/modules/buildflags.gni")
489...
490
491# Put the JNI entrypoint in a component, so that the component build has a
492# library to include in the foo module. This makes things feel consistent with
493# a release build.
494component("foo") {
495 sources = [
496 "entrypoints.cc",
497 ]
498 deps = [
499 ":jni_registration",
Christopher Grantf649d282020-01-09 22:56:08500 "//base",
Henrique Nakashimacfdcce32020-04-24 22:19:36501 "//chrome/browser/foo/internal:native",
Christopher Grantf649d282020-01-09 22:56:08502 ]
503
504 # Instruct the compiler to flag exported entrypoint function as belonging in
505 # foo's library. The linker will use this information when creating the
506 # native libraries. The partition name must be <feature>_partition.
507 if (use_native_partitions) {
508 cflags = [ "-fsymbol-partition=foo_partition" ]
509 }
510}
511
512# Generate JNI registration for the methods called by the Java side. Note the
513# no_transitive_deps argument, which ensures that JNI is generated for only the
514# specified Java target, and not all its transitive deps (which could include
515# the base module).
516generate_jni_registration("jni_registration") {
Henrique Nakashimacfdcce32020-04-24 22:19:36517 targets = [ "//chrome/browser/foo/internal:java" ]
Christopher Grantf649d282020-01-09 22:56:08518 header_output = "$target_gen_dir/jni_registration.h"
519 namespace = "foo"
520 no_transitive_deps = true
521}
522
523# This group is a convenience alias representing the module's native code,
524# allowing it to be named "native" for clarity in module descriptors.
525group("native") {
526 deps = [
527 ":foo",
528 ]
529}
530```
531
532Now, over to the implementation of the module. These are the parts that
533shouldn't know or care whether they're living in a module or not.
534
535Add a stub implementation in
Henrique Nakashimacfdcce32020-04-24 22:19:36536`//chrome/browser/foo/internal/android/foo_impl.cc`:
Christopher Grantf649d282020-01-09 22:56:08537
538```c++
539#include "base/logging.h"
Henrique Nakashimacfdcce32020-04-24 22:19:36540#include "chrome/browser/foo/internal/jni_headers/FooImpl_jni.h"
Christopher Grantf649d282020-01-09 22:56:08541
542static int JNI_FooImpl_Execute(JNIEnv* env) {
543 LOG(INFO) << "Running foo feature code!";
544 return 123;
545}
546```
547
548And, the associated build config in
Henrique Nakashimacfdcce32020-04-24 22:19:36549`//chrome/browser/foo/internal/BUILD.gn`:
Christopher Grantf649d282020-01-09 22:56:08550
551```gn
552import("//build/config/android/rules.gni")
553
554...
555
556source_set("native") {
557 sources = [
Henrique Nakashimacfdcce32020-04-24 22:19:36558 "android/foo_impl.cc",
Christopher Grantf649d282020-01-09 22:56:08559 ]
560
561 deps = [
562 ":jni_headers",
563 "//base",
564 ]
565}
566
567generate_jni("jni_headers") {
568 sources = [
Henrique Nakashimacfdcce32020-04-24 22:19:36569 "android/java/src/org/chromium/chrome/browser/foo/FooImpl.java",
Christopher Grantf649d282020-01-09 22:56:08570 ]
571}
572```
573
574With a declaration of the native method on the Java side:
575
576```java
577public class FooImpl implements Foo {
578 ...
579
580 @NativeMethods
581 interface Natives {
582 int execute();
583 }
584}
585```
586
587Finally, augment the module descriptor in
588`//chrome/android/modules/foo/foo_module.gni` with the native dependencies:
589
590```gn
591foo_module_desc = {
592 ...
593 native_deps = [
Christopher Grantf649d282020-01-09 22:56:08594 "//chrome/android/modules/foo/internal:native",
Henrique Nakashimacfdcce32020-04-24 22:19:36595 "//chrome/browser/foo/internal:native",
Christopher Grantf649d282020-01-09 22:56:08596 ]
Samuel Huang3dc9fce82020-02-26 18:09:57597 load_native_on_get_impl = true
Christopher Grantf649d282020-01-09 22:56:08598}
599```
600
Samuel Huang3dc9fce82020-02-26 18:09:57601If `load_native_on_get_impl` is set to `true` then Chrome automatically loads
602Foo DFM's native libraries and PAK file resources when `FooModule.getImpl()` is
603called for the first time. The loading requires Chrome's main native libraries
604to be loaded. If you wish to call `FooModule.getImpl()` earlier than that, then
605you'd need to set `load_native_on_get_impl` to `false`, and manage native
606libraries / resources loading yourself (potentially, on start-up and on install,
607or on use).
608
Christopher Grantf649d282020-01-09 22:56:08609#### Calling feature module native code from base the module
610
611If planning to use direct native-native calls into DFM code, then the module
612should have a purely virtual interface available. The main module can obtain a
613pointer to a DFM-created object or factory (implemented by the feature), and
614call its virtual methods.
615
616Ideally, the interface to the feature will avoid feature-specific types. If a
617feature defines complex data types, and uses them in its own interface, then its
618likely the main library will utilize the code backing these types. That code,
619and anything it references, will in turn be pulled back into the main library,
620negating the intent to house code in the DFM.
621
622Therefore, designing the feature interface to use C types, C++ standard types,
623or classes that aren't expected to move out of Chrome's main library is ideal.
624If feature-specific classes are needed, they simply need to avoid referencing
625feature library internals.
Eric Stevenson8c9ab26b2019-08-30 15:44:40626
Christopher Grant8fea5a12019-07-31 19:12:31627### Adding Android resources
Tibor Goldschwendt19364ba2019-04-10 15:59:55628
629In this section we will add the required build targets to add Android resources
630to the Foo DFM.
631
Tibor Goldschwendt68c5f722019-08-01 15:10:15632First, add a resources target to
Henrique Nakashimacfdcce32020-04-24 22:19:36633`//chrome/browser/foo/internal/BUILD.gn` and add it as a dependency on
Tibor Goldschwendt68c5f722019-08-01 15:10:15634Foo's `java` target in the same file:
Tibor Goldschwendt19364ba2019-04-10 15:59:55635
636```gn
637...
638android_resources("java_resources") {
639 # Define like ordinary Android resources target.
640 ...
Henrique Nakashimacfdcce32020-04-24 22:19:36641 custom_package = "org.chromium.chrome.browser.foo"
Tibor Goldschwendt19364ba2019-04-10 15:59:55642}
643...
644android_library("java") {
645 ...
646 deps = [
647 ":java_resources",
648 ]
649}
650```
651
652To add strings follow steps
653[here](http://dev.chromium.org/developers/design-documents/ui-localization) to
654add new Java GRD file. Then create
Henrique Nakashimacfdcce32020-04-24 22:19:36655`//chrome/browser/foo/internal/android/resources/strings/android_foo_strings.grd` as
Tibor Goldschwendt68c5f722019-08-01 15:10:15656follows:
Tibor Goldschwendt19364ba2019-04-10 15:59:55657
658```xml
659<?xml version="1.0" encoding="UTF-8"?>
660<grit
661 current_release="1"
662 latest_public_release="0"
663 output_all_resource_defines="false">
664 <outputs>
665 <output
666 filename="values-am/android_foo_strings.xml"
667 lang="am"
668 type="android" />
669 <!-- List output file for all other supported languages. See
Henrique Nakashimaf5439f82021-01-29 23:52:24670 //chrome/browser/ui/android/strings/android_chrome_strings.grd for the
671 full list. -->
Tibor Goldschwendt19364ba2019-04-10 15:59:55672 ...
673 </outputs>
674 <translations>
675 <file lang="am" path="vr_translations/android_foo_strings_am.xtb" />
676 <!-- Here, too, list XTB files for all other supported languages. -->
677 ...
678 </translations>
Matt Stark1debb5de2021-02-15 16:08:24679 <release seq="1">
Tibor Goldschwendt19364ba2019-04-10 15:59:55680 <messages fallback_to_english="true">
681 <message name="IDS_BAR_IMPL_TEXT" desc="Magical string.">
682 impl
683 </message>
684 </messages>
685 </release>
686</grit>
687```
688
689Then, create a new GRD target and add it as a dependency on `java_resources` in
Henrique Nakashimacfdcce32020-04-24 22:19:36690`//chrome/browser/foo/internal/BUILD.gn`:
Tibor Goldschwendt19364ba2019-04-10 15:59:55691
692```gn
693...
694java_strings_grd("java_strings_grd") {
695 defines = chrome_grit_defines
Henrique Nakashimacfdcce32020-04-24 22:19:36696 grd_file = "android/resources/strings/android_foo_strings.grd"
Tibor Goldschwendt19364ba2019-04-10 15:59:55697 outputs = [
698 "values-am/android_foo_strings.xml",
699 # Here, too, list output files for other supported languages.
700 ...
701 ]
702}
703...
704android_resources("java_resources") {
705 ...
706 deps = [":java_strings_grd"]
Henrique Nakashimacfdcce32020-04-24 22:19:36707 custom_package = "org.chromium.chrome.browser.foo"
Tibor Goldschwendt19364ba2019-04-10 15:59:55708}
709...
710```
711
712You can then access Foo's resources using the
Henrique Nakashimacfdcce32020-04-24 22:19:36713`org.chromium.chrome.browser.foo.R` class. To do this change
714`//chrome/browser/foo/internal/android/java/src/org/chromium/chrome/browser/foo/FooImpl.java`
Tibor Goldschwendt19364ba2019-04-10 15:59:55715to:
716
717```java
Henrique Nakashimacfdcce32020-04-24 22:19:36718package org.chromium.chrome.browser.foo;
Tibor Goldschwendt19364ba2019-04-10 15:59:55719
720import org.chromium.base.ContextUtils;
721import org.chromium.base.Log;
Tibor Goldschwendt573cf3022019-05-10 17:23:30722import org.chromium.base.annotations.UsedByReflection;
Henrique Nakashimacfdcce32020-04-24 22:19:36723import org.chromium.chrome.browser.foo.R;
Tibor Goldschwendt19364ba2019-04-10 15:59:55724
Tibor Goldschwendt573cf3022019-05-10 17:23:30725@UsedByReflection("FooModule")
Tibor Goldschwendt19364ba2019-04-10 15:59:55726public class FooImpl implements Foo {
727 @Override
728 public void bar() {
729 Log.i("FOO", ContextUtils.getApplicationContext().getString(
730 R.string.bar_impl_text));
731 }
732}
733```
734
Samuel Huang3dc9fce82020-02-26 18:09:57735### Adding non-string native resources
736
737This section describes how to add non-string native resources to Foo DFM.
738Key ideas:
739
740* The compiled resource file shipped with the DFM is `foo_resourcess.pak`.
741* At run time, native resources need to be loaded before use. Also, DFM native
742 resources can only be used from the Browser process.
743
744#### Creating PAK file
745
746Two ways to create `foo_resourcess.pak` (using GRIT) are:
747
7481. (Preferred) Use `foo_resourcess.grd` to refer to individual files (e.g.,
749 images, HTML, JS, or CSS) and assigns resource text IDs. `foo_resourcess.pak`
750 must have an entry in `/tools/gritsettings/resource_ids.spec`.
7511. Combine existing .pak files via `repack` rules in GN build files. This is
752 done by the DevUI DFM, which aggregates resources from many DevUI pages.
753
754#### Loading PAK file
755
756At runtime, `foo_resources.pak` needs to be loaded (memory-mapped) before any of
757its resource gets used. Alternatives to do this are:
758
7591. (Simplest) Specify native resources (with native libraries if any exist) to
760 be automatically loaded on first call to `FooModule.getImpl()`. This behavior
761 is specified via `load_native_on_get_impl = true` in `foo_module_desc`.
7621. In Java code, call `FooModule.ensureNativeLoaded()`.
7631. In C++ code, use JNI to call `FooModule.ensureNativeLoaded()`. The code to do
764 this can be placed in a helper class, which can also have JNI calls to
765 `FooModule.isInstalled()` and `FooModule.installModule()`.
766
767#### Cautionary notes
768
769Compiling `foo_resources.pak` auto-generates `foo_resources.h`, which defines
770textual resource IDs, e.g., `IDR_FOO_HTML`. C++ code then uses these IDs to get
771resource bytes. Unfortunately, this behavior is fragile: If `IDR_FOO_HTML` is
772accessed before the Foo DFM is (a) installed, or (b) loaded, then runtime error
773ensues! Some mitigation strategies are as follows:
774
775* (Ideal) Access Foo DFM's native resources only from code in Foo DFM's native
776 libraries. So by the time that `IDR_FOO_HTML` is accessed, everything is
777 already in place! This isn't always possible; henceforth we assume that
778 `IDR_FOO_HTML` is accessed by code in the base DFM.
779* Before accessing IDR_FOO_HTML, ensure Foo DFM is installed and loaded. The
780 latter can use `FooModule.ensureNativeLoaded()` (needs to be called from
781 Browser thread).
782* Use inclusion of `foo_resources.h` to restrict availability of `IDR_FOO_HTML`.
783 Only C++ files dedicated to "DFM-gated code" (code that runs only when its DFM
784 is installed and loaded) should include `foo_resources.h`.
785
786#### Associating native resources with DFM
787
788Here are the main GN changes to specify PAK files and default loading behavior
789for a DFM's native resources:
790
791```gn
792foo_module_desc = {
793 ...
Henrique Nakashimacfdcce32020-04-24 22:19:36794 paks = [ "$root_gen_dir/chrome/browser/foo/internal/foo_resourcess.pak" ]
795 pak_deps = [ "//chrome/browser/foo/internal:foo_paks" ]
Samuel Huang3dc9fce82020-02-26 18:09:57796 load_native_on_get_impl = true
797}
798```
799
800Note that `load_native_on_get_impl` specifies both native libraries and native
801resources.
802
Tibor Goldschwendt19364ba2019-04-10 15:59:55803
804### Module install
805
806So far, we have installed the Foo DFM as a true split (`-m foo` option on the
807install script). In production, however, we have to explicitly install the Foo
Tibor Goldschwendt68c5f722019-08-01 15:10:15808DFM for users to get it. There are three install options: _on-demand_,
809_deferred_ and _conditional_.
Tibor Goldschwendt19364ba2019-04-10 15:59:55810
Tibor Goldschwendt19364ba2019-04-10 15:59:55811#### On-demand install
812
813On-demand requesting a module will try to download and install the
814module as soon as possible regardless of whether the user is on a metered
815connection or whether they have turned updates off in the Play Store app.
816
Tibor Goldschwendt573cf3022019-05-10 17:23:30817You can use the autogenerated module class to on-demand install the module like
818so:
Tibor Goldschwendt19364ba2019-04-10 15:59:55819
820```java
Tibor Goldschwendt573cf3022019-05-10 17:23:30821FooModule.install((success) -> {
822 if (success) {
823 FooModule.getImpl().bar();
824 }
Tibor Goldschwendt19364ba2019-04-10 15:59:55825});
826```
827
828**Optionally**, you can show UI telling the user about the install flow. For
Tibor Goldschwendt573cf3022019-05-10 17:23:30829this, add a function like the one below. Note, it is possible
Tibor Goldschwendt19364ba2019-04-10 15:59:55830to only show either one of the install, failure and success UI or any
831combination of the three.
832
833```java
834public static void installModuleWithUi(
835 Tab tab, OnModuleInstallFinishedListener onFinishedListener) {
836 ModuleInstallUi ui =
837 new ModuleInstallUi(
838 tab,
839 R.string.foo_module_title,
840 new ModuleInstallUi.FailureUiListener() {
841 @Override
Samuel Huangfebcccd2019-08-21 20:48:47842 public void onFailureUiResponse(retry) {
843 if (retry) {
844 installModuleWithUi(tab, onFinishedListener);
845 } else {
846 onFinishedListener.onFinished(false);
847 }
Tibor Goldschwendt19364ba2019-04-10 15:59:55848 }
849 });
850 // At the time of writing, shows toast informing user about install start.
851 ui.showInstallStartUi();
Tibor Goldschwendt573cf3022019-05-10 17:23:30852 FooModule.install(
Tibor Goldschwendt19364ba2019-04-10 15:59:55853 (success) -> {
854 if (!success) {
855 // At the time of writing, shows infobar allowing user
856 // to retry install.
857 ui.showInstallFailureUi();
858 return;
859 }
860 // At the time of writing, shows toast informing user about
861 // install success.
862 ui.showInstallSuccessUi();
863 onFinishedListener.onFinished(true);
864 });
865}
866```
867
868To test on-demand install, "fake-install" the DFM. It's fake because
Peter Wen8bf82d42021-08-13 22:03:54869the DFM is not installed as a true split. Instead it will be emulated by play
870core's `--local-testing` [mode][play-core-local-testing].
Tibor Goldschwendt19364ba2019-04-10 15:59:55871Fake-install and launch Chrome with the following command:
872
873```shell
874$ $OUTDIR/bin/monochrome_public_bundle install -m base -f foo
Peter Wen8bf82d42021-08-13 22:03:54875$ $OUTDIR/bin/monochrome_public_bundle launch
Tibor Goldschwendt19364ba2019-04-10 15:59:55876```
877
878When running the install code, the Foo DFM module will be emulated.
879This will be the case in production right after installing the module. Emulation
880will last until Play Store has a chance to install your module as a true split.
Peter Wen577a6fe52019-12-11 22:02:05881This usually takes about a day. After it has been installed, it will be updated
882atomically alongside Chrome. Always check that it is installed and available
883before invoking code within the DFM.
Tibor Goldschwendt19364ba2019-04-10 15:59:55884
885*** note
886**Warning:** There are subtle differences between emulating a module and
887installing it as a true split. We therefore recommend that you always test both
888install methods.
889***
890
Samuel Huang6f5c7ddb82020-05-14 17:10:52891*** note
892To simplify development, the DevUI DFM (dev_ui) is installed by default, i.e.,
893`-m dev_ui` is implied by default. This is overridden by:
894* `--no-module dev_ui`, to test error from missing DevUI,
895* `-f dev_ui`, for fake module install.
896***
Tibor Goldschwendt19364ba2019-04-10 15:59:55897
898#### Deferred install
899
900Deferred install means that the DFM is installed in the background when the
901device is on an unmetered connection and charging. The DFM will only be
902available after Chrome restarts. When deferred installing a module it will
903not be faked installed.
904
905To defer install Foo do the following:
906
907```java
Tibor Goldschwendt573cf3022019-05-10 17:23:30908FooModule.installDeferred();
Tibor Goldschwendt19364ba2019-04-10 15:59:55909```
910
Tibor Goldschwendt68c5f722019-08-01 15:10:15911#### Conditional install
912
913Conditional install means the DFM will be installed automatically upon first
914installing or updating Chrome if the device supports a particular feature.
915Conditional install is configured in the module's manifest. To install your
916module on all Daydream-ready devices for instance, your
Henrique Nakashimacfdcce32020-04-24 22:19:36917`//chrome/android/modules/foo/internal/java/AndroidManifest.xml` should look
Tibor Goldschwendt68c5f722019-08-01 15:10:15918like this:
919
920```xml
921<?xml version="1.0" encoding="utf-8"?>
922<manifest xmlns:android="http://schemas.android.com/apk/res/android"
923 xmlns:dist="http://schemas.android.com/apk/distribution"
924 featureSplit="foo">
925
926 <dist:module
927 dist:instant="false"
928 dist:title="@string/foo_module_title">
Ben Masone571ea5a2019-09-06 18:29:37929 <dist:fusing dist:include="true" />
Tibor Goldschwendt68c5f722019-08-01 15:10:15930 <dist:delivery>
931 <dist:install-time>
932 <dist:conditions>
933 <dist:device-feature
934 dist:name="android.hardware.vr.high_performance" />
935 </dist:conditions>
936 </dist:install-time>
937 <!-- Allows on-demand or deferred install on non-Daydream-ready
938 devices. -->
939 <dist:on-demand />
940 </dist:delivery>
941 </dist:module>
942
943 <application />
944</manifest>
945```
946
Andrew Grievef6069feb2021-04-29 18:29:17947You can also specify no conditions to have your module always installed.
948You might want to do this in order to delay the performance implications
949of loading your module until its first use (true only on Android O+ where
950[android:isolatedSplits](https://developer.android.com/reference/android/R.attr#isolatedSplits)
951is supported. See [go/isolated-splits-dev-guide](http://go/isolated-splits-dev-guide)
952(googlers only).
953
Tibor Goldschwendtf430b272019-11-25 19:19:41954### Metrics
955
956After adding your module to `AndroidFeatureModuleName` (see
957[above](#create-dfm-target)) we will collect, among others, the following
958metrics:
959
960* `Android.FeatureModules.AvailabilityStatus.Foo`: Measures your module's
961 install penetration. That is, the share of users who eventually installed
962 the module after requesting it (once or multiple times).
963
964* `Android.FeatureModules.InstallStatus.Foo`: The result of an on-demand
965 install request. Can be success or one of several error conditions.
966
967* `Android.FeatureModules.UncachedAwakeInstallDuration.Foo`: The duration to
968 install your module successfully after on-demand requesting it.
969
Tibor Goldschwendt19364ba2019-04-10 15:59:55970
Andrew Grievef6069feb2021-04-29 18:29:17971### chrome_public_apk and Integration Tests
Tibor Goldschwendt19364ba2019-04-10 15:59:55972
Andrew Grievef6069feb2021-04-29 18:29:17973To make the Foo feature available in the non-bundle `chrome_public_apk`
974target, add the `java` target to the `chrome_public_common_apk_or_module_tmpl`
975in `//chrome/android/chrome_public_apk_tmpl.gni` like so:
Tibor Goldschwendt19364ba2019-04-10 15:59:55976
977```gn
978template("chrome_public_common_apk_or_module_tmpl") {
979 ...
980 target(_target_type, target_name) {
981 ...
982 if (_target_type != "android_app_bundle_module") {
983 deps += [
Henrique Nakashimacfdcce32020-04-24 22:19:36984 "//chrome/browser/foo/internal:java",
Tibor Goldschwendt19364ba2019-04-10 15:59:55985 ]
986 }
987 }
988}
989```
990
Andrew Grievef6069feb2021-04-29 18:29:17991You may also have to add `java` as a dependency of `chrome_test_java` if you want
992to call into Foo from test code.
Peter Wen8bf82d42021-08-13 22:03:54993
994[play-core-local-testing]: https://developer.android.com/guide/playcore/feature-delivery/on-demand#local-testing