blob: 242987f2c5b664824c8db0a5dfdb3c4cc75a71ca [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"
54 featureSplit="foo"
55 package="{{manifest_package}}">
56
57 <!-- For Chrome Modern use android:minSdkVersion="21". -->
58 <uses-sdk
59 android:minSdkVersion="24"
60 android:targetSdkVersion="{{target_sdk_version}}" />
61
62 <!-- dist:onDemand="true" makes this a separately installed module.
63 dist:onDemand="false" would always install the module alongside the
64 rest of Chrome. -->
65 <dist:module
66 dist:onDemand="true"
67 dist:title="@string/foo_module_title">
68 <!-- This will prevent the module to become part of the Android K
69 build in case we ever want to use bundles on Android K. -->
70 <dist:fusing dist:include="false" />
71 </dist:module>
72
Samuel Huang39c7db632019-05-15 14:57:1873 <!-- Remove android:hasCode="false" when adding Java code. -->
74 <application android:hasCode="false" />
Tibor Goldschwendt19364ba2019-04-10 15:59:5575</manifest>
76```
77
78Then, add a package ID for Foo so that Foo's resources have unique identifiers.
79For this, add a new ID to
Samuel Huang39c7db632019-05-15 14:57:1880`//chrome/android/features/dynamic_feature_modules.gni`:
Tibor Goldschwendt19364ba2019-04-10 15:59:5581
82```gn
83resource_packages_id_mapping = [
84 ...,
85 "foo=0x{XX}", # Set {XX} to next lower hex number.
86]
87```
88
89Next, create a template that contains the Foo module target.
90
91*** note
92**Note:** We put the module target into a template because we have to
93instantiate it for each Chrome bundle (Chrome Modern, Monochrome and Trichrome
94for both upstream and downstream) you want to ship your module in.
95***
96
97To do this, create `//chrome/android/features/foo/foo_module_tmpl.gni` and add
98the following:
99
100```gn
101import("//build/config/android/rules.gni")
102import("//build/config/locales.gni")
Samuel Huang39c7db632019-05-15 14:57:18103import("//chrome/android/features/dynamic_feature_modules.gni")
Tibor Goldschwendt19364ba2019-04-10 15:59:55104
105template("foo_module_tmpl") {
106 _manifest = "$target_gen_dir/$target_name/AndroidManifest.xml"
107 _manifest_target = "${target_name}__manifest"
108 jinja_template(_manifest_target) {
109 input = "//chrome/android/features/foo/java/AndroidManifest.xml"
110 output = _manifest
111 variables = [
112 "target_sdk_version=$android_sdk_version",
113 "manifest_package=${invoker.manifest_package}",
114 ]
115 }
116
117 android_app_bundle_module(target_name) {
118 forward_variables_from(invoker,
119 [
120 "base_module_target",
121 "module_name",
122 "uncompress_shared_libraries",
123 "version_code",
124 "version_name",
125 ])
126 android_manifest = _manifest
127 android_manifest_dep = ":${_manifest_target}"
128 proguard_enabled = !is_java_debug
129 aapt_locale_whitelist = locales
130 package_name = "foo"
131 package_name_to_id_mapping = resource_packages_id_mapping
132 }
133}
134```
135
136Then, instantiate the module template in `//chrome/android/BUILD.gn` inside the
Samuel Huang39c7db632019-05-15 14:57:18137`monochrome_or_trichrome_public_bundle_tmpl` template and add it to the bundle
138target:
Tibor Goldschwendt19364ba2019-04-10 15:59:55139
140```gn
141...
142import("modules/foo/foo_module_tmpl.gni")
143...
Samuel Huang39c7db632019-05-15 14:57:18144template("monochrome_or_trichrome_public_bundle_tmpl") {
Tibor Goldschwendt19364ba2019-04-10 15:59:55145 ...
146 foo_module_tmpl("${target_name}__foo_bundle_module") {
147 manifest_package = manifest_package
148 module_name = "Foo" + _bundle_name
149 base_module_target = ":$_base_module_target_name"
Tibor Goldschwendt19364ba2019-04-10 15:59:55150 uncompress_shared_libraries = true
Samuel Huang39c7db632019-05-15 14:57:18151 version_code = _version_code
152 version_name = _version_name
Tibor Goldschwendt19364ba2019-04-10 15:59:55153 }
154 ...
155 android_app_bundle(target_name) {
156 ...
157 extra_modules += [
158 {
159 name = "foo"
160 module_target = ":${target_name}__foo_bundle_module"
161 },
162 ]
163 }
164}
165```
166
167The next step is to add Foo to the list of feature modules for UMA recording.
168For this, add `foo` to the `AndroidFeatureModuleName` in
169`//tools/metrics/histograms/histograms.xml`:
170
171```xml
172<histogram_suffixes name="AndroidFeatureModuleName" ...>
173 ...
174 <suffix name="foo" label="Super Duper Foo Module" />
175 ...
176</histogram_suffixes>
177```
178
179<!--- TODO(tiborg): Add info about install UI. -->
180Lastly, give your module a title that Chrome and Play can use for the install
181UI. To do this, add a string to
182`//chrome/android/java/strings/android_chrome_strings.grd`:
183
184```xml
185...
186<message name="IDS_FOO_MODULE_TITLE"
187 desc="Text shown when the Foo module is referenced in install start, success,
188 failure UI (e.g. in IDS_MODULE_INSTALL_START_TEXT, which will expand to
189 'Installing Foo for Chrome…').">
190 Foo
191</message>
192...
193```
194
195Congrats! You added the DFM Foo to Monochrome. That is a big step but not very
196useful so far. In the next sections you'll learn how to add code and resources
197to it.
198
199
200### Building and installing modules
201
202Before we are going to jump into adding content to Foo, let's take a look on how
203to build and deploy the Monochrome bundle with the Foo DFM. The remainder of
204this guide assumes the environment variable `OUTDIR` is set to a properly
205configured GN build directory (e.g. `out/Debug`).
206
207To build and install the Monochrome bundle to your connected device, run:
208
209```shell
210$ autoninja -C $OUTDIR monochrome_public_bundle
211$ $OUTDIR/bin/monochrome_public_bundle install -m base -m foo
212```
213
214This will install Foo alongside the rest of Chrome. The rest of Chrome is called
215_base_ module in the bundle world. The Base module will always be put on the
216device when initially installing Chrome.
217
218*** note
219**Note:** You have to specify `-m base` here to make it explicit which modules
220will be installed. If you only specify `-m foo` the command will fail. It is
221also possible to specify no modules. In that case, the script will install the
222set of modules that the Play Store would install when first installing Chrome.
223That may be different than just specifying `-m base` if we have non-on-demand
224modules.
225***
226
227You can then check that the install worked with:
228
229```shell
230$ adb shell dumpsys package org.chromium.chrome | grep splits
231> splits=[base, config.en, foo]
232```
233
234Then try installing the Monochrome bundle without your module and print the
235installed modules:
236
237```shell
238$ $OUTDIR/bin/monochrome_public_bundle install -m base
239$ adb shell dumpsys package org.chromium.chrome | grep splits
240> splits=[base, config.en]
241```
242
243
244### Adding java code
245
246To make Foo useful, let's add some Java code to it. This section will walk you
247through the required steps.
248
Tibor Goldschwendt573cf3022019-05-10 17:23:30249First, define a module interface for Foo. This is accomplished by adding the
250`@ModuleInterface` annotation to the Foo interface. This annotation
251automatically creates a `FooModule` class that can be used later to install and
252access the module. To do this, add the following in the new file
Tibor Goldschwendt19364ba2019-04-10 15:59:55253`//chrome/android/features/foo/public/java/src/org/chromium/chrome/features/foo/Foo.java`:
254
255```java
256package org.chromium.chrome.features.foo;
257
Tibor Goldschwendt573cf3022019-05-10 17:23:30258import org.chromium.components.module_installer.ModuleInterface;
259
Tibor Goldschwendt19364ba2019-04-10 15:59:55260/** Interface to call into Foo feature. */
Tibor Goldschwendt573cf3022019-05-10 17:23:30261@ModuleInterface(module = "foo", impl = "org.chromium.chrome.features.FooImpl")
Tibor Goldschwendt19364ba2019-04-10 15:59:55262public interface Foo {
263 /** Magical function. */
264 void bar();
265}
266```
267
268*** note
269**Note:** To reflect the separation from "Chrome browser" code, features should
270be defined in their own package name, distinct from the chrome package - i.e.
271`org.chromium.chrome.features.<feature-name>`.
272***
273
274Next, define an implementation that goes into the module in the new file
275`//chrome/android/features/foo/java/src/org/chromium/chrome/features/foo/FooImpl.java`:
276
277```java
278package org.chromium.chrome.features.foo;
279
280import org.chromium.base.Log;
Tibor Goldschwendt573cf3022019-05-10 17:23:30281import org.chromium.base.annotations.UsedByReflection;
Tibor Goldschwendt19364ba2019-04-10 15:59:55282
Tibor Goldschwendt573cf3022019-05-10 17:23:30283@UsedByReflection("FooModule")
Tibor Goldschwendt19364ba2019-04-10 15:59:55284public class FooImpl implements Foo {
285 @Override
286 public void bar() {
287 Log.i("FOO", "bar in module");
288 }
289}
290```
291
Tibor Goldschwendt19364ba2019-04-10 15:59:55292You can then use this provider to access the module if it is installed. To test
293that, instantiate Foo and call `bar()` somewhere in Chrome:
294
295```java
Tibor Goldschwendt573cf3022019-05-10 17:23:30296if (FooModule.isInstalled()) {
297 FooModule.getImpl().bar();
Tibor Goldschwendt19364ba2019-04-10 15:59:55298} else {
299 Log.i("FOO", "module not installed");
300}
301```
302
Tibor Goldschwendt573cf3022019-05-10 17:23:30303The interface has to be available regardless of whether the Foo DFM is present.
304Therefore, put those classes into the base module. For this create a list of
305those Java files in
Tibor Goldschwendt19364ba2019-04-10 15:59:55306`//chrome/android/features/foo/public/foo_public_java_sources.gni`:
307
308```gn
309foo_public_java_sources = [
310 "//chrome/android/features/foo/public/java/src/org/chromium/chrome/features/foo/Foo.java",
Tibor Goldschwendt19364ba2019-04-10 15:59:55311]
312```
313
314Then add this list to `chrome_java in //chrome/android/BUILD.gn`:
315
316```gn
317...
318import("modules/foo/public/foo_public_java_sources.gni")
319...
320android_library("chrome_java") {
321 ...
322 java_files += foo_public_java_sources
323}
324...
325```
326
327The actual implementation, however, should go into the Foo DFM. For this
328purpose, create a new file `//chrome/android/features/foo/BUILD.gn` and make a
329library with the module Java code in it:
330
331```gn
332import("//build/config/android/rules.gni")
333
334android_library("java") {
335 # Define like ordinary Java Android library.
336 java_files = [
337 "java/src/org/chromium/chrome/features/foo/FooImpl.java",
338 # Add other Java classes that should go into the Foo DFM here.
339 ]
340 # Put other Chrome libs into the classpath so that you can call into the rest
341 # of Chrome from the Foo DFM.
342 classpath_deps = [
343 "//base:base_java",
344 "//chrome/android:chrome_java",
345 # etc.
346 # Also, you'll need to depend on any //third_party or //components code you
347 # are using in the module code.
348 ]
349}
350```
351
352Then, add this new library as a dependency of the Foo module target in
353`//chrome/android/features/foo/foo_module_tmpl.gni`:
354
355```gn
356android_app_bundle_module(target_name) {
357 ...
358 deps = [
359 "//chrome/android/module/foo:java",
360 ]
361}
362```
363
364Finally, tell Android that your module is now containing code. Do that by
Samuel Huang39c7db632019-05-15 14:57:18365removing the `android:hasCode="false"` attribute from the `<application>` tag in
Tibor Goldschwendt19364ba2019-04-10 15:59:55366`//chrome/android/features/foo/java/AndroidManifest.xml`. You should be left
367with an empty tag like so:
368
369```xml
370...
371 <application />
372...
373```
374
375Rebuild and install `monochrome_public_bundle`. Start Chrome and run through a
376flow that tries to executes `bar()`. Depending on whether you installed your
377module (`-m foo`) "`bar in module`" or "`module not installed`" is printed to
378logcat. Yay!
379
380
381### Adding native code
382
383Coming soon (
384[crbug/874564](https://bugs.chromium.org/p/chromium/issues/detail?id=874564)).
385
386You can already add third party native code or native Chrome code that has no
387dependency on other Chrome code. To add such code add it as a loadable module to
388the bundle module target in `//chrome/android/features/foo/foo_module_tmpl.gni`:
389
390```gn
391...
392template("foo_module_tmpl") {
393 ...
394 android_app_bundle_module(target_name) {
395 ...
396 loadable_modules = [ "//path/to/lib.so" ]
397 }
398}
399```
400
401
402### Adding android resources
403
404In this section we will add the required build targets to add Android resources
405to the Foo DFM.
406
407First, add a resources target to `//chrome/android/features/foo/BUILD.gn` and
408add it as a dependency on Foo's `java` target in the same file:
409
410```gn
411...
412android_resources("java_resources") {
413 # Define like ordinary Android resources target.
414 ...
415 custom_package = "org.chromium.chrome.features.foo"
416}
417...
418android_library("java") {
419 ...
420 deps = [
421 ":java_resources",
422 ]
423}
424```
425
426To add strings follow steps
427[here](http://dev.chromium.org/developers/design-documents/ui-localization) to
428add new Java GRD file. Then create
429`//chrome/android/features/foo/java/strings/android_foo_strings.grd` as follows:
430
431```xml
432<?xml version="1.0" encoding="UTF-8"?>
433<grit
434 current_release="1"
435 latest_public_release="0"
436 output_all_resource_defines="false">
437 <outputs>
438 <output
439 filename="values-am/android_foo_strings.xml"
440 lang="am"
441 type="android" />
442 <!-- List output file for all other supported languages. See
443 //chrome/android/java/strings/android_chrome_strings.grd for the full
444 list. -->
445 ...
446 </outputs>
447 <translations>
448 <file lang="am" path="vr_translations/android_foo_strings_am.xtb" />
449 <!-- Here, too, list XTB files for all other supported languages. -->
450 ...
451 </translations>
452 <release allow_pseudo="false" seq="1">
453 <messages fallback_to_english="true">
454 <message name="IDS_BAR_IMPL_TEXT" desc="Magical string.">
455 impl
456 </message>
457 </messages>
458 </release>
459</grit>
460```
461
462Then, create a new GRD target and add it as a dependency on `java_resources` in
463`//chrome/android/features/foo/BUILD.gn`:
464
465```gn
466...
467java_strings_grd("java_strings_grd") {
468 defines = chrome_grit_defines
469 grd_file = "java/strings/android_foo_strings.grd"
470 outputs = [
471 "values-am/android_foo_strings.xml",
472 # Here, too, list output files for other supported languages.
473 ...
474 ]
475}
476...
477android_resources("java_resources") {
478 ...
479 deps = [":java_strings_grd"]
480 custom_package = "org.chromium.chrome.features.foo"
481}
482...
483```
484
485You can then access Foo's resources using the
486`org.chromium.chrome.features.foo.R` class. To do this change
487`//chrome/android/features/foo/java/src/org/chromium/chrome/features/foo/FooImpl.java`
488to:
489
490```java
491package org.chromium.chrome.features.foo;
492
493import org.chromium.base.ContextUtils;
494import org.chromium.base.Log;
Tibor Goldschwendt573cf3022019-05-10 17:23:30495import org.chromium.base.annotations.UsedByReflection;
Tibor Goldschwendt19364ba2019-04-10 15:59:55496import org.chromium.chrome.features.foo.R;
497
Tibor Goldschwendt573cf3022019-05-10 17:23:30498@UsedByReflection("FooModule")
Tibor Goldschwendt19364ba2019-04-10 15:59:55499public class FooImpl implements Foo {
500 @Override
501 public void bar() {
502 Log.i("FOO", ContextUtils.getApplicationContext().getString(
503 R.string.bar_impl_text));
504 }
505}
506```
507
508*** note
509**Warning:** While your module is emulated (see [below](#on-demand-install))
510your resources are only available through
511`ContextUtils.getApplicationContext()`. Not through activities, etc. We
512therefore recommend that you only access DFM resources this way. See
513[crbug/949729](https://bugs.chromium.org/p/chromium/issues/detail?id=949729)
514for progress on making this more robust.
515***
516
517
518### Module install
519
520So far, we have installed the Foo DFM as a true split (`-m foo` option on the
521install script). In production, however, we have to explicitly install the Foo
522DFM for users to get it. There are two install options: _on-demand_ and
523_deferred_.
524
525
526#### On-demand install
527
528On-demand requesting a module will try to download and install the
529module as soon as possible regardless of whether the user is on a metered
530connection or whether they have turned updates off in the Play Store app.
531
Tibor Goldschwendt573cf3022019-05-10 17:23:30532You can use the autogenerated module class to on-demand install the module like
533so:
Tibor Goldschwendt19364ba2019-04-10 15:59:55534
535```java
Tibor Goldschwendt573cf3022019-05-10 17:23:30536FooModule.install((success) -> {
537 if (success) {
538 FooModule.getImpl().bar();
539 }
Tibor Goldschwendt19364ba2019-04-10 15:59:55540});
541```
542
543**Optionally**, you can show UI telling the user about the install flow. For
Tibor Goldschwendt573cf3022019-05-10 17:23:30544this, add a function like the one below. Note, it is possible
Tibor Goldschwendt19364ba2019-04-10 15:59:55545to only show either one of the install, failure and success UI or any
546combination of the three.
547
548```java
549public static void installModuleWithUi(
550 Tab tab, OnModuleInstallFinishedListener onFinishedListener) {
551 ModuleInstallUi ui =
552 new ModuleInstallUi(
553 tab,
554 R.string.foo_module_title,
555 new ModuleInstallUi.FailureUiListener() {
556 @Override
557 public void onRetry() {
558 installModuleWithUi(tab, onFinishedListener);
559 }
560
561 @Override
562 public void onCancel() {
563 onFinishedListener.onFinished(false);
564 }
565 });
566 // At the time of writing, shows toast informing user about install start.
567 ui.showInstallStartUi();
Tibor Goldschwendt573cf3022019-05-10 17:23:30568 FooModule.install(
Tibor Goldschwendt19364ba2019-04-10 15:59:55569 (success) -> {
570 if (!success) {
571 // At the time of writing, shows infobar allowing user
572 // to retry install.
573 ui.showInstallFailureUi();
574 return;
575 }
576 // At the time of writing, shows toast informing user about
577 // install success.
578 ui.showInstallSuccessUi();
579 onFinishedListener.onFinished(true);
580 });
581}
582```
583
584To test on-demand install, "fake-install" the DFM. It's fake because
585the DFM is not installed as a true split. Instead it will be emulated by Chrome.
586Fake-install and launch Chrome with the following command:
587
588```shell
589$ $OUTDIR/bin/monochrome_public_bundle install -m base -f foo
Samuel Huang39c7db632019-05-15 14:57:18590$ $OUTDIR/bin/monochrome_public_bundle launch --args="--fake-feature-module-install"
Tibor Goldschwendt19364ba2019-04-10 15:59:55591```
592
593When running the install code, the Foo DFM module will be emulated.
594This will be the case in production right after installing the module. Emulation
595will last until Play Store has a chance to install your module as a true split.
596This usually takes about a day.
597
598*** note
599**Warning:** There are subtle differences between emulating a module and
600installing it as a true split. We therefore recommend that you always test both
601install methods.
602***
603
604
605#### Deferred install
606
607Deferred install means that the DFM is installed in the background when the
608device is on an unmetered connection and charging. The DFM will only be
609available after Chrome restarts. When deferred installing a module it will
610not be faked installed.
611
612To defer install Foo do the following:
613
614```java
Tibor Goldschwendt573cf3022019-05-10 17:23:30615FooModule.installDeferred();
Tibor Goldschwendt19364ba2019-04-10 15:59:55616```
617
618
619### Integration test APK and Android K support
620
621On Android K we still ship an APK. To make the Foo feature available on Android
622K add its code to the APK build. For this, add the `java` target to
623the `chrome_public_common_apk_or_module_tmpl` in
624`//chrome/android/chrome_public_apk_tmpl.gni` like so:
625
626```gn
627template("chrome_public_common_apk_or_module_tmpl") {
628 ...
629 target(_target_type, target_name) {
630 ...
631 if (_target_type != "android_app_bundle_module") {
632 deps += [
633 "//chrome/android/module/foo:java",
634 ]
635 }
636 }
637}
638```
639
640This will also add Foo's Java to the integration test APK. You may also have to
641add `java` as a dependency of `chrome_test_java` if you want to call into Foo
642from test code.