blob: 4096ef30a189c096fa884c9b742b1999c89776c1 [file] [log] [blame] [view]
Tibor Goldschwendt19364ba2019-04-10 15:59:551# Dynamic Feature Modules (DFMs)
2
3[Android App bundles and Dynamic Feature Modules (DFMs)](https://developer.android.com/guide/app-bundle)
4is a Play Store feature that allows delivering pieces of an app when they are
5needed rather than at install time. We use DFMs to modularize Chrome and make
6Chrome's install size smaller.
7
8[TOC]
9
10
11## Limitations
12
13Currently (March 2019), DFMs have the following limitations:
14
15* **WebView:** We don't support DFMs for WebView. If your feature is used by
16 WebView you cannot put it into a DFM. See
17 [crbug/949717](https://bugs.chromium.org/p/chromium/issues/detail?id=949717)
18 for progress.
19* **Android K:** DFMs are based on split APKs, a feature introduced in Android
20 L. Therefore, we don't support DFMs on Android K. As a workaround
21 you can add your feature to the Android K APK build. See
22 [crbug/881354](https://bugs.chromium.org/p/chromium/issues/detail?id=881354)
23 for progress.
24* **Native Code:** We cannot move native Chrome code into a DFM. See
25 [crbug/874564](https://bugs.chromium.org/p/chromium/issues/detail?id=874564)
26 for progress.
27
28## Getting started
29
30This guide walks you through the steps to create a DFM called _Foo_ and add it
31to the public Monochrome bundle. If you want to ship a DFM, you will also have
32to add it to the public Chrome Modern and Trichrome Chrome bundle as well as the
33downstream bundles.
34
35*** note
36**Note:** To make your own module you'll essentially have to replace every
37instance of `foo`/`Foo`/`FOO` with `your_feature_name`/`YourFeatureName`/
38`YOUR_FEATURE_NAME`.
39***
40
41
42### Create DFM target
43
44DFMs are APKs. They have a manifest and can contain Java and native code as well
45as resources. This section walks you through creating the module target in our
46build system.
47
48First, create the file `//chrome/android/features/foo/java/AndroidManifest.xml`
49and add:
50
51```xml
52<manifest xmlns:android="http://schemas.android.com/apk/res/android"
53 xmlns:dist="http://schemas.android.com/apk/distribution"
Tibor Goldschwendt5172118f2019-06-24 21:57:4754 featureSplit="foo">
Tibor Goldschwendt19364ba2019-04-10 15:59:5555
Tibor Goldschwendt19364ba2019-04-10 15:59:5556 <!-- dist:onDemand="true" makes this a separately installed module.
57 dist:onDemand="false" would always install the module alongside the
58 rest of Chrome. -->
59 <dist:module
60 dist:onDemand="true"
61 dist:title="@string/foo_module_title">
62 <!-- This will prevent the module to become part of the Android K
63 build in case we ever want to use bundles on Android K. -->
64 <dist:fusing dist:include="false" />
65 </dist:module>
66
Samuel Huang39c7db632019-05-15 14:57:1867 <!-- Remove android:hasCode="false" when adding Java code. -->
68 <application android:hasCode="false" />
Tibor Goldschwendt19364ba2019-04-10 15:59:5569</manifest>
70```
71
72Then, add a package ID for Foo so that Foo's resources have unique identifiers.
73For this, add a new ID to
Samuel Huang39c7db632019-05-15 14:57:1874`//chrome/android/features/dynamic_feature_modules.gni`:
Tibor Goldschwendt19364ba2019-04-10 15:59:5575
76```gn
77resource_packages_id_mapping = [
78 ...,
79 "foo=0x{XX}", # Set {XX} to next lower hex number.
80]
81```
82
83Next, create a template that contains the Foo module target.
84
85*** note
86**Note:** We put the module target into a template because we have to
87instantiate it for each Chrome bundle (Chrome Modern, Monochrome and Trichrome
88for both upstream and downstream) you want to ship your module in.
89***
90
91To do this, create `//chrome/android/features/foo/foo_module_tmpl.gni` and add
92the following:
93
94```gn
95import("//build/config/android/rules.gni")
96import("//build/config/locales.gni")
Samuel Huang39c7db632019-05-15 14:57:1897import("//chrome/android/features/dynamic_feature_modules.gni")
Tibor Goldschwendt19364ba2019-04-10 15:59:5598
99template("foo_module_tmpl") {
Tibor Goldschwendt19364ba2019-04-10 15:59:55100 android_app_bundle_module(target_name) {
101 forward_variables_from(invoker,
102 [
103 "base_module_target",
104 "module_name",
105 "uncompress_shared_libraries",
106 "version_code",
107 "version_name",
108 ])
Tibor Goldschwendt5172118f2019-06-24 21:57:47109 android_manifest = "//chrome/android/features/foo/java/AndroidManifest.xml"
110 min_sdk_version = 21
111 target_sdk_version = android_sdk_version
Tibor Goldschwendt19364ba2019-04-10 15:59:55112 proguard_enabled = !is_java_debug
113 aapt_locale_whitelist = locales
114 package_name = "foo"
115 package_name_to_id_mapping = resource_packages_id_mapping
116 }
117}
118```
119
120Then, instantiate the module template in `//chrome/android/BUILD.gn` inside the
Samuel Huang39c7db632019-05-15 14:57:18121`monochrome_or_trichrome_public_bundle_tmpl` template and add it to the bundle
122target:
Tibor Goldschwendt19364ba2019-04-10 15:59:55123
124```gn
125...
126import("modules/foo/foo_module_tmpl.gni")
127...
Samuel Huang39c7db632019-05-15 14:57:18128template("monochrome_or_trichrome_public_bundle_tmpl") {
Tibor Goldschwendt19364ba2019-04-10 15:59:55129 ...
130 foo_module_tmpl("${target_name}__foo_bundle_module") {
131 manifest_package = manifest_package
132 module_name = "Foo" + _bundle_name
133 base_module_target = ":$_base_module_target_name"
Tibor Goldschwendt19364ba2019-04-10 15:59:55134 uncompress_shared_libraries = true
Samuel Huang39c7db632019-05-15 14:57:18135 version_code = _version_code
136 version_name = _version_name
Tibor Goldschwendt19364ba2019-04-10 15:59:55137 }
138 ...
139 android_app_bundle(target_name) {
140 ...
141 extra_modules += [
142 {
143 name = "foo"
144 module_target = ":${target_name}__foo_bundle_module"
145 },
146 ]
147 }
148}
149```
150
151The next step is to add Foo to the list of feature modules for UMA recording.
152For this, add `foo` to the `AndroidFeatureModuleName` in
153`//tools/metrics/histograms/histograms.xml`:
154
155```xml
156<histogram_suffixes name="AndroidFeatureModuleName" ...>
157 ...
158 <suffix name="foo" label="Super Duper Foo Module" />
159 ...
160</histogram_suffixes>
161```
162
163<!--- TODO(tiborg): Add info about install UI. -->
164Lastly, give your module a title that Chrome and Play can use for the install
165UI. To do this, add a string to
166`//chrome/android/java/strings/android_chrome_strings.grd`:
167
168```xml
169...
170<message name="IDS_FOO_MODULE_TITLE"
171 desc="Text shown when the Foo module is referenced in install start, success,
172 failure UI (e.g. in IDS_MODULE_INSTALL_START_TEXT, which will expand to
173 'Installing Foo for Chrome…').">
174 Foo
175</message>
176...
177```
178
Samuel Huang7f2b53752019-05-23 15:10:05179*** note
180**Note:** This is for module title only. Other strings specific to the module
181should go in the module, not here (in the base module).
182***
183
Tibor Goldschwendt19364ba2019-04-10 15:59:55184Congrats! You added the DFM Foo to Monochrome. That is a big step but not very
185useful so far. In the next sections you'll learn how to add code and resources
186to it.
187
188
189### Building and installing modules
190
191Before we are going to jump into adding content to Foo, let's take a look on how
192to build and deploy the Monochrome bundle with the Foo DFM. The remainder of
193this guide assumes the environment variable `OUTDIR` is set to a properly
194configured GN build directory (e.g. `out/Debug`).
195
196To build and install the Monochrome bundle to your connected device, run:
197
198```shell
199$ autoninja -C $OUTDIR monochrome_public_bundle
200$ $OUTDIR/bin/monochrome_public_bundle install -m base -m foo
201```
202
203This will install Foo alongside the rest of Chrome. The rest of Chrome is called
204_base_ module in the bundle world. The Base module will always be put on the
205device when initially installing Chrome.
206
207*** note
208**Note:** You have to specify `-m base` here to make it explicit which modules
209will be installed. If you only specify `-m foo` the command will fail. It is
210also possible to specify no modules. In that case, the script will install the
211set of modules that the Play Store would install when first installing Chrome.
212That may be different than just specifying `-m base` if we have non-on-demand
213modules.
214***
215
216You can then check that the install worked with:
217
218```shell
219$ adb shell dumpsys package org.chromium.chrome | grep splits
220> splits=[base, config.en, foo]
221```
222
223Then try installing the Monochrome bundle without your module and print the
224installed modules:
225
226```shell
227$ $OUTDIR/bin/monochrome_public_bundle install -m base
228$ adb shell dumpsys package org.chromium.chrome | grep splits
229> splits=[base, config.en]
230```
231
232
233### Adding java code
234
235To make Foo useful, let's add some Java code to it. This section will walk you
236through the required steps.
237
Tibor Goldschwendt573cf3022019-05-10 17:23:30238First, define a module interface for Foo. This is accomplished by adding the
239`@ModuleInterface` annotation to the Foo interface. This annotation
240automatically creates a `FooModule` class that can be used later to install and
241access the module. To do this, add the following in the new file
Tibor Goldschwendt19364ba2019-04-10 15:59:55242`//chrome/android/features/foo/public/java/src/org/chromium/chrome/features/foo/Foo.java`:
243
244```java
245package org.chromium.chrome.features.foo;
246
Tibor Goldschwendt573cf3022019-05-10 17:23:30247import org.chromium.components.module_installer.ModuleInterface;
248
Tibor Goldschwendt19364ba2019-04-10 15:59:55249/** Interface to call into Foo feature. */
Tibor Goldschwendt573cf3022019-05-10 17:23:30250@ModuleInterface(module = "foo", impl = "org.chromium.chrome.features.FooImpl")
Tibor Goldschwendt19364ba2019-04-10 15:59:55251public interface Foo {
252 /** Magical function. */
253 void bar();
254}
255```
256
257*** note
258**Note:** To reflect the separation from "Chrome browser" code, features should
259be defined in their own package name, distinct from the chrome package - i.e.
260`org.chromium.chrome.features.<feature-name>`.
261***
262
263Next, define an implementation that goes into the module in the new file
264`//chrome/android/features/foo/java/src/org/chromium/chrome/features/foo/FooImpl.java`:
265
266```java
267package org.chromium.chrome.features.foo;
268
269import org.chromium.base.Log;
Tibor Goldschwendt573cf3022019-05-10 17:23:30270import org.chromium.base.annotations.UsedByReflection;
Tibor Goldschwendt19364ba2019-04-10 15:59:55271
Tibor Goldschwendt573cf3022019-05-10 17:23:30272@UsedByReflection("FooModule")
Tibor Goldschwendt19364ba2019-04-10 15:59:55273public class FooImpl implements Foo {
274 @Override
275 public void bar() {
276 Log.i("FOO", "bar in module");
277 }
278}
279```
280
Tibor Goldschwendt19364ba2019-04-10 15:59:55281You can then use this provider to access the module if it is installed. To test
282that, instantiate Foo and call `bar()` somewhere in Chrome:
283
284```java
Tibor Goldschwendt573cf3022019-05-10 17:23:30285if (FooModule.isInstalled()) {
286 FooModule.getImpl().bar();
Tibor Goldschwendt19364ba2019-04-10 15:59:55287} else {
288 Log.i("FOO", "module not installed");
289}
290```
291
Tibor Goldschwendt573cf3022019-05-10 17:23:30292The interface has to be available regardless of whether the Foo DFM is present.
293Therefore, put those classes into the base module. For this create a list of
294those Java files in
Tibor Goldschwendt19364ba2019-04-10 15:59:55295`//chrome/android/features/foo/public/foo_public_java_sources.gni`:
296
297```gn
298foo_public_java_sources = [
299 "//chrome/android/features/foo/public/java/src/org/chromium/chrome/features/foo/Foo.java",
Tibor Goldschwendt19364ba2019-04-10 15:59:55300]
301```
302
303Then add this list to `chrome_java in //chrome/android/BUILD.gn`:
304
305```gn
306...
307import("modules/foo/public/foo_public_java_sources.gni")
308...
309android_library("chrome_java") {
310 ...
311 java_files += foo_public_java_sources
312}
313...
314```
315
316The actual implementation, however, should go into the Foo DFM. For this
317purpose, create a new file `//chrome/android/features/foo/BUILD.gn` and make a
318library with the module Java code in it:
319
320```gn
321import("//build/config/android/rules.gni")
322
323android_library("java") {
324 # Define like ordinary Java Android library.
325 java_files = [
326 "java/src/org/chromium/chrome/features/foo/FooImpl.java",
327 # Add other Java classes that should go into the Foo DFM here.
328 ]
329 # Put other Chrome libs into the classpath so that you can call into the rest
330 # of Chrome from the Foo DFM.
Fred Mellob32b3022019-06-21 18:10:11331 deps = [
Tibor Goldschwendt19364ba2019-04-10 15:59:55332 "//base:base_java",
333 "//chrome/android:chrome_java",
334 # etc.
335 # Also, you'll need to depend on any //third_party or //components code you
336 # are using in the module code.
337 ]
338}
339```
340
341Then, add this new library as a dependency of the Foo module target in
342`//chrome/android/features/foo/foo_module_tmpl.gni`:
343
344```gn
345android_app_bundle_module(target_name) {
346 ...
347 deps = [
348 "//chrome/android/module/foo:java",
349 ]
350}
351```
352
353Finally, tell Android that your module is now containing code. Do that by
Samuel Huang39c7db632019-05-15 14:57:18354removing the `android:hasCode="false"` attribute from the `<application>` tag in
Tibor Goldschwendt19364ba2019-04-10 15:59:55355`//chrome/android/features/foo/java/AndroidManifest.xml`. You should be left
356with an empty tag like so:
357
358```xml
359...
360 <application />
361...
362```
363
364Rebuild and install `monochrome_public_bundle`. Start Chrome and run through a
365flow that tries to executes `bar()`. Depending on whether you installed your
366module (`-m foo`) "`bar in module`" or "`module not installed`" is printed to
367logcat. Yay!
368
369
370### Adding native code
371
372Coming soon (
373[crbug/874564](https://bugs.chromium.org/p/chromium/issues/detail?id=874564)).
374
375You can already add third party native code or native Chrome code that has no
376dependency on other Chrome code. To add such code add it as a loadable module to
377the bundle module target in `//chrome/android/features/foo/foo_module_tmpl.gni`:
378
379```gn
380...
381template("foo_module_tmpl") {
382 ...
383 android_app_bundle_module(target_name) {
384 ...
385 loadable_modules = [ "//path/to/lib.so" ]
386 }
387}
388```
389
390
391### Adding android resources
392
393In this section we will add the required build targets to add Android resources
394to the Foo DFM.
395
396First, add a resources target to `//chrome/android/features/foo/BUILD.gn` and
397add it as a dependency on Foo's `java` target in the same file:
398
399```gn
400...
401android_resources("java_resources") {
402 # Define like ordinary Android resources target.
403 ...
404 custom_package = "org.chromium.chrome.features.foo"
405}
406...
407android_library("java") {
408 ...
409 deps = [
410 ":java_resources",
411 ]
412}
413```
414
415To add strings follow steps
416[here](http://dev.chromium.org/developers/design-documents/ui-localization) to
417add new Java GRD file. Then create
418`//chrome/android/features/foo/java/strings/android_foo_strings.grd` as follows:
419
420```xml
421<?xml version="1.0" encoding="UTF-8"?>
422<grit
423 current_release="1"
424 latest_public_release="0"
425 output_all_resource_defines="false">
426 <outputs>
427 <output
428 filename="values-am/android_foo_strings.xml"
429 lang="am"
430 type="android" />
431 <!-- List output file for all other supported languages. See
432 //chrome/android/java/strings/android_chrome_strings.grd for the full
433 list. -->
434 ...
435 </outputs>
436 <translations>
437 <file lang="am" path="vr_translations/android_foo_strings_am.xtb" />
438 <!-- Here, too, list XTB files for all other supported languages. -->
439 ...
440 </translations>
441 <release allow_pseudo="false" seq="1">
442 <messages fallback_to_english="true">
443 <message name="IDS_BAR_IMPL_TEXT" desc="Magical string.">
444 impl
445 </message>
446 </messages>
447 </release>
448</grit>
449```
450
451Then, create a new GRD target and add it as a dependency on `java_resources` in
452`//chrome/android/features/foo/BUILD.gn`:
453
454```gn
455...
456java_strings_grd("java_strings_grd") {
457 defines = chrome_grit_defines
458 grd_file = "java/strings/android_foo_strings.grd"
459 outputs = [
460 "values-am/android_foo_strings.xml",
461 # Here, too, list output files for other supported languages.
462 ...
463 ]
464}
465...
466android_resources("java_resources") {
467 ...
468 deps = [":java_strings_grd"]
469 custom_package = "org.chromium.chrome.features.foo"
470}
471...
472```
473
474You can then access Foo's resources using the
475`org.chromium.chrome.features.foo.R` class. To do this change
476`//chrome/android/features/foo/java/src/org/chromium/chrome/features/foo/FooImpl.java`
477to:
478
479```java
480package org.chromium.chrome.features.foo;
481
482import org.chromium.base.ContextUtils;
483import org.chromium.base.Log;
Tibor Goldschwendt573cf3022019-05-10 17:23:30484import org.chromium.base.annotations.UsedByReflection;
Tibor Goldschwendt19364ba2019-04-10 15:59:55485import org.chromium.chrome.features.foo.R;
486
Tibor Goldschwendt573cf3022019-05-10 17:23:30487@UsedByReflection("FooModule")
Tibor Goldschwendt19364ba2019-04-10 15:59:55488public class FooImpl implements Foo {
489 @Override
490 public void bar() {
491 Log.i("FOO", ContextUtils.getApplicationContext().getString(
492 R.string.bar_impl_text));
493 }
494}
495```
496
497*** note
498**Warning:** While your module is emulated (see [below](#on-demand-install))
499your resources are only available through
500`ContextUtils.getApplicationContext()`. Not through activities, etc. We
501therefore recommend that you only access DFM resources this way. See
502[crbug/949729](https://bugs.chromium.org/p/chromium/issues/detail?id=949729)
503for progress on making this more robust.
504***
505
506
507### Module install
508
509So far, we have installed the Foo DFM as a true split (`-m foo` option on the
510install script). In production, however, we have to explicitly install the Foo
511DFM for users to get it. There are two install options: _on-demand_ and
512_deferred_.
513
514
515#### On-demand install
516
517On-demand requesting a module will try to download and install the
518module as soon as possible regardless of whether the user is on a metered
519connection or whether they have turned updates off in the Play Store app.
520
Tibor Goldschwendt573cf3022019-05-10 17:23:30521You can use the autogenerated module class to on-demand install the module like
522so:
Tibor Goldschwendt19364ba2019-04-10 15:59:55523
524```java
Tibor Goldschwendt573cf3022019-05-10 17:23:30525FooModule.install((success) -> {
526 if (success) {
527 FooModule.getImpl().bar();
528 }
Tibor Goldschwendt19364ba2019-04-10 15:59:55529});
530```
531
532**Optionally**, you can show UI telling the user about the install flow. For
Tibor Goldschwendt573cf3022019-05-10 17:23:30533this, add a function like the one below. Note, it is possible
Tibor Goldschwendt19364ba2019-04-10 15:59:55534to only show either one of the install, failure and success UI or any
535combination of the three.
536
537```java
538public static void installModuleWithUi(
539 Tab tab, OnModuleInstallFinishedListener onFinishedListener) {
540 ModuleInstallUi ui =
541 new ModuleInstallUi(
542 tab,
543 R.string.foo_module_title,
544 new ModuleInstallUi.FailureUiListener() {
545 @Override
546 public void onRetry() {
547 installModuleWithUi(tab, onFinishedListener);
548 }
549
550 @Override
551 public void onCancel() {
552 onFinishedListener.onFinished(false);
553 }
554 });
555 // At the time of writing, shows toast informing user about install start.
556 ui.showInstallStartUi();
Tibor Goldschwendt573cf3022019-05-10 17:23:30557 FooModule.install(
Tibor Goldschwendt19364ba2019-04-10 15:59:55558 (success) -> {
559 if (!success) {
560 // At the time of writing, shows infobar allowing user
561 // to retry install.
562 ui.showInstallFailureUi();
563 return;
564 }
565 // At the time of writing, shows toast informing user about
566 // install success.
567 ui.showInstallSuccessUi();
568 onFinishedListener.onFinished(true);
569 });
570}
571```
572
573To test on-demand install, "fake-install" the DFM. It's fake because
574the DFM is not installed as a true split. Instead it will be emulated by Chrome.
575Fake-install and launch Chrome with the following command:
576
577```shell
578$ $OUTDIR/bin/monochrome_public_bundle install -m base -f foo
Samuel Huang39c7db632019-05-15 14:57:18579$ $OUTDIR/bin/monochrome_public_bundle launch --args="--fake-feature-module-install"
Tibor Goldschwendt19364ba2019-04-10 15:59:55580```
581
582When running the install code, the Foo DFM module will be emulated.
583This will be the case in production right after installing the module. Emulation
584will last until Play Store has a chance to install your module as a true split.
585This usually takes about a day.
586
587*** note
588**Warning:** There are subtle differences between emulating a module and
589installing it as a true split. We therefore recommend that you always test both
590install methods.
591***
592
593
594#### Deferred install
595
596Deferred install means that the DFM is installed in the background when the
597device is on an unmetered connection and charging. The DFM will only be
598available after Chrome restarts. When deferred installing a module it will
599not be faked installed.
600
601To defer install Foo do the following:
602
603```java
Tibor Goldschwendt573cf3022019-05-10 17:23:30604FooModule.installDeferred();
Tibor Goldschwendt19364ba2019-04-10 15:59:55605```
606
607
608### Integration test APK and Android K support
609
610On Android K we still ship an APK. To make the Foo feature available on Android
611K add its code to the APK build. For this, add the `java` target to
612the `chrome_public_common_apk_or_module_tmpl` in
613`//chrome/android/chrome_public_apk_tmpl.gni` like so:
614
615```gn
616template("chrome_public_common_apk_or_module_tmpl") {
617 ...
618 target(_target_type, target_name) {
619 ...
620 if (_target_type != "android_app_bundle_module") {
621 deps += [
622 "//chrome/android/module/foo:java",
623 ]
624 }
625 }
626}
627```
628
629This will also add Foo's Java to the integration test APK. You may also have to
630add `java` as a dependency of `chrome_test_java` if you want to call into Foo
631from test code.