blob: ee6e050c21018646fa7ed7fe6f96a99a8f62f3dc [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
Andrew Grieve520e0902022-04-27 13:19:2518 "feature splits". Chrome enables [isolated splits], which means feature
19 splits have no performance overhead until used (on Android O+ at least).
Andrew Grievef6069feb2021-04-29 18:29:17203. Feature splits can be downloaded on-demand, saving disk space for users that
21 do not need the functionality they provide. These are known as
22 "Dynamic feature modules", or "DFMs".
23 * E.g. Chrome's VR support is packaged in this way, via the `vr` module.
24
25You can inspect which `.apk` files are produced by a bundle target via:
26```
27out/Default/bin/${target_name} build-bundle-apks --output-apks foo.apks
28unzip -l foo.apks
29```
30
Andrew Grieve23c828d2021-05-13 19:37:2931*** note
32Adding new features vis feature splits is highly encouraged when it makes sense
33to do so:
34 * Has a non-trivial amount of Dex (>50kb)
35 * Not needed on startup
Andrew Grieve7e777abf2021-08-17 19:43:5936 * Has a small integration surface (calls into it must be done with reflection)
37 * Not used by WebView (WebView does not support DFMs)
Andrew Grieve23c828d2021-05-13 19:37:2938***
39
Andrew Grievef6069feb2021-04-29 18:29:1740[android_build_instructions.md#multiple-chrome-targets]: android_build_instructions.md#multiple-chrome-targets
41[Android App Bundles]: https://developer.android.com/guide/app-bundle
Andrew Grieve520e0902022-04-27 13:19:2542[isolated splits]: android_isolated_splits.md
Tibor Goldschwendt19364ba2019-04-10 15:59:5543
Andrew Grieve7e777abf2021-08-17 19:43:5944### Declaring App Bundles with GN Templates
Tibor Goldschwendt19364ba2019-04-10 15:59:5545
Andrew Grieve7e777abf2021-08-17 19:43:5946Here's an example that shows how to declare a simple bundle that contains a
47single base module, which enables language-based splits:
Tibor Goldschwendt19364ba2019-04-10 15:59:5548
Andrew Grieve7e777abf2021-08-17 19:43:5949```gn
50 android_app_bundle_module("foo_base_module") {
51 # Declaration are similar to android_apk here.
52 ...
53 }
Tibor Goldschwendt19364ba2019-04-10 15:59:5554
Andrew Grieve7e777abf2021-08-17 19:43:5955 android_app_bundle("foo_bundle") {
56 base_module_target = ":foo_base_module"
57
58 # The name of our bundle file (without any suffix).
59 bundle_name = "FooBundle"
60
61 # Enable language-based splits for this bundle. Which means that
62 # resources and assets specific to a given language will be placed
63 # into their own split APK in the final .apks archive.
64 enable_language_splits = true
65
66 # Proguard settings must be passed at the bundle, not module, target.
67 proguard_enabled = !is_java_debug
68 }
69```
70
71When generating the `foo_bundle` target with Ninja, you will end up with
72the following:
73
74 * The bundle file under `out/Release/apks/FooBundle.aab`
75
76 * A helper script called `out/Release/bin/foo_bundle`, which can be used
77 to install / launch / uninstall the bundle on local devices.
78
79 This works like an APK wrapper script (e.g. `foo_apk`). Use `--help`
80 to see all possible commands supported by the script.
81
82
83The remainder of this doc focuses on DFMs.
84
85## Declaring Dynamic Feature Modules (DFMs)
Tibor Goldschwendt19364ba2019-04-10 15:59:5586
87This guide walks you through the steps to create a DFM called _Foo_ and add it
Tibor Goldschwendtaef8e392019-07-19 16:39:1088to the Chrome bundles.
Tibor Goldschwendt19364ba2019-04-10 15:59:5589
90*** note
91**Note:** To make your own module you'll essentially have to replace every
92instance of `foo`/`Foo`/`FOO` with `your_feature_name`/`YourFeatureName`/
93`YOUR_FEATURE_NAME`.
94***
95
Christopher Grantf649d282020-01-09 22:56:0896### Reference DFM
97
98In addition to this guide, the
99[Test Dummy](https://cs.chromium.org/chromium/src/chrome/android/modules/test_dummy/test_dummy_module.gni)
100module serves as an actively-maintained reference DFM. Test Dummy is used in
101automated bundle testing, and covers both Java and native code and resource
102usage.
Tibor Goldschwendt19364ba2019-04-10 15:59:55103
104### Create DFM target
105
106DFMs are APKs. They have a manifest and can contain Java and native code as well
107as resources. This section walks you through creating the module target in our
108build system.
109
Tibor Goldschwendt68c5f722019-08-01 15:10:15110First, create the file
Henrique Nakashimacfdcce32020-04-24 22:19:36111`//chrome/android/modules/foo/internal/java/AndroidManifest.xml` and add:
Tibor Goldschwendt19364ba2019-04-10 15:59:55112
113```xml
Tibor Goldschwendt68c5f722019-08-01 15:10:15114<?xml version="1.0" encoding="utf-8"?>
Tibor Goldschwendt19364ba2019-04-10 15:59:55115<manifest xmlns:android="http://schemas.android.com/apk/res/android"
116 xmlns:dist="http://schemas.android.com/apk/distribution"
Tibor Goldschwendt5172118f2019-06-24 21:57:47117 featureSplit="foo">
Tibor Goldschwendt19364ba2019-04-10 15:59:55118
Tibor Goldschwendt19364ba2019-04-10 15:59:55119 <!-- dist:onDemand="true" makes this a separately installed module.
120 dist:onDemand="false" would always install the module alongside the
121 rest of Chrome. -->
122 <dist:module
123 dist:onDemand="true"
124 dist:title="@string/foo_module_title">
Ben Masone571ea5a2019-09-06 18:29:37125 <!-- This will fuse the module into the base APK if a system image
126 APK is built from this bundle. -->
127 <dist:fusing dist:include="true" />
Tibor Goldschwendt19364ba2019-04-10 15:59:55128 </dist:module>
129
Samuel Huang39c7db632019-05-15 14:57:18130 <!-- Remove android:hasCode="false" when adding Java code. -->
131 <application android:hasCode="false" />
Tibor Goldschwendt19364ba2019-04-10 15:59:55132</manifest>
133```
134
Tibor Goldschwendtaef8e392019-07-19 16:39:10135Next, create a descriptor configuring the Foo module. To do this, create
Henrique Nakashimacfdcce32020-04-24 22:19:36136`//chrome/android/modules/foo/foo_module.gni` and add the following:
Tibor Goldschwendt19364ba2019-04-10 15:59:55137
138```gn
Tibor Goldschwendtaef8e392019-07-19 16:39:10139foo_module_desc = {
140 name = "foo"
Tibor Goldschwendt68c5f722019-08-01 15:10:15141 android_manifest =
Henrique Nakashimacfdcce32020-04-24 22:19:36142 "//chrome/android/modules/foo/internal/java/AndroidManifest.xml"
Tibor Goldschwendt19364ba2019-04-10 15:59:55143}
144```
145
Tibor Goldschwendtaef8e392019-07-19 16:39:10146Then, add the module descriptor to the appropriate descriptor list in
Andrew Grieve912f66332023-04-11 16:11:56147//chrome/android/modules/chrome_feature_modules.gni, e.g. the Chrome list:
Tibor Goldschwendt19364ba2019-04-10 15:59:55148
149```gn
Henrique Nakashimacfdcce32020-04-24 22:19:36150import("//chrome/android/modules/foo/foo_module.gni")
Tibor Goldschwendt19364ba2019-04-10 15:59:55151...
Andrew Grieve912f66332023-04-11 16:11:56152chrome_module_descs += [ foo_module_desc ]
Tibor Goldschwendt19364ba2019-04-10 15:59:55153```
154
155The next step is to add Foo to the list of feature modules for UMA recording.
156For this, add `foo` to the `AndroidFeatureModuleName` in
157`//tools/metrics/histograms/histograms.xml`:
158
159```xml
160<histogram_suffixes name="AndroidFeatureModuleName" ...>
161 ...
162 <suffix name="foo" label="Super Duper Foo Module" />
163 ...
164</histogram_suffixes>
165```
166
Tibor Goldschwendtf430b272019-11-25 19:19:41167See [below](#metrics) for what metrics will be automatically collected after
168this step.
169
Tibor Goldschwendt19364ba2019-04-10 15:59:55170<!--- TODO(tiborg): Add info about install UI. -->
171Lastly, give your module a title that Chrome and Play can use for the install
172UI. To do this, add a string to
Adam Langley891ea2b2020-04-10 17:11:18173`//chrome/browser/ui/android/strings/android_chrome_strings.grd`:
Tibor Goldschwendt19364ba2019-04-10 15:59:55174
175```xml
176...
177<message name="IDS_FOO_MODULE_TITLE"
178 desc="Text shown when the Foo module is referenced in install start, success,
179 failure UI (e.g. in IDS_MODULE_INSTALL_START_TEXT, which will expand to
180 'Installing Foo for Chrome…').">
181 Foo
182</message>
183...
184```
185
Samuel Huang7f2b53752019-05-23 15:10:05186*** note
187**Note:** This is for module title only. Other strings specific to the module
188should go in the module, not here (in the base module).
189***
190
Andrew Grieve912f66332023-04-11 16:11:56191Congrats! You added the DFM Foo to Chrome. That is a big step but not very
Tibor Goldschwendt19364ba2019-04-10 15:59:55192useful so far. In the next sections you'll learn how to add code and resources
193to it.
194
195
196### Building and installing modules
197
198Before we are going to jump into adding content to Foo, let's take a look on how
199to build and deploy the Monochrome bundle with the Foo DFM. The remainder of
200this guide assumes the environment variable `OUTDIR` is set to a properly
201configured GN build directory (e.g. `out/Debug`).
202
203To build and install the Monochrome bundle to your connected device, run:
204
205```shell
206$ autoninja -C $OUTDIR monochrome_public_bundle
Andrew Grievef0d977762021-08-18 20:20:43207$ $OUTDIR/bin/monochrome_public_bundle install -m foo
Tibor Goldschwendt19364ba2019-04-10 15:59:55208```
209
Andrew Grievef0d977762021-08-18 20:20:43210This will install the `Foo` module, the `base` module, and all modules with an
211`AndroidManifest.xml` that:
212 * Sets `<module dist:onDemand="false">`, or
213 * Has `<dist:delivery>` conditions that are satisfied by the device being
214 installed to.
Tibor Goldschwendt19364ba2019-04-10 15:59:55215
216*** note
Tibor Goldschwendtf430b272019-11-25 19:19:41217**Note:** The install script may install more modules than you specify, e.g.
218when there are default or conditionally installed modules (see
219[below](#conditional-install) for details).
Tibor Goldschwendt19364ba2019-04-10 15:59:55220***
221
222You can then check that the install worked with:
223
224```shell
225$ adb shell dumpsys package org.chromium.chrome | grep splits
226> splits=[base, config.en, foo]
227```
228
229Then try installing the Monochrome bundle without your module and print the
230installed modules:
231
232```shell
Andrew Grievef0d977762021-08-18 20:20:43233$ $OUTDIR/bin/monochrome_public_bundle install
Tibor Goldschwendt19364ba2019-04-10 15:59:55234$ adb shell dumpsys package org.chromium.chrome | grep splits
235> splits=[base, config.en]
236```
237
Andrew Grieve7e777abf2021-08-17 19:43:59238*** note
239The wrapper script's `install` command does approximately:
240```sh
Mohamed Heikalf9d9edb2021-10-04 20:05:10241java -jar third_party/android_build_tools/bundletool/bundletool.jar build-apks --output tmp.apks ...
242java -jar third_party/android_build_tools/bundletool/bundletool.jar install-apks --apks tmp.apks
Andrew Grieve7e777abf2021-08-17 19:43:59243```
244
245The `install-apks` command uses `adb install-multiple` under-the-hood.
246***
Tibor Goldschwendt19364ba2019-04-10 15:59:55247
Samuel Huang3dc9fce82020-02-26 18:09:57248### Adding Java code
Tibor Goldschwendt19364ba2019-04-10 15:59:55249
250To make Foo useful, let's add some Java code to it. This section will walk you
251through the required steps.
252
Tibor Goldschwendt573cf3022019-05-10 17:23:30253First, define a module interface for Foo. This is accomplished by adding the
254`@ModuleInterface` annotation to the Foo interface. This annotation
255automatically creates a `FooModule` class that can be used later to install and
256access the module. To do this, add the following in the new file
Henrique Nakashimacfdcce32020-04-24 22:19:36257`//chrome/browser/foo/android/java/src/org/chromium/chrome/browser/foo/Foo.java`:
Tibor Goldschwendt19364ba2019-04-10 15:59:55258
259```java
Henrique Nakashimacfdcce32020-04-24 22:19:36260package org.chromium.chrome.browser.foo;
Tibor Goldschwendt19364ba2019-04-10 15:59:55261
Fred Mello2623e052019-10-02 20:18:04262import org.chromium.components.module_installer.builder.ModuleInterface;
Tibor Goldschwendt573cf3022019-05-10 17:23:30263
Tibor Goldschwendt19364ba2019-04-10 15:59:55264/** Interface to call into Foo feature. */
Henrique Nakashimacfdcce32020-04-24 22:19:36265@ModuleInterface(module = "foo", impl = "org.chromium.chrome.browser.FooImpl")
Tibor Goldschwendt19364ba2019-04-10 15:59:55266public interface Foo {
267 /** Magical function. */
268 void bar();
269}
270```
271
Tibor Goldschwendt19364ba2019-04-10 15:59:55272Next, define an implementation that goes into the module in the new file
Henrique Nakashimacfdcce32020-04-24 22:19:36273`//chrome/browser/foo/internal/android/java/src/org/chromium/chrome/browser/foo/FooImpl.java`:
Tibor Goldschwendt19364ba2019-04-10 15:59:55274
275```java
Henrique Nakashimacfdcce32020-04-24 22:19:36276package org.chromium.chrome.browser.foo;
Tibor Goldschwendt19364ba2019-04-10 15:59:55277
278import org.chromium.base.Log;
279
280public class FooImpl implements Foo {
281 @Override
282 public void bar() {
283 Log.i("FOO", "bar in module");
284 }
285}
286```
287
Tibor Goldschwendt19364ba2019-04-10 15:59:55288You can then use this provider to access the module if it is installed. To test
289that, instantiate Foo and call `bar()` somewhere in Chrome:
290
291```java
Tibor Goldschwendt573cf3022019-05-10 17:23:30292if (FooModule.isInstalled()) {
293 FooModule.getImpl().bar();
Tibor Goldschwendt19364ba2019-04-10 15:59:55294} else {
295 Log.i("FOO", "module not installed");
296}
297```
298
Tibor Goldschwendt573cf3022019-05-10 17:23:30299The interface has to be available regardless of whether the Foo DFM is present.
Henrique Nakashimacfdcce32020-04-24 22:19:36300Therefore, put those classes into the base module, creating a new public
301build target in: `//chrome/browser/foo/BUILD.gn`:
Tibor Goldschwendt19364ba2019-04-10 15:59:55302
303```gn
Henrique Nakashimacfdcce32020-04-24 22:19:36304import("//build/config/android/rules.gni")
305
306android_library("java") {
307 sources = [
308 "android/java/src/org/chromium/chrome/browser/foo/Foo.java",
309 ]
Mohamed Heikal826b4d52021-06-25 18:13:57310 deps = [
311 "//components/module_installer/android:module_installer_java",
312 "//components/module_installer/android:module_interface_java",
313 ]
314 annotation_processor_deps =
315 [ "//components/module_installer/android:module_interface_processor" ]
Henrique Nakashimacfdcce32020-04-24 22:19:36316}
Tibor Goldschwendt19364ba2019-04-10 15:59:55317```
318
Henrique Nakashimacfdcce32020-04-24 22:19:36319Then, depend on this target from where it is used as usual. For example, if the
320caller is in `chrome_java in //chrome/android/BUILD.gn`:
Tibor Goldschwendt19364ba2019-04-10 15:59:55321
322```gn
323...
Tibor Goldschwendt19364ba2019-04-10 15:59:55324android_library("chrome_java") {
Henrique Nakashimacfdcce32020-04-24 22:19:36325 deps =[
326 ...
327 "//chrome/browser/foo:java",
328 ...
329 ]
Tibor Goldschwendt19364ba2019-04-10 15:59:55330}
331...
332```
333
334The actual implementation, however, should go into the Foo DFM. For this
Henrique Nakashimacfdcce32020-04-24 22:19:36335purpose, create a new file `//chrome/browser/foo/internal/BUILD.gn` and
Tibor Goldschwendt68c5f722019-08-01 15:10:15336make a library with the module Java code in it:
Tibor Goldschwendt19364ba2019-04-10 15:59:55337
338```gn
339import("//build/config/android/rules.gni")
340
341android_library("java") {
342 # Define like ordinary Java Android library.
Natalie Chouinardcbdc6dc2019-12-24 00:02:35343 sources = [
Henrique Nakashimacfdcce32020-04-24 22:19:36344 "android/java/src/org/chromium/chrome/browser/foo/FooImpl.java",
Tibor Goldschwendt19364ba2019-04-10 15:59:55345 # Add other Java classes that should go into the Foo DFM here.
346 ]
Fred Mellob32b3022019-06-21 18:10:11347 deps = [
Tibor Goldschwendt19364ba2019-04-10 15:59:55348 "//base:base_java",
Henrique Nakashimacfdcce32020-04-24 22:19:36349 # Put other Chrome libs into the classpath so that you can call into them
350 # from the Foo DFM.
351 "//chrome/browser/bar:java",
352 # The module can depend even on `chrome_java` due to factory magic, but this
353 # is discouraged. Consider passing a delegate interface in instead.
Tibor Goldschwendt19364ba2019-04-10 15:59:55354 "//chrome/android:chrome_java",
Tibor Goldschwendt19364ba2019-04-10 15:59:55355 # Also, you'll need to depend on any //third_party or //components code you
356 # are using in the module code.
357 ]
358}
359```
360
Tibor Goldschwendtaef8e392019-07-19 16:39:10361Then, add this new library as a dependency of the Foo module descriptor in
Henrique Nakashimacfdcce32020-04-24 22:19:36362`//chrome/android/modules/foo/foo_module.gni`:
Tibor Goldschwendt19364ba2019-04-10 15:59:55363
364```gn
Tibor Goldschwendtaef8e392019-07-19 16:39:10365foo_module_desc = {
Tibor Goldschwendt19364ba2019-04-10 15:59:55366 ...
Tibor Goldschwendtaef8e392019-07-19 16:39:10367 java_deps = [
Henrique Nakashimacfdcce32020-04-24 22:19:36368 "//chrome/browser/foo/internal:java",
Tibor Goldschwendt19364ba2019-04-10 15:59:55369 ]
370}
371```
372
373Finally, tell Android that your module is now containing code. Do that by
Samuel Huang39c7db632019-05-15 14:57:18374removing the `android:hasCode="false"` attribute from the `<application>` tag in
Henrique Nakashimacfdcce32020-04-24 22:19:36375`//chrome/android/modules/foo/internal/java/AndroidManifest.xml`. You should be
Tibor Goldschwendt68c5f722019-08-01 15:10:15376left with an empty tag like so:
Tibor Goldschwendt19364ba2019-04-10 15:59:55377
378```xml
379...
380 <application />
381...
382```
383
384Rebuild and install `monochrome_public_bundle`. Start Chrome and run through a
385flow that tries to executes `bar()`. Depending on whether you installed your
386module (`-m foo`) "`bar in module`" or "`module not installed`" is printed to
387logcat. Yay!
388
Christopher Grantf649d282020-01-09 22:56:08389### Adding pre-built native libraries
Tibor Goldschwendt19364ba2019-04-10 15:59:55390
Christopher Grant8fea5a12019-07-31 19:12:31391You can add a third-party native library (or any standalone library that doesn't
392depend on Chrome code) by adding it as a loadable module to the module descriptor in
Henrique Nakashimacfdcce32020-04-24 22:19:36393`//chrome/android/moduiles/foo/foo_module.gni`:
Tibor Goldschwendt19364ba2019-04-10 15:59:55394
395```gn
Tibor Goldschwendtaef8e392019-07-19 16:39:10396foo_module_desc = {
Tibor Goldschwendt19364ba2019-04-10 15:59:55397 ...
Tibor Goldschwendtaef8e392019-07-19 16:39:10398 loadable_modules_32_bit = [ "//path/to/32/bit/lib.so" ]
399 loadable_modules_64_bit = [ "//path/to/64/bit/lib.so" ]
Tibor Goldschwendt19364ba2019-04-10 15:59:55400}
401```
402
Christopher Grant8fea5a12019-07-31 19:12:31403### Adding Chrome native code
Tibor Goldschwendt19364ba2019-04-10 15:59:55404
Christopher Grantf649d282020-01-09 22:56:08405Chrome native code may be placed in a DFM. The easiest way to access native
406feature code is by calling it from Java via JNI. When a module is first
407accessed, its native library (or potentially libraries, if using a component
408build), are automatically opened by the DFM framework, and a feature-specific
409JNI method (supplied by the feature's implementation) is invoked. Hence, a
410module's Java code may freely use JNI to call module native code.
Christopher Grant8fea5a12019-07-31 19:12:31411
Christopher Grantf649d282020-01-09 22:56:08412Using the module framework and JNI to access the native code eliminates concerns
413with DFM library file names (which vary across build variants),
414`android_dlopen_ext()` (needed to open feature libraries), and use of dlsym().
Christopher Grant8fea5a12019-07-31 19:12:31415
Christopher Grantf649d282020-01-09 22:56:08416This mechanism can be extended if necessary by DFM implementers to facilitate
417subsequent native-native calls, by having a JNI-called initialization method
418create instance of a object or factory, and register it through a call to the
419base module's native code (DFM native code can call base module code directly).
Christopher Grant8fea5a12019-07-31 19:12:31420
Eric Stevenson8c9ab26b2019-08-30 15:44:40421#### JNI
422
Sam Maiere1df6f22023-08-11 14:20:40423Read the `jni_generator` [docs](../third_party/jni_zero/README.md) before
Eric Stevenson8c9ab26b2019-08-30 15:44:40424reading this section.
425
426There are some subtleties to how JNI registration works with DFMs:
427
428* Generated wrapper `ClassNameJni` classes are packaged into the DFM's dex file
Sam Maier79e40da2023-01-27 17:26:11429* The class containing the actual native definitions,
430 `<module_name>_GEN_JNI.java`, is currently stored in the base module, but
431 could be moved out
432* The `Natives` interface you provide will need to be annotated with your module
433 name as an argument to `NativeMethods`, eg. `@NativeMethods("foo")`, resulting
434 in a uniquely named `foo_GEN_JNI.java`
435* The DFM will need to provide a `generate_jni_registration` target
Eric Stevenson8c9ab26b2019-08-30 15:44:40436 that will generate all of the native registration functions
437
Christopher Grantf649d282020-01-09 22:56:08438#### Calling DFM native code via JNI
439
440A linker-assisted partitioning system automates the placement of code into
441either the main Chrome library or feature-specific .so libraries. Feature code
442may continue to make use of core Chrome code (eg. base::) without modification,
443but Chrome must call feature code through a virtual interface (any "direct"
444calls to the feature code from the main library will cause the feature code to
445be pulled back into the main library).
446
447Partitioning is explained in [Android Native
448Libraries](android_native_libraries.md#partitioned-libraries).
449
450First, build a module native interface. Supply a JNI method named
451`JNI_OnLoad_foo` for the module framework to call, in
452`//chrome/android/modules/foo/internal/entrypoints.cc`. This method is invoked
453on all Chrome build variants, including Monochrome (unlike base module JNI).
454
455```c++
Sam Maiere1df6f22023-08-11 14:20:40456#include "third_party/jni_zero/jni_generator_helper.h"
Christopher Grantf649d282020-01-09 22:56:08457#include "base/android/jni_utils.h"
458#include "chrome/android/modules/foo/internal/jni_registration.h"
459
460extern "C" {
461// This JNI registration method is found and called by module framework code.
462JNI_GENERATOR_EXPORT bool JNI_OnLoad_foo(JNIEnv* env) {
Sam Maierb0bd6d92023-04-05 13:22:23463 if (!foo::RegisterNatives(env)) {
Christopher Grantf649d282020-01-09 22:56:08464 return false;
465 }
466 return true;
467}
468} // extern "C"
469```
470
471Next, include the module entrypoint and related pieces in the build config at
472`//chrome/android/modules/foo/internal/BUILD.gn`:
473
474```gn
475import("//build/config/android/rules.gni")
476import("//chrome/android/modules/buildflags.gni")
477...
478
479# Put the JNI entrypoint in a component, so that the component build has a
480# library to include in the foo module. This makes things feel consistent with
481# a release build.
482component("foo") {
483 sources = [
484 "entrypoints.cc",
485 ]
486 deps = [
487 ":jni_registration",
Christopher Grantf649d282020-01-09 22:56:08488 "//base",
Henrique Nakashimacfdcce32020-04-24 22:19:36489 "//chrome/browser/foo/internal:native",
Christopher Grantf649d282020-01-09 22:56:08490 ]
491
492 # Instruct the compiler to flag exported entrypoint function as belonging in
493 # foo's library. The linker will use this information when creating the
494 # native libraries. The partition name must be <feature>_partition.
495 if (use_native_partitions) {
496 cflags = [ "-fsymbol-partition=foo_partition" ]
497 }
498}
499
500# Generate JNI registration for the methods called by the Java side. Note the
501# no_transitive_deps argument, which ensures that JNI is generated for only the
502# specified Java target, and not all its transitive deps (which could include
503# the base module).
504generate_jni_registration("jni_registration") {
Henrique Nakashimacfdcce32020-04-24 22:19:36505 targets = [ "//chrome/browser/foo/internal:java" ]
Christopher Grantf649d282020-01-09 22:56:08506 namespace = "foo"
507 no_transitive_deps = true
Sam Maier79e40da2023-01-27 17:26:11508 manual_jni_registration = true
Christopher Grantf649d282020-01-09 22:56:08509}
510
511# This group is a convenience alias representing the module's native code,
512# allowing it to be named "native" for clarity in module descriptors.
513group("native") {
514 deps = [
515 ":foo",
516 ]
517}
518```
519
520Now, over to the implementation of the module. These are the parts that
521shouldn't know or care whether they're living in a module or not.
522
523Add a stub implementation in
Henrique Nakashimacfdcce32020-04-24 22:19:36524`//chrome/browser/foo/internal/android/foo_impl.cc`:
Christopher Grantf649d282020-01-09 22:56:08525
526```c++
527#include "base/logging.h"
Henrique Nakashimacfdcce32020-04-24 22:19:36528#include "chrome/browser/foo/internal/jni_headers/FooImpl_jni.h"
Christopher Grantf649d282020-01-09 22:56:08529
530static int JNI_FooImpl_Execute(JNIEnv* env) {
531 LOG(INFO) << "Running foo feature code!";
532 return 123;
533}
534```
535
536And, the associated build config in
Henrique Nakashimacfdcce32020-04-24 22:19:36537`//chrome/browser/foo/internal/BUILD.gn`:
Christopher Grantf649d282020-01-09 22:56:08538
539```gn
540import("//build/config/android/rules.gni")
541
542...
543
544source_set("native") {
545 sources = [
Henrique Nakashimacfdcce32020-04-24 22:19:36546 "android/foo_impl.cc",
Christopher Grantf649d282020-01-09 22:56:08547 ]
548
549 deps = [
550 ":jni_headers",
551 "//base",
552 ]
553}
554
555generate_jni("jni_headers") {
556 sources = [
Henrique Nakashimacfdcce32020-04-24 22:19:36557 "android/java/src/org/chromium/chrome/browser/foo/FooImpl.java",
Christopher Grantf649d282020-01-09 22:56:08558 ]
559}
560```
561
562With a declaration of the native method on the Java side:
563
564```java
565public class FooImpl implements Foo {
566 ...
567
Sam Maier79e40da2023-01-27 17:26:11568 @NativeMethods("foo")
Christopher Grantf649d282020-01-09 22:56:08569 interface Natives {
570 int execute();
571 }
572}
573```
574
575Finally, augment the module descriptor in
576`//chrome/android/modules/foo/foo_module.gni` with the native dependencies:
577
578```gn
579foo_module_desc = {
580 ...
581 native_deps = [
Christopher Grantf649d282020-01-09 22:56:08582 "//chrome/android/modules/foo/internal:native",
Henrique Nakashimacfdcce32020-04-24 22:19:36583 "//chrome/browser/foo/internal:native",
Christopher Grantf649d282020-01-09 22:56:08584 ]
Samuel Huang3dc9fce82020-02-26 18:09:57585 load_native_on_get_impl = true
Christopher Grantf649d282020-01-09 22:56:08586}
587```
588
Samuel Huang3dc9fce82020-02-26 18:09:57589If `load_native_on_get_impl` is set to `true` then Chrome automatically loads
590Foo DFM's native libraries and PAK file resources when `FooModule.getImpl()` is
591called for the first time. The loading requires Chrome's main native libraries
592to be loaded. If you wish to call `FooModule.getImpl()` earlier than that, then
593you'd need to set `load_native_on_get_impl` to `false`, and manage native
594libraries / resources loading yourself (potentially, on start-up and on install,
595or on use).
596
Christopher Grantf649d282020-01-09 22:56:08597#### Calling feature module native code from base the module
598
599If planning to use direct native-native calls into DFM code, then the module
600should have a purely virtual interface available. The main module can obtain a
601pointer to a DFM-created object or factory (implemented by the feature), and
602call its virtual methods.
603
604Ideally, the interface to the feature will avoid feature-specific types. If a
605feature defines complex data types, and uses them in its own interface, then its
606likely the main library will utilize the code backing these types. That code,
607and anything it references, will in turn be pulled back into the main library,
608negating the intent to house code in the DFM.
609
610Therefore, designing the feature interface to use C types, C++ standard types,
611or classes that aren't expected to move out of Chrome's main library is ideal.
612If feature-specific classes are needed, they simply need to avoid referencing
613feature library internals.
Eric Stevenson8c9ab26b2019-08-30 15:44:40614
Christopher Grant8fea5a12019-07-31 19:12:31615### Adding Android resources
Tibor Goldschwendt19364ba2019-04-10 15:59:55616
617In this section we will add the required build targets to add Android resources
618to the Foo DFM.
619
Tibor Goldschwendt68c5f722019-08-01 15:10:15620First, add a resources target to
Henrique Nakashimacfdcce32020-04-24 22:19:36621`//chrome/browser/foo/internal/BUILD.gn` and add it as a dependency on
Tibor Goldschwendt68c5f722019-08-01 15:10:15622Foo's `java` target in the same file:
Tibor Goldschwendt19364ba2019-04-10 15:59:55623
624```gn
625...
626android_resources("java_resources") {
627 # Define like ordinary Android resources target.
628 ...
Henrique Nakashimacfdcce32020-04-24 22:19:36629 custom_package = "org.chromium.chrome.browser.foo"
Tibor Goldschwendt19364ba2019-04-10 15:59:55630}
631...
632android_library("java") {
633 ...
634 deps = [
635 ":java_resources",
636 ]
637}
638```
639
640To add strings follow steps
641[here](http://dev.chromium.org/developers/design-documents/ui-localization) to
642add new Java GRD file. Then create
Henrique Nakashimacfdcce32020-04-24 22:19:36643`//chrome/browser/foo/internal/android/resources/strings/android_foo_strings.grd` as
Tibor Goldschwendt68c5f722019-08-01 15:10:15644follows:
Tibor Goldschwendt19364ba2019-04-10 15:59:55645
646```xml
647<?xml version="1.0" encoding="UTF-8"?>
648<grit
649 current_release="1"
650 latest_public_release="0"
651 output_all_resource_defines="false">
652 <outputs>
653 <output
654 filename="values-am/android_foo_strings.xml"
655 lang="am"
656 type="android" />
657 <!-- List output file for all other supported languages. See
Henrique Nakashimaf5439f82021-01-29 23:52:24658 //chrome/browser/ui/android/strings/android_chrome_strings.grd for the
659 full list. -->
Tibor Goldschwendt19364ba2019-04-10 15:59:55660 ...
661 </outputs>
662 <translations>
663 <file lang="am" path="vr_translations/android_foo_strings_am.xtb" />
664 <!-- Here, too, list XTB files for all other supported languages. -->
665 ...
666 </translations>
Matt Stark1debb5de2021-02-15 16:08:24667 <release seq="1">
Tibor Goldschwendt19364ba2019-04-10 15:59:55668 <messages fallback_to_english="true">
669 <message name="IDS_BAR_IMPL_TEXT" desc="Magical string.">
670 impl
671 </message>
672 </messages>
673 </release>
674</grit>
675```
676
677Then, create a new GRD target and add it as a dependency on `java_resources` in
Henrique Nakashimacfdcce32020-04-24 22:19:36678`//chrome/browser/foo/internal/BUILD.gn`:
Tibor Goldschwendt19364ba2019-04-10 15:59:55679
680```gn
681...
682java_strings_grd("java_strings_grd") {
683 defines = chrome_grit_defines
Henrique Nakashimacfdcce32020-04-24 22:19:36684 grd_file = "android/resources/strings/android_foo_strings.grd"
Tibor Goldschwendt19364ba2019-04-10 15:59:55685 outputs = [
686 "values-am/android_foo_strings.xml",
687 # Here, too, list output files for other supported languages.
688 ...
689 ]
690}
691...
692android_resources("java_resources") {
693 ...
694 deps = [":java_strings_grd"]
Henrique Nakashimacfdcce32020-04-24 22:19:36695 custom_package = "org.chromium.chrome.browser.foo"
Tibor Goldschwendt19364ba2019-04-10 15:59:55696}
697...
698```
699
700You can then access Foo's resources using the
Henrique Nakashimacfdcce32020-04-24 22:19:36701`org.chromium.chrome.browser.foo.R` class. To do this change
702`//chrome/browser/foo/internal/android/java/src/org/chromium/chrome/browser/foo/FooImpl.java`
Tibor Goldschwendt19364ba2019-04-10 15:59:55703to:
704
705```java
Henrique Nakashimacfdcce32020-04-24 22:19:36706package org.chromium.chrome.browser.foo;
Tibor Goldschwendt19364ba2019-04-10 15:59:55707
708import org.chromium.base.ContextUtils;
709import org.chromium.base.Log;
Henrique Nakashimacfdcce32020-04-24 22:19:36710import org.chromium.chrome.browser.foo.R;
Tibor Goldschwendt19364ba2019-04-10 15:59:55711
712public class FooImpl implements Foo {
713 @Override
714 public void bar() {
715 Log.i("FOO", ContextUtils.getApplicationContext().getString(
716 R.string.bar_impl_text));
717 }
718}
719```
720
Samuel Huang3dc9fce82020-02-26 18:09:57721### Adding non-string native resources
722
723This section describes how to add non-string native resources to Foo DFM.
724Key ideas:
725
726* The compiled resource file shipped with the DFM is `foo_resourcess.pak`.
727* At run time, native resources need to be loaded before use. Also, DFM native
728 resources can only be used from the Browser process.
729
730#### Creating PAK file
731
732Two ways to create `foo_resourcess.pak` (using GRIT) are:
733
7341. (Preferred) Use `foo_resourcess.grd` to refer to individual files (e.g.,
735 images, HTML, JS, or CSS) and assigns resource text IDs. `foo_resourcess.pak`
736 must have an entry in `/tools/gritsettings/resource_ids.spec`.
7371. Combine existing .pak files via `repack` rules in GN build files. This is
738 done by the DevUI DFM, which aggregates resources from many DevUI pages.
739
740#### Loading PAK file
741
742At runtime, `foo_resources.pak` needs to be loaded (memory-mapped) before any of
743its resource gets used. Alternatives to do this are:
744
7451. (Simplest) Specify native resources (with native libraries if any exist) to
746 be automatically loaded on first call to `FooModule.getImpl()`. This behavior
747 is specified via `load_native_on_get_impl = true` in `foo_module_desc`.
7481. In Java code, call `FooModule.ensureNativeLoaded()`.
7491. In C++ code, use JNI to call `FooModule.ensureNativeLoaded()`. The code to do
750 this can be placed in a helper class, which can also have JNI calls to
751 `FooModule.isInstalled()` and `FooModule.installModule()`.
752
753#### Cautionary notes
754
755Compiling `foo_resources.pak` auto-generates `foo_resources.h`, which defines
756textual resource IDs, e.g., `IDR_FOO_HTML`. C++ code then uses these IDs to get
757resource bytes. Unfortunately, this behavior is fragile: If `IDR_FOO_HTML` is
758accessed before the Foo DFM is (a) installed, or (b) loaded, then runtime error
759ensues! Some mitigation strategies are as follows:
760
761* (Ideal) Access Foo DFM's native resources only from code in Foo DFM's native
762 libraries. So by the time that `IDR_FOO_HTML` is accessed, everything is
763 already in place! This isn't always possible; henceforth we assume that
764 `IDR_FOO_HTML` is accessed by code in the base DFM.
765* Before accessing IDR_FOO_HTML, ensure Foo DFM is installed and loaded. The
766 latter can use `FooModule.ensureNativeLoaded()` (needs to be called from
767 Browser thread).
768* Use inclusion of `foo_resources.h` to restrict availability of `IDR_FOO_HTML`.
769 Only C++ files dedicated to "DFM-gated code" (code that runs only when its DFM
770 is installed and loaded) should include `foo_resources.h`.
771
772#### Associating native resources with DFM
773
774Here are the main GN changes to specify PAK files and default loading behavior
775for a DFM's native resources:
776
777```gn
778foo_module_desc = {
779 ...
Henrique Nakashimacfdcce32020-04-24 22:19:36780 paks = [ "$root_gen_dir/chrome/browser/foo/internal/foo_resourcess.pak" ]
781 pak_deps = [ "//chrome/browser/foo/internal:foo_paks" ]
Samuel Huang3dc9fce82020-02-26 18:09:57782 load_native_on_get_impl = true
783}
784```
785
786Note that `load_native_on_get_impl` specifies both native libraries and native
787resources.
788
Tibor Goldschwendt19364ba2019-04-10 15:59:55789
790### Module install
791
792So far, we have installed the Foo DFM as a true split (`-m foo` option on the
793install script). In production, however, we have to explicitly install the Foo
Tibor Goldschwendt68c5f722019-08-01 15:10:15794DFM for users to get it. There are three install options: _on-demand_,
795_deferred_ and _conditional_.
Tibor Goldschwendt19364ba2019-04-10 15:59:55796
Tibor Goldschwendt19364ba2019-04-10 15:59:55797#### On-demand install
798
799On-demand requesting a module will try to download and install the
800module as soon as possible regardless of whether the user is on a metered
801connection or whether they have turned updates off in the Play Store app.
802
Tibor Goldschwendt573cf3022019-05-10 17:23:30803You can use the autogenerated module class to on-demand install the module like
804so:
Tibor Goldschwendt19364ba2019-04-10 15:59:55805
806```java
Tibor Goldschwendt573cf3022019-05-10 17:23:30807FooModule.install((success) -> {
808 if (success) {
809 FooModule.getImpl().bar();
810 }
Tibor Goldschwendt19364ba2019-04-10 15:59:55811});
812```
813
814**Optionally**, you can show UI telling the user about the install flow. For
Tibor Goldschwendt573cf3022019-05-10 17:23:30815this, add a function like the one below. Note, it is possible
Tibor Goldschwendt19364ba2019-04-10 15:59:55816to only show either one of the install, failure and success UI or any
817combination of the three.
818
819```java
820public static void installModuleWithUi(
821 Tab tab, OnModuleInstallFinishedListener onFinishedListener) {
822 ModuleInstallUi ui =
823 new ModuleInstallUi(
824 tab,
825 R.string.foo_module_title,
826 new ModuleInstallUi.FailureUiListener() {
827 @Override
Samuel Huangfebcccd2019-08-21 20:48:47828 public void onFailureUiResponse(retry) {
829 if (retry) {
830 installModuleWithUi(tab, onFinishedListener);
831 } else {
832 onFinishedListener.onFinished(false);
833 }
Tibor Goldschwendt19364ba2019-04-10 15:59:55834 }
835 });
836 // At the time of writing, shows toast informing user about install start.
837 ui.showInstallStartUi();
Tibor Goldschwendt573cf3022019-05-10 17:23:30838 FooModule.install(
Tibor Goldschwendt19364ba2019-04-10 15:59:55839 (success) -> {
840 if (!success) {
841 // At the time of writing, shows infobar allowing user
842 // to retry install.
843 ui.showInstallFailureUi();
844 return;
845 }
846 // At the time of writing, shows toast informing user about
847 // install success.
848 ui.showInstallSuccessUi();
849 onFinishedListener.onFinished(true);
850 });
851}
852```
853
854To test on-demand install, "fake-install" the DFM. It's fake because
Peter Wen8bf82d42021-08-13 22:03:54855the DFM is not installed as a true split. Instead it will be emulated by play
856core's `--local-testing` [mode][play-core-local-testing].
Tibor Goldschwendt19364ba2019-04-10 15:59:55857Fake-install and launch Chrome with the following command:
858
859```shell
Andrew Grievef0d977762021-08-18 20:20:43860$ $OUTDIR/bin/monochrome_public_bundle install -f foo
Peter Wen8bf82d42021-08-13 22:03:54861$ $OUTDIR/bin/monochrome_public_bundle launch
Tibor Goldschwendt19364ba2019-04-10 15:59:55862```
863
864When running the install code, the Foo DFM module will be emulated.
865This will be the case in production right after installing the module. Emulation
866will last until Play Store has a chance to install your module as a true split.
Peter Wen577a6fe52019-12-11 22:02:05867This usually takes about a day. After it has been installed, it will be updated
868atomically alongside Chrome. Always check that it is installed and available
869before invoking code within the DFM.
Tibor Goldschwendt19364ba2019-04-10 15:59:55870
871*** note
872**Warning:** There are subtle differences between emulating a module and
873installing it as a true split. We therefore recommend that you always test both
874install methods.
875***
876
Samuel Huang6f5c7ddb82020-05-14 17:10:52877*** note
878To simplify development, the DevUI DFM (dev_ui) is installed by default, i.e.,
879`-m dev_ui` is implied by default. This is overridden by:
880* `--no-module dev_ui`, to test error from missing DevUI,
881* `-f dev_ui`, for fake module install.
882***
Tibor Goldschwendt19364ba2019-04-10 15:59:55883
884#### Deferred install
885
886Deferred install means that the DFM is installed in the background when the
887device is on an unmetered connection and charging. The DFM will only be
888available after Chrome restarts. When deferred installing a module it will
889not be faked installed.
890
891To defer install Foo do the following:
892
893```java
Tibor Goldschwendt573cf3022019-05-10 17:23:30894FooModule.installDeferred();
Tibor Goldschwendt19364ba2019-04-10 15:59:55895```
896
Tibor Goldschwendt68c5f722019-08-01 15:10:15897#### Conditional install
898
899Conditional install means the DFM will be installed automatically upon first
900installing or updating Chrome if the device supports a particular feature.
901Conditional install is configured in the module's manifest. To install your
902module on all Daydream-ready devices for instance, your
Henrique Nakashimacfdcce32020-04-24 22:19:36903`//chrome/android/modules/foo/internal/java/AndroidManifest.xml` should look
Tibor Goldschwendt68c5f722019-08-01 15:10:15904like this:
905
906```xml
907<?xml version="1.0" encoding="utf-8"?>
908<manifest xmlns:android="http://schemas.android.com/apk/res/android"
909 xmlns:dist="http://schemas.android.com/apk/distribution"
910 featureSplit="foo">
911
912 <dist:module
913 dist:instant="false"
914 dist:title="@string/foo_module_title">
Ben Masone571ea5a2019-09-06 18:29:37915 <dist:fusing dist:include="true" />
Tibor Goldschwendt68c5f722019-08-01 15:10:15916 <dist:delivery>
917 <dist:install-time>
918 <dist:conditions>
919 <dist:device-feature
920 dist:name="android.hardware.vr.high_performance" />
921 </dist:conditions>
922 </dist:install-time>
923 <!-- Allows on-demand or deferred install on non-Daydream-ready
924 devices. -->
925 <dist:on-demand />
926 </dist:delivery>
927 </dist:module>
928
929 <application />
930</manifest>
931```
932
Andrew Grievef6069feb2021-04-29 18:29:17933You can also specify no conditions to have your module always installed.
934You might want to do this in order to delay the performance implications
935of loading your module until its first use (true only on Android O+ where
936[android:isolatedSplits](https://developer.android.com/reference/android/R.attr#isolatedSplits)
937is supported. See [go/isolated-splits-dev-guide](http://go/isolated-splits-dev-guide)
938(googlers only).
939
Tibor Goldschwendtf430b272019-11-25 19:19:41940### Metrics
941
942After adding your module to `AndroidFeatureModuleName` (see
943[above](#create-dfm-target)) we will collect, among others, the following
944metrics:
945
946* `Android.FeatureModules.AvailabilityStatus.Foo`: Measures your module's
947 install penetration. That is, the share of users who eventually installed
948 the module after requesting it (once or multiple times).
949
950* `Android.FeatureModules.InstallStatus.Foo`: The result of an on-demand
951 install request. Can be success or one of several error conditions.
952
953* `Android.FeatureModules.UncachedAwakeInstallDuration.Foo`: The duration to
954 install your module successfully after on-demand requesting it.
955
Tibor Goldschwendt19364ba2019-04-10 15:59:55956
Andrew Grievef6069feb2021-04-29 18:29:17957### chrome_public_apk and Integration Tests
Tibor Goldschwendt19364ba2019-04-10 15:59:55958
Andrew Grievef6069feb2021-04-29 18:29:17959To make the Foo feature available in the non-bundle `chrome_public_apk`
960target, add the `java` target to the `chrome_public_common_apk_or_module_tmpl`
961in `//chrome/android/chrome_public_apk_tmpl.gni` like so:
Tibor Goldschwendt19364ba2019-04-10 15:59:55962
963```gn
964template("chrome_public_common_apk_or_module_tmpl") {
965 ...
966 target(_target_type, target_name) {
967 ...
968 if (_target_type != "android_app_bundle_module") {
969 deps += [
Henrique Nakashimacfdcce32020-04-24 22:19:36970 "//chrome/browser/foo/internal:java",
Tibor Goldschwendt19364ba2019-04-10 15:59:55971 ]
972 }
973 }
974}
975```
976
Andrew Grievef6069feb2021-04-29 18:29:17977You may also have to add `java` as a dependency of `chrome_test_java` if you want
978to call into Foo from test code.
Peter Wen8bf82d42021-08-13 22:03:54979
Mohamed Heikalf9d9edb2021-10-04 20:05:10980[play-core-local-testing]: https://developer.android.com/guide/playcore/feature-delivery/on-demand#local-testing