blob: 3eec02f0427749e53546b5d4b76bc7256e611c2b [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".
Andrew Grievebf63c452024-09-04 16:47:4923 * **The install experience for DFMs is quite poor (5-30 seconds install times,
24 sometimes fails, sometimes [triggers a crash]).**
Andrew Grievef6069feb2021-04-29 18:29:1725
26You can inspect which `.apk` files are produced by a bundle target via:
27```
28out/Default/bin/${target_name} build-bundle-apks --output-apks foo.apks
29unzip -l foo.apks
30```
31
Andrew Grieve23c828d2021-05-13 19:37:2932*** note
Andrew Grieve5d8fc662024-08-16 18:10:4033Adding new features via feature splits is highly encouraged when it makes sense
Andrew Grieve23c828d2021-05-13 19:37:2934to do so:
Andrew Grieve5d8fc662024-08-16 18:10:4035 * Has a non-trivial amount of Java code (after optimization). E.g. >150kb
Andrew Grieve23c828d2021-05-13 19:37:2936 * Not needed on startup
Andrew Grieve7e777abf2021-08-17 19:43:5937 * Has a small integration surface (calls into it must be done with reflection)
Andrew Grieve5d8fc662024-08-16 18:10:4038 * Not used by WebView
Andrew Grieve23c828d2021-05-13 19:37:2939***
40
Andrew Grievef6069feb2021-04-29 18:29:1741[android_build_instructions.md#multiple-chrome-targets]: android_build_instructions.md#multiple-chrome-targets
42[Android App Bundles]: https://developer.android.com/guide/app-bundle
Andrew Grieve520e0902022-04-27 13:19:2543[isolated splits]: android_isolated_splits.md
Andrew Grievebf63c452024-09-04 16:47:4944[triggers a crash]: https://chromium.googlesource.com/chromium/src/+/main/docs/android_isolated_splits.md#Conflicting-ClassLoaders-2
Tibor Goldschwendt19364ba2019-04-10 15:59:5545
Andrew Grieve7e777abf2021-08-17 19:43:5946### Declaring App Bundles with GN Templates
Tibor Goldschwendt19364ba2019-04-10 15:59:5547
Andrew Grieve7e777abf2021-08-17 19:43:5948Here's an example that shows how to declare a simple bundle that contains a
49single base module, which enables language-based splits:
Tibor Goldschwendt19364ba2019-04-10 15:59:5550
Andrew Grieve7e777abf2021-08-17 19:43:5951```gn
52 android_app_bundle_module("foo_base_module") {
53 # Declaration are similar to android_apk here.
54 ...
55 }
Tibor Goldschwendt19364ba2019-04-10 15:59:5556
Andrew Grieve7e777abf2021-08-17 19:43:5957 android_app_bundle("foo_bundle") {
58 base_module_target = ":foo_base_module"
59
60 # The name of our bundle file (without any suffix).
61 bundle_name = "FooBundle"
62
63 # Enable language-based splits for this bundle. Which means that
64 # resources and assets specific to a given language will be placed
65 # into their own split APK in the final .apks archive.
66 enable_language_splits = true
67
68 # Proguard settings must be passed at the bundle, not module, target.
69 proguard_enabled = !is_java_debug
70 }
71```
72
73When generating the `foo_bundle` target with Ninja, you will end up with
74the following:
75
76 * The bundle file under `out/Release/apks/FooBundle.aab`
77
78 * A helper script called `out/Release/bin/foo_bundle`, which can be used
79 to install / launch / uninstall the bundle on local devices.
80
81 This works like an APK wrapper script (e.g. `foo_apk`). Use `--help`
82 to see all possible commands supported by the script.
83
84
85The remainder of this doc focuses on DFMs.
86
87## Declaring Dynamic Feature Modules (DFMs)
Tibor Goldschwendt19364ba2019-04-10 15:59:5588
89This guide walks you through the steps to create a DFM called _Foo_ and add it
Tibor Goldschwendtaef8e392019-07-19 16:39:1090to the Chrome bundles.
Tibor Goldschwendt19364ba2019-04-10 15:59:5591
92*** note
93**Note:** To make your own module you'll essentially have to replace every
94instance of `foo`/`Foo`/`FOO` with `your_feature_name`/`YourFeatureName`/
95`YOUR_FEATURE_NAME`.
96***
97
Christopher Grantf649d282020-01-09 22:56:0898### Reference DFM
99
100In addition to this guide, the
101[Test Dummy](https://cs.chromium.org/chromium/src/chrome/android/modules/test_dummy/test_dummy_module.gni)
102module serves as an actively-maintained reference DFM. Test Dummy is used in
103automated bundle testing, and covers both Java and native code and resource
104usage.
Tibor Goldschwendt19364ba2019-04-10 15:59:55105
106### Create DFM target
107
108DFMs are APKs. They have a manifest and can contain Java and native code as well
109as resources. This section walks you through creating the module target in our
110build system.
111
Tibor Goldschwendt68c5f722019-08-01 15:10:15112First, create the file
Henrique Nakashimacfdcce32020-04-24 22:19:36113`//chrome/android/modules/foo/internal/java/AndroidManifest.xml` and add:
Tibor Goldschwendt19364ba2019-04-10 15:59:55114
115```xml
Tibor Goldschwendt68c5f722019-08-01 15:10:15116<?xml version="1.0" encoding="utf-8"?>
Tibor Goldschwendt19364ba2019-04-10 15:59:55117<manifest xmlns:android="http://schemas.android.com/apk/res/android"
118 xmlns:dist="http://schemas.android.com/apk/distribution"
Tibor Goldschwendt5172118f2019-06-24 21:57:47119 featureSplit="foo">
Tibor Goldschwendt19364ba2019-04-10 15:59:55120
Tibor Goldschwendt19364ba2019-04-10 15:59:55121 <!-- dist:onDemand="true" makes this a separately installed module.
122 dist:onDemand="false" would always install the module alongside the
123 rest of Chrome. -->
124 <dist:module
125 dist:onDemand="true"
126 dist:title="@string/foo_module_title">
Ben Masone571ea5a2019-09-06 18:29:37127 <!-- This will fuse the module into the base APK if a system image
128 APK is built from this bundle. -->
129 <dist:fusing dist:include="true" />
Tibor Goldschwendt19364ba2019-04-10 15:59:55130 </dist:module>
131
Samuel Huang39c7db632019-05-15 14:57:18132 <!-- Remove android:hasCode="false" when adding Java code. -->
133 <application android:hasCode="false" />
Tibor Goldschwendt19364ba2019-04-10 15:59:55134</manifest>
135```
136
Tibor Goldschwendtaef8e392019-07-19 16:39:10137Next, create a descriptor configuring the Foo module. To do this, create
Henrique Nakashimacfdcce32020-04-24 22:19:36138`//chrome/android/modules/foo/foo_module.gni` and add the following:
Tibor Goldschwendt19364ba2019-04-10 15:59:55139
140```gn
Tibor Goldschwendtaef8e392019-07-19 16:39:10141foo_module_desc = {
142 name = "foo"
Tibor Goldschwendt68c5f722019-08-01 15:10:15143 android_manifest =
Henrique Nakashimacfdcce32020-04-24 22:19:36144 "//chrome/android/modules/foo/internal/java/AndroidManifest.xml"
Tibor Goldschwendt19364ba2019-04-10 15:59:55145}
146```
147
Tibor Goldschwendtaef8e392019-07-19 16:39:10148Then, add the module descriptor to the appropriate descriptor list in
Andrew Grieve912f66332023-04-11 16:11:56149//chrome/android/modules/chrome_feature_modules.gni, e.g. the Chrome list:
Tibor Goldschwendt19364ba2019-04-10 15:59:55150
151```gn
Henrique Nakashimacfdcce32020-04-24 22:19:36152import("//chrome/android/modules/foo/foo_module.gni")
Tibor Goldschwendt19364ba2019-04-10 15:59:55153...
Andrew Grieve912f66332023-04-11 16:11:56154chrome_module_descs += [ foo_module_desc ]
Tibor Goldschwendt19364ba2019-04-10 15:59:55155```
156
157The next step is to add Foo to the list of feature modules for UMA recording.
158For this, add `foo` to the `AndroidFeatureModuleName` in
James Leeec45f5d2024-06-21 07:13:38159`//tools/metrics/histograms/metadata/histogram_suffixes_list.xml`:
Tibor Goldschwendt19364ba2019-04-10 15:59:55160
161```xml
162<histogram_suffixes name="AndroidFeatureModuleName" ...>
163 ...
164 <suffix name="foo" label="Super Duper Foo Module" />
165 ...
166</histogram_suffixes>
167```
168
Tibor Goldschwendt19364ba2019-04-10 15:59:55169Lastly, give your module a title that Chrome and Play can use for the install
170UI. To do this, add a string to
Adam Langley891ea2b2020-04-10 17:11:18171`//chrome/browser/ui/android/strings/android_chrome_strings.grd`:
Tibor Goldschwendt19364ba2019-04-10 15:59:55172
173```xml
174...
175<message name="IDS_FOO_MODULE_TITLE"
176 desc="Text shown when the Foo module is referenced in install start, success,
177 failure UI (e.g. in IDS_MODULE_INSTALL_START_TEXT, which will expand to
178 'Installing Foo for Chrome…').">
179 Foo
180</message>
181...
182```
183
Samuel Huang7f2b53752019-05-23 15:10:05184*** note
185**Note:** This is for module title only. Other strings specific to the module
186should go in the module, not here (in the base module).
187***
188
Andrew Grieve912f66332023-04-11 16:11:56189Congrats! You added the DFM Foo to Chrome. That is a big step but not very
Tibor Goldschwendt19364ba2019-04-10 15:59:55190useful so far. In the next sections you'll learn how to add code and resources
191to it.
192
193
194### Building and installing modules
195
196Before we are going to jump into adding content to Foo, let's take a look on how
197to build and deploy the Monochrome bundle with the Foo DFM. The remainder of
198this guide assumes the environment variable `OUTDIR` is set to a properly
199configured GN build directory (e.g. `out/Debug`).
200
201To build and install the Monochrome bundle to your connected device, run:
202
203```shell
204$ autoninja -C $OUTDIR monochrome_public_bundle
Andrew Grievef0d977762021-08-18 20:20:43205$ $OUTDIR/bin/monochrome_public_bundle install -m foo
Tibor Goldschwendt19364ba2019-04-10 15:59:55206```
207
Andrew Grievef0d977762021-08-18 20:20:43208This will install the `Foo` module, the `base` module, and all modules with an
209`AndroidManifest.xml` that:
210 * Sets `<module dist:onDemand="false">`, or
211 * Has `<dist:delivery>` conditions that are satisfied by the device being
212 installed to.
Tibor Goldschwendt19364ba2019-04-10 15:59:55213
214*** note
Tibor Goldschwendtf430b272019-11-25 19:19:41215**Note:** The install script may install more modules than you specify, e.g.
216when there are default or conditionally installed modules (see
217[below](#conditional-install) for details).
Tibor Goldschwendt19364ba2019-04-10 15:59:55218***
219
220You can then check that the install worked with:
221
222```shell
223$ adb shell dumpsys package org.chromium.chrome | grep splits
224> splits=[base, config.en, foo]
225```
226
227Then try installing the Monochrome bundle without your module and print the
228installed modules:
229
230```shell
Andrew Grievef0d977762021-08-18 20:20:43231$ $OUTDIR/bin/monochrome_public_bundle install
Tibor Goldschwendt19364ba2019-04-10 15:59:55232$ adb shell dumpsys package org.chromium.chrome | grep splits
233> splits=[base, config.en]
234```
235
Andrew Grieve7e777abf2021-08-17 19:43:59236*** note
237The wrapper script's `install` command does approximately:
238```sh
Joanna Wang81bea752024-08-15 16:26:56239java -jar third_party/android_build_tools/bundletool/cipd/bundletool.jar build-apks --output tmp.apks ...
240java -jar third_party/android_build_tools/bundletool/cipd/bundletool.jar install-apks --apks tmp.apks
Andrew Grieve7e777abf2021-08-17 19:43:59241```
242
243The `install-apks` command uses `adb install-multiple` under-the-hood.
244***
Tibor Goldschwendt19364ba2019-04-10 15:59:55245
Samuel Huang3dc9fce82020-02-26 18:09:57246### Adding Java code
Tibor Goldschwendt19364ba2019-04-10 15:59:55247
248To make Foo useful, let's add some Java code to it. This section will walk you
249through the required steps.
250
Tibor Goldschwendt573cf3022019-05-10 17:23:30251First, define a module interface for Foo. This is accomplished by adding the
252`@ModuleInterface` annotation to the Foo interface. This annotation
253automatically creates a `FooModule` class that can be used later to install and
254access the module. To do this, add the following in the new file
Henrique Nakashimacfdcce32020-04-24 22:19:36255`//chrome/browser/foo/android/java/src/org/chromium/chrome/browser/foo/Foo.java`:
Tibor Goldschwendt19364ba2019-04-10 15:59:55256
257```java
Henrique Nakashimacfdcce32020-04-24 22:19:36258package org.chromium.chrome.browser.foo;
Tibor Goldschwendt19364ba2019-04-10 15:59:55259
Fred Mello2623e052019-10-02 20:18:04260import org.chromium.components.module_installer.builder.ModuleInterface;
Tibor Goldschwendt573cf3022019-05-10 17:23:30261
Tibor Goldschwendt19364ba2019-04-10 15:59:55262/** Interface to call into Foo feature. */
Henrique Nakashimacfdcce32020-04-24 22:19:36263@ModuleInterface(module = "foo", impl = "org.chromium.chrome.browser.FooImpl")
Tibor Goldschwendt19364ba2019-04-10 15:59:55264public interface Foo {
265 /** Magical function. */
266 void bar();
267}
268```
269
Tibor Goldschwendt19364ba2019-04-10 15:59:55270Next, define an implementation that goes into the module in the new file
Henrique Nakashimacfdcce32020-04-24 22:19:36271`//chrome/browser/foo/internal/android/java/src/org/chromium/chrome/browser/foo/FooImpl.java`:
Tibor Goldschwendt19364ba2019-04-10 15:59:55272
273```java
Henrique Nakashimacfdcce32020-04-24 22:19:36274package org.chromium.chrome.browser.foo;
Tibor Goldschwendt19364ba2019-04-10 15:59:55275
276import org.chromium.base.Log;
277
278public class FooImpl implements Foo {
279 @Override
280 public void bar() {
281 Log.i("FOO", "bar in module");
282 }
283}
284```
285
Tibor Goldschwendt19364ba2019-04-10 15:59:55286You can then use this provider to access the module if it is installed. To test
287that, instantiate Foo and call `bar()` somewhere in Chrome:
288
289```java
Tibor Goldschwendt573cf3022019-05-10 17:23:30290if (FooModule.isInstalled()) {
291 FooModule.getImpl().bar();
Tibor Goldschwendt19364ba2019-04-10 15:59:55292} else {
293 Log.i("FOO", "module not installed");
294}
295```
296
Tibor Goldschwendt573cf3022019-05-10 17:23:30297The interface has to be available regardless of whether the Foo DFM is present.
Henrique Nakashimacfdcce32020-04-24 22:19:36298Therefore, put those classes into the base module, creating a new public
299build target in: `//chrome/browser/foo/BUILD.gn`:
Tibor Goldschwendt19364ba2019-04-10 15:59:55300
301```gn
Henrique Nakashimacfdcce32020-04-24 22:19:36302import("//build/config/android/rules.gni")
303
304android_library("java") {
305 sources = [
306 "android/java/src/org/chromium/chrome/browser/foo/Foo.java",
307 ]
Mohamed Heikal826b4d52021-06-25 18:13:57308 deps = [
309 "//components/module_installer/android:module_installer_java",
310 "//components/module_installer/android:module_interface_java",
311 ]
312 annotation_processor_deps =
313 [ "//components/module_installer/android:module_interface_processor" ]
Henrique Nakashimacfdcce32020-04-24 22:19:36314}
Tibor Goldschwendt19364ba2019-04-10 15:59:55315```
316
Henrique Nakashimacfdcce32020-04-24 22:19:36317Then, depend on this target from where it is used as usual. For example, if the
318caller is in `chrome_java in //chrome/android/BUILD.gn`:
Tibor Goldschwendt19364ba2019-04-10 15:59:55319
320```gn
321...
Tibor Goldschwendt19364ba2019-04-10 15:59:55322android_library("chrome_java") {
Henrique Nakashimacfdcce32020-04-24 22:19:36323 deps =[
324 ...
325 "//chrome/browser/foo:java",
326 ...
327 ]
Tibor Goldschwendt19364ba2019-04-10 15:59:55328}
329...
330```
331
332The actual implementation, however, should go into the Foo DFM. For this
Henrique Nakashimacfdcce32020-04-24 22:19:36333purpose, create a new file `//chrome/browser/foo/internal/BUILD.gn` and
Tibor Goldschwendt68c5f722019-08-01 15:10:15334make a library with the module Java code in it:
Tibor Goldschwendt19364ba2019-04-10 15:59:55335
336```gn
337import("//build/config/android/rules.gni")
338
339android_library("java") {
340 # Define like ordinary Java Android library.
Natalie Chouinardcbdc6dc2019-12-24 00:02:35341 sources = [
Henrique Nakashimacfdcce32020-04-24 22:19:36342 "android/java/src/org/chromium/chrome/browser/foo/FooImpl.java",
Tibor Goldschwendt19364ba2019-04-10 15:59:55343 # Add other Java classes that should go into the Foo DFM here.
344 ]
Fred Mellob32b3022019-06-21 18:10:11345 deps = [
Tibor Goldschwendt19364ba2019-04-10 15:59:55346 "//base:base_java",
Henrique Nakashimacfdcce32020-04-24 22:19:36347 # Put other Chrome libs into the classpath so that you can call into them
348 # from the Foo DFM.
349 "//chrome/browser/bar:java",
350 # The module can depend even on `chrome_java` due to factory magic, but this
351 # is discouraged. Consider passing a delegate interface in instead.
Tibor Goldschwendt19364ba2019-04-10 15:59:55352 "//chrome/android:chrome_java",
Tibor Goldschwendt19364ba2019-04-10 15:59:55353 # Also, you'll need to depend on any //third_party or //components code you
354 # are using in the module code.
355 ]
356}
357```
358
Tibor Goldschwendtaef8e392019-07-19 16:39:10359Then, add this new library as a dependency of the Foo module descriptor in
Henrique Nakashimacfdcce32020-04-24 22:19:36360`//chrome/android/modules/foo/foo_module.gni`:
Tibor Goldschwendt19364ba2019-04-10 15:59:55361
362```gn
Tibor Goldschwendtaef8e392019-07-19 16:39:10363foo_module_desc = {
Tibor Goldschwendt19364ba2019-04-10 15:59:55364 ...
Tibor Goldschwendtaef8e392019-07-19 16:39:10365 java_deps = [
Henrique Nakashimacfdcce32020-04-24 22:19:36366 "//chrome/browser/foo/internal:java",
Tibor Goldschwendt19364ba2019-04-10 15:59:55367 ]
368}
369```
370
371Finally, tell Android that your module is now containing code. Do that by
Samuel Huang39c7db632019-05-15 14:57:18372removing the `android:hasCode="false"` attribute from the `<application>` tag in
Henrique Nakashimacfdcce32020-04-24 22:19:36373`//chrome/android/modules/foo/internal/java/AndroidManifest.xml`. You should be
Tibor Goldschwendt68c5f722019-08-01 15:10:15374left with an empty tag like so:
Tibor Goldschwendt19364ba2019-04-10 15:59:55375
376```xml
377...
378 <application />
379...
380```
381
382Rebuild and install `monochrome_public_bundle`. Start Chrome and run through a
383flow that tries to executes `bar()`. Depending on whether you installed your
384module (`-m foo`) "`bar in module`" or "`module not installed`" is printed to
385logcat. Yay!
386
Christopher Grantf649d282020-01-09 22:56:08387### Adding pre-built native libraries
Tibor Goldschwendt19364ba2019-04-10 15:59:55388
Christopher Grant8fea5a12019-07-31 19:12:31389You can add a third-party native library (or any standalone library that doesn't
390depend on Chrome code) by adding it as a loadable module to the module descriptor in
Henrique Nakashimacfdcce32020-04-24 22:19:36391`//chrome/android/moduiles/foo/foo_module.gni`:
Tibor Goldschwendt19364ba2019-04-10 15:59:55392
393```gn
Tibor Goldschwendtaef8e392019-07-19 16:39:10394foo_module_desc = {
Tibor Goldschwendt19364ba2019-04-10 15:59:55395 ...
Tibor Goldschwendtaef8e392019-07-19 16:39:10396 loadable_modules_32_bit = [ "//path/to/32/bit/lib.so" ]
397 loadable_modules_64_bit = [ "//path/to/64/bit/lib.so" ]
Tibor Goldschwendt19364ba2019-04-10 15:59:55398}
399```
400
Christopher Grant8fea5a12019-07-31 19:12:31401### Adding Chrome native code
Tibor Goldschwendt19364ba2019-04-10 15:59:55402
Christopher Grantf649d282020-01-09 22:56:08403Chrome native code may be placed in a DFM. The easiest way to access native
404feature code is by calling it from Java via JNI. When a module is first
405accessed, its native library (or potentially libraries, if using a component
406build), are automatically opened by the DFM framework, and a feature-specific
407JNI method (supplied by the feature's implementation) is invoked. Hence, a
408module's Java code may freely use JNI to call module native code.
Christopher Grant8fea5a12019-07-31 19:12:31409
Christopher Grantf649d282020-01-09 22:56:08410Using the module framework and JNI to access the native code eliminates concerns
411with DFM library file names (which vary across build variants),
412`android_dlopen_ext()` (needed to open feature libraries), and use of dlsym().
Christopher Grant8fea5a12019-07-31 19:12:31413
Christopher Grantf649d282020-01-09 22:56:08414This mechanism can be extended if necessary by DFM implementers to facilitate
415subsequent native-native calls, by having a JNI-called initialization method
416create instance of a object or factory, and register it through a call to the
417base module's native code (DFM native code can call base module code directly).
Christopher Grant8fea5a12019-07-31 19:12:31418
Eric Stevenson8c9ab26b2019-08-30 15:44:40419#### JNI
420
Sam Maiere1df6f22023-08-11 14:20:40421Read the `jni_generator` [docs](../third_party/jni_zero/README.md) before
Eric Stevenson8c9ab26b2019-08-30 15:44:40422reading this section.
423
424There are some subtleties to how JNI registration works with DFMs:
425
426* Generated wrapper `ClassNameJni` classes are packaged into the DFM's dex file
Sam Maier79e40da2023-01-27 17:26:11427* The class containing the actual native definitions,
428 `<module_name>_GEN_JNI.java`, is currently stored in the base module, but
429 could be moved out
430* The `Natives` interface you provide will need to be annotated with your module
431 name as an argument to `NativeMethods`, eg. `@NativeMethods("foo")`, resulting
432 in a uniquely named `foo_GEN_JNI.java`
433* The DFM will need to provide a `generate_jni_registration` target
Eric Stevenson8c9ab26b2019-08-30 15:44:40434 that will generate all of the native registration functions
435
Christopher Grantf649d282020-01-09 22:56:08436#### Calling DFM native code via JNI
437
438A linker-assisted partitioning system automates the placement of code into
439either the main Chrome library or feature-specific .so libraries. Feature code
440may continue to make use of core Chrome code (eg. base::) without modification,
441but Chrome must call feature code through a virtual interface (any "direct"
442calls to the feature code from the main library will cause the feature code to
443be pulled back into the main library).
444
445Partitioning is explained in [Android Native
446Libraries](android_native_libraries.md#partitioned-libraries).
447
448First, build a module native interface. Supply a JNI method named
449`JNI_OnLoad_foo` for the module framework to call, in
450`//chrome/android/modules/foo/internal/entrypoints.cc`. This method is invoked
451on all Chrome build variants, including Monochrome (unlike base module JNI).
452
453```c++
Sam Maieraa615182024-02-13 19:56:17454#include "third_party/jni_zero/jni_zero_helper.h"
Christopher Grantf649d282020-01-09 22:56:08455#include "base/android/jni_utils.h"
456#include "chrome/android/modules/foo/internal/jni_registration.h"
457
458extern "C" {
459// This JNI registration method is found and called by module framework code.
Andrew Grieveff18140b2024-11-25 17:29:19460JNI_ZERO_BOUNDARY_EXPORT bool JNI_OnLoad_foo(JNIEnv* env) {
Sam Maierb0bd6d92023-04-05 13:22:23461 if (!foo::RegisterNatives(env)) {
Christopher Grantf649d282020-01-09 22:56:08462 return false;
463 }
464 return true;
465}
466} // extern "C"
467```
468
469Next, include the module entrypoint and related pieces in the build config at
470`//chrome/android/modules/foo/internal/BUILD.gn`:
471
472```gn
473import("//build/config/android/rules.gni")
474import("//chrome/android/modules/buildflags.gni")
475...
476
477# Put the JNI entrypoint in a component, so that the component build has a
478# library to include in the foo module. This makes things feel consistent with
479# a release build.
480component("foo") {
481 sources = [
482 "entrypoints.cc",
483 ]
484 deps = [
485 ":jni_registration",
Christopher Grantf649d282020-01-09 22:56:08486 "//base",
Henrique Nakashimacfdcce32020-04-24 22:19:36487 "//chrome/browser/foo/internal:native",
Christopher Grantf649d282020-01-09 22:56:08488 ]
489
490 # Instruct the compiler to flag exported entrypoint function as belonging in
491 # foo's library. The linker will use this information when creating the
492 # native libraries. The partition name must be <feature>_partition.
493 if (use_native_partitions) {
494 cflags = [ "-fsymbol-partition=foo_partition" ]
495 }
496}
497
498# Generate JNI registration for the methods called by the Java side. Note the
499# no_transitive_deps argument, which ensures that JNI is generated for only the
500# specified Java target, and not all its transitive deps (which could include
501# the base module).
502generate_jni_registration("jni_registration") {
Henrique Nakashimacfdcce32020-04-24 22:19:36503 targets = [ "//chrome/browser/foo/internal:java" ]
Christopher Grantf649d282020-01-09 22:56:08504 namespace = "foo"
505 no_transitive_deps = true
Sam Maier79e40da2023-01-27 17:26:11506 manual_jni_registration = true
Christopher Grantf649d282020-01-09 22:56:08507}
508
509# This group is a convenience alias representing the module's native code,
510# allowing it to be named "native" for clarity in module descriptors.
511group("native") {
512 deps = [
513 ":foo",
514 ]
515}
516```
517
518Now, over to the implementation of the module. These are the parts that
519shouldn't know or care whether they're living in a module or not.
520
521Add a stub implementation in
Henrique Nakashimacfdcce32020-04-24 22:19:36522`//chrome/browser/foo/internal/android/foo_impl.cc`:
Christopher Grantf649d282020-01-09 22:56:08523
524```c++
525#include "base/logging.h"
Henrique Nakashimacfdcce32020-04-24 22:19:36526#include "chrome/browser/foo/internal/jni_headers/FooImpl_jni.h"
Christopher Grantf649d282020-01-09 22:56:08527
528static int JNI_FooImpl_Execute(JNIEnv* env) {
529 LOG(INFO) << "Running foo feature code!";
530 return 123;
531}
532```
533
534And, the associated build config in
Henrique Nakashimacfdcce32020-04-24 22:19:36535`//chrome/browser/foo/internal/BUILD.gn`:
Christopher Grantf649d282020-01-09 22:56:08536
537```gn
538import("//build/config/android/rules.gni")
539
540...
541
542source_set("native") {
543 sources = [
Henrique Nakashimacfdcce32020-04-24 22:19:36544 "android/foo_impl.cc",
Christopher Grantf649d282020-01-09 22:56:08545 ]
546
547 deps = [
548 ":jni_headers",
549 "//base",
550 ]
551}
552
553generate_jni("jni_headers") {
554 sources = [
Henrique Nakashimacfdcce32020-04-24 22:19:36555 "android/java/src/org/chromium/chrome/browser/foo/FooImpl.java",
Christopher Grantf649d282020-01-09 22:56:08556 ]
557}
558```
559
560With a declaration of the native method on the Java side:
561
562```java
563public class FooImpl implements Foo {
564 ...
565
Sam Maier79e40da2023-01-27 17:26:11566 @NativeMethods("foo")
Christopher Grantf649d282020-01-09 22:56:08567 interface Natives {
568 int execute();
569 }
570}
571```
572
573Finally, augment the module descriptor in
574`//chrome/android/modules/foo/foo_module.gni` with the native dependencies:
575
576```gn
577foo_module_desc = {
578 ...
579 native_deps = [
Christopher Grantf649d282020-01-09 22:56:08580 "//chrome/android/modules/foo/internal:native",
Henrique Nakashimacfdcce32020-04-24 22:19:36581 "//chrome/browser/foo/internal:native",
Christopher Grantf649d282020-01-09 22:56:08582 ]
Samuel Huang3dc9fce82020-02-26 18:09:57583 load_native_on_get_impl = true
Christopher Grantf649d282020-01-09 22:56:08584}
585```
586
Samuel Huang3dc9fce82020-02-26 18:09:57587If `load_native_on_get_impl` is set to `true` then Chrome automatically loads
588Foo DFM's native libraries and PAK file resources when `FooModule.getImpl()` is
589called for the first time. The loading requires Chrome's main native libraries
590to be loaded. If you wish to call `FooModule.getImpl()` earlier than that, then
591you'd need to set `load_native_on_get_impl` to `false`, and manage native
592libraries / resources loading yourself (potentially, on start-up and on install,
593or on use).
594
Christopher Grantf649d282020-01-09 22:56:08595#### Calling feature module native code from base the module
596
597If planning to use direct native-native calls into DFM code, then the module
598should have a purely virtual interface available. The main module can obtain a
599pointer to a DFM-created object or factory (implemented by the feature), and
600call its virtual methods.
601
602Ideally, the interface to the feature will avoid feature-specific types. If a
Jennifer Nejad1a8e2082024-10-15 23:30:32603feature defines complex data types, and uses them in its own interface, then it's
Christopher Grantf649d282020-01-09 22:56:08604likely the main library will utilize the code backing these types. That code,
605and anything it references, will in turn be pulled back into the main library,
606negating the intent to house code in the DFM.
607
608Therefore, designing the feature interface to use C types, C++ standard types,
609or classes that aren't expected to move out of Chrome's main library is ideal.
610If feature-specific classes are needed, they simply need to avoid referencing
611feature library internals.
Eric Stevenson8c9ab26b2019-08-30 15:44:40612
Christopher Grant8fea5a12019-07-31 19:12:31613### Adding Android resources
Tibor Goldschwendt19364ba2019-04-10 15:59:55614
615In this section we will add the required build targets to add Android resources
616to the Foo DFM.
617
Tibor Goldschwendt68c5f722019-08-01 15:10:15618First, add a resources target to
Henrique Nakashimacfdcce32020-04-24 22:19:36619`//chrome/browser/foo/internal/BUILD.gn` and add it as a dependency on
Tibor Goldschwendt68c5f722019-08-01 15:10:15620Foo's `java` target in the same file:
Tibor Goldschwendt19364ba2019-04-10 15:59:55621
622```gn
623...
624android_resources("java_resources") {
625 # Define like ordinary Android resources target.
626 ...
Henrique Nakashimacfdcce32020-04-24 22:19:36627 custom_package = "org.chromium.chrome.browser.foo"
Tibor Goldschwendt19364ba2019-04-10 15:59:55628}
629...
630android_library("java") {
631 ...
632 deps = [
633 ":java_resources",
634 ]
635}
636```
637
638To add strings follow steps
639[here](http://dev.chromium.org/developers/design-documents/ui-localization) to
640add new Java GRD file. Then create
Henrique Nakashimacfdcce32020-04-24 22:19:36641`//chrome/browser/foo/internal/android/resources/strings/android_foo_strings.grd` as
Tibor Goldschwendt68c5f722019-08-01 15:10:15642follows:
Tibor Goldschwendt19364ba2019-04-10 15:59:55643
644```xml
645<?xml version="1.0" encoding="UTF-8"?>
dpapadbfad4d42025-01-14 09:29:31646<grit current_release="1" latest_public_release="0">
Tibor Goldschwendt19364ba2019-04-10 15:59:55647 <outputs>
648 <output
649 filename="values-am/android_foo_strings.xml"
650 lang="am"
651 type="android" />
652 <!-- List output file for all other supported languages. See
Henrique Nakashimaf5439f82021-01-29 23:52:24653 //chrome/browser/ui/android/strings/android_chrome_strings.grd for the
654 full list. -->
Tibor Goldschwendt19364ba2019-04-10 15:59:55655 ...
656 </outputs>
657 <translations>
658 <file lang="am" path="vr_translations/android_foo_strings_am.xtb" />
659 <!-- Here, too, list XTB files for all other supported languages. -->
660 ...
661 </translations>
Matt Stark1debb5de2021-02-15 16:08:24662 <release seq="1">
Tibor Goldschwendt19364ba2019-04-10 15:59:55663 <messages fallback_to_english="true">
664 <message name="IDS_BAR_IMPL_TEXT" desc="Magical string.">
665 impl
666 </message>
667 </messages>
668 </release>
669</grit>
670```
671
672Then, create a new GRD target and add it as a dependency on `java_resources` in
Henrique Nakashimacfdcce32020-04-24 22:19:36673`//chrome/browser/foo/internal/BUILD.gn`:
Tibor Goldschwendt19364ba2019-04-10 15:59:55674
675```gn
676...
677java_strings_grd("java_strings_grd") {
678 defines = chrome_grit_defines
Henrique Nakashimacfdcce32020-04-24 22:19:36679 grd_file = "android/resources/strings/android_foo_strings.grd"
Tibor Goldschwendt19364ba2019-04-10 15:59:55680 outputs = [
681 "values-am/android_foo_strings.xml",
682 # Here, too, list output files for other supported languages.
683 ...
684 ]
685}
686...
687android_resources("java_resources") {
688 ...
689 deps = [":java_strings_grd"]
Henrique Nakashimacfdcce32020-04-24 22:19:36690 custom_package = "org.chromium.chrome.browser.foo"
Tibor Goldschwendt19364ba2019-04-10 15:59:55691}
692...
693```
694
695You can then access Foo's resources using the
Henrique Nakashimacfdcce32020-04-24 22:19:36696`org.chromium.chrome.browser.foo.R` class. To do this change
697`//chrome/browser/foo/internal/android/java/src/org/chromium/chrome/browser/foo/FooImpl.java`
Tibor Goldschwendt19364ba2019-04-10 15:59:55698to:
699
700```java
Henrique Nakashimacfdcce32020-04-24 22:19:36701package org.chromium.chrome.browser.foo;
Tibor Goldschwendt19364ba2019-04-10 15:59:55702
703import org.chromium.base.ContextUtils;
704import org.chromium.base.Log;
Henrique Nakashimacfdcce32020-04-24 22:19:36705import org.chromium.chrome.browser.foo.R;
Tibor Goldschwendt19364ba2019-04-10 15:59:55706
707public class FooImpl implements Foo {
708 @Override
709 public void bar() {
710 Log.i("FOO", ContextUtils.getApplicationContext().getString(
711 R.string.bar_impl_text));
712 }
713}
714```
715
Samuel Huang3dc9fce82020-02-26 18:09:57716### Adding non-string native resources
717
718This section describes how to add non-string native resources to Foo DFM.
719Key ideas:
720
721* The compiled resource file shipped with the DFM is `foo_resourcess.pak`.
722* At run time, native resources need to be loaded before use. Also, DFM native
723 resources can only be used from the Browser process.
724
725#### Creating PAK file
726
727Two ways to create `foo_resourcess.pak` (using GRIT) are:
728
7291. (Preferred) Use `foo_resourcess.grd` to refer to individual files (e.g.,
730 images, HTML, JS, or CSS) and assigns resource text IDs. `foo_resourcess.pak`
731 must have an entry in `/tools/gritsettings/resource_ids.spec`.
7321. Combine existing .pak files via `repack` rules in GN build files. This is
733 done by the DevUI DFM, which aggregates resources from many DevUI pages.
734
735#### Loading PAK file
736
737At runtime, `foo_resources.pak` needs to be loaded (memory-mapped) before any of
738its resource gets used. Alternatives to do this are:
739
7401. (Simplest) Specify native resources (with native libraries if any exist) to
741 be automatically loaded on first call to `FooModule.getImpl()`. This behavior
742 is specified via `load_native_on_get_impl = true` in `foo_module_desc`.
7431. In Java code, call `FooModule.ensureNativeLoaded()`.
7441. In C++ code, use JNI to call `FooModule.ensureNativeLoaded()`. The code to do
745 this can be placed in a helper class, which can also have JNI calls to
746 `FooModule.isInstalled()` and `FooModule.installModule()`.
747
748#### Cautionary notes
749
750Compiling `foo_resources.pak` auto-generates `foo_resources.h`, which defines
751textual resource IDs, e.g., `IDR_FOO_HTML`. C++ code then uses these IDs to get
752resource bytes. Unfortunately, this behavior is fragile: If `IDR_FOO_HTML` is
753accessed before the Foo DFM is (a) installed, or (b) loaded, then runtime error
754ensues! Some mitigation strategies are as follows:
755
756* (Ideal) Access Foo DFM's native resources only from code in Foo DFM's native
757 libraries. So by the time that `IDR_FOO_HTML` is accessed, everything is
758 already in place! This isn't always possible; henceforth we assume that
759 `IDR_FOO_HTML` is accessed by code in the base DFM.
760* Before accessing IDR_FOO_HTML, ensure Foo DFM is installed and loaded. The
761 latter can use `FooModule.ensureNativeLoaded()` (needs to be called from
762 Browser thread).
763* Use inclusion of `foo_resources.h` to restrict availability of `IDR_FOO_HTML`.
764 Only C++ files dedicated to "DFM-gated code" (code that runs only when its DFM
765 is installed and loaded) should include `foo_resources.h`.
766
767#### Associating native resources with DFM
768
769Here are the main GN changes to specify PAK files and default loading behavior
770for a DFM's native resources:
771
772```gn
773foo_module_desc = {
774 ...
Henrique Nakashimacfdcce32020-04-24 22:19:36775 paks = [ "$root_gen_dir/chrome/browser/foo/internal/foo_resourcess.pak" ]
776 pak_deps = [ "//chrome/browser/foo/internal:foo_paks" ]
Samuel Huang3dc9fce82020-02-26 18:09:57777 load_native_on_get_impl = true
778}
779```
780
781Note that `load_native_on_get_impl` specifies both native libraries and native
782resources.
783
Tibor Goldschwendt19364ba2019-04-10 15:59:55784
785### Module install
786
787So far, we have installed the Foo DFM as a true split (`-m foo` option on the
788install script). In production, however, we have to explicitly install the Foo
Tibor Goldschwendt68c5f722019-08-01 15:10:15789DFM for users to get it. There are three install options: _on-demand_,
790_deferred_ and _conditional_.
Tibor Goldschwendt19364ba2019-04-10 15:59:55791
Tibor Goldschwendt19364ba2019-04-10 15:59:55792#### On-demand install
793
794On-demand requesting a module will try to download and install the
795module as soon as possible regardless of whether the user is on a metered
796connection or whether they have turned updates off in the Play Store app.
797
Tibor Goldschwendt573cf3022019-05-10 17:23:30798You can use the autogenerated module class to on-demand install the module like
799so:
Tibor Goldschwendt19364ba2019-04-10 15:59:55800
801```java
Tibor Goldschwendt573cf3022019-05-10 17:23:30802FooModule.install((success) -> {
803 if (success) {
804 FooModule.getImpl().bar();
805 }
Tibor Goldschwendt19364ba2019-04-10 15:59:55806});
807```
808
809**Optionally**, you can show UI telling the user about the install flow. For
Tibor Goldschwendt573cf3022019-05-10 17:23:30810this, add a function like the one below. Note, it is possible
Tibor Goldschwendt19364ba2019-04-10 15:59:55811to only show either one of the install, failure and success UI or any
812combination of the three.
813
814```java
815public static void installModuleWithUi(
816 Tab tab, OnModuleInstallFinishedListener onFinishedListener) {
817 ModuleInstallUi ui =
818 new ModuleInstallUi(
819 tab,
820 R.string.foo_module_title,
821 new ModuleInstallUi.FailureUiListener() {
822 @Override
Samuel Huangfebcccd2019-08-21 20:48:47823 public void onFailureUiResponse(retry) {
824 if (retry) {
825 installModuleWithUi(tab, onFinishedListener);
826 } else {
827 onFinishedListener.onFinished(false);
828 }
Tibor Goldschwendt19364ba2019-04-10 15:59:55829 }
830 });
831 // At the time of writing, shows toast informing user about install start.
832 ui.showInstallStartUi();
Tibor Goldschwendt573cf3022019-05-10 17:23:30833 FooModule.install(
Tibor Goldschwendt19364ba2019-04-10 15:59:55834 (success) -> {
835 if (!success) {
836 // At the time of writing, shows infobar allowing user
837 // to retry install.
838 ui.showInstallFailureUi();
839 return;
840 }
841 // At the time of writing, shows toast informing user about
842 // install success.
843 ui.showInstallSuccessUi();
844 onFinishedListener.onFinished(true);
845 });
846}
847```
848
849To test on-demand install, "fake-install" the DFM. It's fake because
Peter Wen8bf82d42021-08-13 22:03:54850the DFM is not installed as a true split. Instead it will be emulated by play
851core's `--local-testing` [mode][play-core-local-testing].
Tibor Goldschwendt19364ba2019-04-10 15:59:55852Fake-install and launch Chrome with the following command:
853
854```shell
Andrew Grievef0d977762021-08-18 20:20:43855$ $OUTDIR/bin/monochrome_public_bundle install -f foo
Peter Wen8bf82d42021-08-13 22:03:54856$ $OUTDIR/bin/monochrome_public_bundle launch
Tibor Goldschwendt19364ba2019-04-10 15:59:55857```
858
859When running the install code, the Foo DFM module will be emulated.
860This will be the case in production right after installing the module. Emulation
861will last until Play Store has a chance to install your module as a true split.
Peter Wen577a6fe52019-12-11 22:02:05862This usually takes about a day. After it has been installed, it will be updated
863atomically alongside Chrome. Always check that it is installed and available
864before invoking code within the DFM.
Tibor Goldschwendt19364ba2019-04-10 15:59:55865
866*** note
867**Warning:** There are subtle differences between emulating a module and
868installing it as a true split. We therefore recommend that you always test both
869install methods.
870***
871
Samuel Huang6f5c7ddb82020-05-14 17:10:52872*** note
873To simplify development, the DevUI DFM (dev_ui) is installed by default, i.e.,
874`-m dev_ui` is implied by default. This is overridden by:
875* `--no-module dev_ui`, to test error from missing DevUI,
876* `-f dev_ui`, for fake module install.
877***
Tibor Goldschwendt19364ba2019-04-10 15:59:55878
879#### Deferred install
880
881Deferred install means that the DFM is installed in the background when the
882device is on an unmetered connection and charging. The DFM will only be
883available after Chrome restarts. When deferred installing a module it will
884not be faked installed.
885
886To defer install Foo do the following:
887
888```java
Tibor Goldschwendt573cf3022019-05-10 17:23:30889FooModule.installDeferred();
Tibor Goldschwendt19364ba2019-04-10 15:59:55890```
891
Tibor Goldschwendt68c5f722019-08-01 15:10:15892#### Conditional install
893
894Conditional install means the DFM will be installed automatically upon first
895installing or updating Chrome if the device supports a particular feature.
896Conditional install is configured in the module's manifest. To install your
897module on all Daydream-ready devices for instance, your
Henrique Nakashimacfdcce32020-04-24 22:19:36898`//chrome/android/modules/foo/internal/java/AndroidManifest.xml` should look
Tibor Goldschwendt68c5f722019-08-01 15:10:15899like this:
900
901```xml
902<?xml version="1.0" encoding="utf-8"?>
903<manifest xmlns:android="http://schemas.android.com/apk/res/android"
904 xmlns:dist="http://schemas.android.com/apk/distribution"
905 featureSplit="foo">
906
907 <dist:module
908 dist:instant="false"
909 dist:title="@string/foo_module_title">
Ben Masone571ea5a2019-09-06 18:29:37910 <dist:fusing dist:include="true" />
Tibor Goldschwendt68c5f722019-08-01 15:10:15911 <dist:delivery>
912 <dist:install-time>
913 <dist:conditions>
914 <dist:device-feature
915 dist:name="android.hardware.vr.high_performance" />
916 </dist:conditions>
917 </dist:install-time>
918 <!-- Allows on-demand or deferred install on non-Daydream-ready
919 devices. -->
920 <dist:on-demand />
921 </dist:delivery>
922 </dist:module>
923
924 <application />
925</manifest>
926```
927
Andrew Grievef6069feb2021-04-29 18:29:17928You can also specify no conditions to have your module always installed.
929You might want to do this in order to delay the performance implications
930of loading your module until its first use (true only on Android O+ where
931[android:isolatedSplits](https://developer.android.com/reference/android/R.attr#isolatedSplits)
932is supported. See [go/isolated-splits-dev-guide](http://go/isolated-splits-dev-guide)
933(googlers only).
934
Andrew Grievef6069feb2021-04-29 18:29:17935### chrome_public_apk and Integration Tests
Tibor Goldschwendt19364ba2019-04-10 15:59:55936
Andrew Grievef6069feb2021-04-29 18:29:17937To make the Foo feature available in the non-bundle `chrome_public_apk`
Andrew Grieve982fe8a2024-01-02 16:41:51938target, add the `java` target to the template in
939`//chrome/android/chrome_public_apk_tmpl.gni` like so:
Tibor Goldschwendt19364ba2019-04-10 15:59:55940
941```gn
Andrew Grieve982fe8a2024-01-02 16:41:51942 # Add to where "chrome_all_java" is added:
943 if (!_is_bundle) {
944 deps += [ "//chrome/browser/foo/internal:java" ]
Tibor Goldschwendt19364ba2019-04-10 15:59:55945 }
946}
947```
948
Martin Kongdfea3ec2025-01-16 21:40:43949You may also have to add `java` as a dependency of
950`//chrome/android/javatests/chrome_test_java_org.chromium.chrome.browser.foo`
951if you want to call into Foo from test code.
Peter Wen8bf82d42021-08-13 22:03:54952
Mohamed Heikalf9d9edb2021-10-04 20:05:10953[play-core-local-testing]: https://developer.android.com/guide/playcore/feature-delivery/on-demand#local-testing