blob: edc18fe42f27e4fdee86931afb28515f3da72bc9 [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
Samuel Huang7f2b53752019-05-23 15:10:05195*** note
196**Note:** This is for module title only. Other strings specific to the module
197should go in the module, not here (in the base module).
198***
199
Tibor Goldschwendt19364ba2019-04-10 15:59:55200Congrats! You added the DFM Foo to Monochrome. That is a big step but not very
201useful so far. In the next sections you'll learn how to add code and resources
202to it.
203
204
205### Building and installing modules
206
207Before we are going to jump into adding content to Foo, let's take a look on how
208to build and deploy the Monochrome bundle with the Foo DFM. The remainder of
209this guide assumes the environment variable `OUTDIR` is set to a properly
210configured GN build directory (e.g. `out/Debug`).
211
212To build and install the Monochrome bundle to your connected device, run:
213
214```shell
215$ autoninja -C $OUTDIR monochrome_public_bundle
216$ $OUTDIR/bin/monochrome_public_bundle install -m base -m foo
217```
218
219This will install Foo alongside the rest of Chrome. The rest of Chrome is called
220_base_ module in the bundle world. The Base module will always be put on the
221device when initially installing Chrome.
222
223*** note
224**Note:** You have to specify `-m base` here to make it explicit which modules
225will be installed. If you only specify `-m foo` the command will fail. It is
226also possible to specify no modules. In that case, the script will install the
227set of modules that the Play Store would install when first installing Chrome.
228That may be different than just specifying `-m base` if we have non-on-demand
229modules.
230***
231
232You can then check that the install worked with:
233
234```shell
235$ adb shell dumpsys package org.chromium.chrome | grep splits
236> splits=[base, config.en, foo]
237```
238
239Then try installing the Monochrome bundle without your module and print the
240installed modules:
241
242```shell
243$ $OUTDIR/bin/monochrome_public_bundle install -m base
244$ adb shell dumpsys package org.chromium.chrome | grep splits
245> splits=[base, config.en]
246```
247
248
249### Adding java code
250
251To make Foo useful, let's add some Java code to it. This section will walk you
252through the required steps.
253
Tibor Goldschwendt573cf3022019-05-10 17:23:30254First, define a module interface for Foo. This is accomplished by adding the
255`@ModuleInterface` annotation to the Foo interface. This annotation
256automatically creates a `FooModule` class that can be used later to install and
257access the module. To do this, add the following in the new file
Tibor Goldschwendt19364ba2019-04-10 15:59:55258`//chrome/android/features/foo/public/java/src/org/chromium/chrome/features/foo/Foo.java`:
259
260```java
261package org.chromium.chrome.features.foo;
262
Tibor Goldschwendt573cf3022019-05-10 17:23:30263import org.chromium.components.module_installer.ModuleInterface;
264
Tibor Goldschwendt19364ba2019-04-10 15:59:55265/** Interface to call into Foo feature. */
Tibor Goldschwendt573cf3022019-05-10 17:23:30266@ModuleInterface(module = "foo", impl = "org.chromium.chrome.features.FooImpl")
Tibor Goldschwendt19364ba2019-04-10 15:59:55267public interface Foo {
268 /** Magical function. */
269 void bar();
270}
271```
272
273*** note
274**Note:** To reflect the separation from "Chrome browser" code, features should
275be defined in their own package name, distinct from the chrome package - i.e.
276`org.chromium.chrome.features.<feature-name>`.
277***
278
279Next, define an implementation that goes into the module in the new file
280`//chrome/android/features/foo/java/src/org/chromium/chrome/features/foo/FooImpl.java`:
281
282```java
283package org.chromium.chrome.features.foo;
284
285import org.chromium.base.Log;
Tibor Goldschwendt573cf3022019-05-10 17:23:30286import org.chromium.base.annotations.UsedByReflection;
Tibor Goldschwendt19364ba2019-04-10 15:59:55287
Tibor Goldschwendt573cf3022019-05-10 17:23:30288@UsedByReflection("FooModule")
Tibor Goldschwendt19364ba2019-04-10 15:59:55289public class FooImpl implements Foo {
290 @Override
291 public void bar() {
292 Log.i("FOO", "bar in module");
293 }
294}
295```
296
Tibor Goldschwendt19364ba2019-04-10 15:59:55297You can then use this provider to access the module if it is installed. To test
298that, instantiate Foo and call `bar()` somewhere in Chrome:
299
300```java
Tibor Goldschwendt573cf3022019-05-10 17:23:30301if (FooModule.isInstalled()) {
302 FooModule.getImpl().bar();
Tibor Goldschwendt19364ba2019-04-10 15:59:55303} else {
304 Log.i("FOO", "module not installed");
305}
306```
307
Tibor Goldschwendt573cf3022019-05-10 17:23:30308The interface has to be available regardless of whether the Foo DFM is present.
309Therefore, put those classes into the base module. For this create a list of
310those Java files in
Tibor Goldschwendt19364ba2019-04-10 15:59:55311`//chrome/android/features/foo/public/foo_public_java_sources.gni`:
312
313```gn
314foo_public_java_sources = [
315 "//chrome/android/features/foo/public/java/src/org/chromium/chrome/features/foo/Foo.java",
Tibor Goldschwendt19364ba2019-04-10 15:59:55316]
317```
318
319Then add this list to `chrome_java in //chrome/android/BUILD.gn`:
320
321```gn
322...
323import("modules/foo/public/foo_public_java_sources.gni")
324...
325android_library("chrome_java") {
326 ...
327 java_files += foo_public_java_sources
328}
329...
330```
331
332The actual implementation, however, should go into the Foo DFM. For this
333purpose, create a new file `//chrome/android/features/foo/BUILD.gn` and make a
334library with the module Java code in it:
335
336```gn
337import("//build/config/android/rules.gni")
338
339android_library("java") {
340 # Define like ordinary Java Android library.
341 java_files = [
342 "java/src/org/chromium/chrome/features/foo/FooImpl.java",
343 # Add other Java classes that should go into the Foo DFM here.
344 ]
345 # Put other Chrome libs into the classpath so that you can call into the rest
346 # of Chrome from the Foo DFM.
347 classpath_deps = [
348 "//base:base_java",
349 "//chrome/android:chrome_java",
350 # etc.
351 # Also, you'll need to depend on any //third_party or //components code you
352 # are using in the module code.
353 ]
354}
355```
356
357Then, add this new library as a dependency of the Foo module target in
358`//chrome/android/features/foo/foo_module_tmpl.gni`:
359
360```gn
361android_app_bundle_module(target_name) {
362 ...
363 deps = [
364 "//chrome/android/module/foo:java",
365 ]
366}
367```
368
369Finally, tell Android that your module is now containing code. Do that by
Samuel Huang39c7db632019-05-15 14:57:18370removing the `android:hasCode="false"` attribute from the `<application>` tag in
Tibor Goldschwendt19364ba2019-04-10 15:59:55371`//chrome/android/features/foo/java/AndroidManifest.xml`. You should be left
372with an empty tag like so:
373
374```xml
375...
376 <application />
377...
378```
379
380Rebuild and install `monochrome_public_bundle`. Start Chrome and run through a
381flow that tries to executes `bar()`. Depending on whether you installed your
382module (`-m foo`) "`bar in module`" or "`module not installed`" is printed to
383logcat. Yay!
384
385
386### Adding native code
387
388Coming soon (
389[crbug/874564](https://bugs.chromium.org/p/chromium/issues/detail?id=874564)).
390
391You can already add third party native code or native Chrome code that has no
392dependency on other Chrome code. To add such code add it as a loadable module to
393the bundle module target in `//chrome/android/features/foo/foo_module_tmpl.gni`:
394
395```gn
396...
397template("foo_module_tmpl") {
398 ...
399 android_app_bundle_module(target_name) {
400 ...
401 loadable_modules = [ "//path/to/lib.so" ]
402 }
403}
404```
405
406
407### Adding android resources
408
409In this section we will add the required build targets to add Android resources
410to the Foo DFM.
411
412First, add a resources target to `//chrome/android/features/foo/BUILD.gn` and
413add it as a dependency on Foo's `java` target in the same file:
414
415```gn
416...
417android_resources("java_resources") {
418 # Define like ordinary Android resources target.
419 ...
420 custom_package = "org.chromium.chrome.features.foo"
421}
422...
423android_library("java") {
424 ...
425 deps = [
426 ":java_resources",
427 ]
428}
429```
430
431To add strings follow steps
432[here](http://dev.chromium.org/developers/design-documents/ui-localization) to
433add new Java GRD file. Then create
434`//chrome/android/features/foo/java/strings/android_foo_strings.grd` as follows:
435
436```xml
437<?xml version="1.0" encoding="UTF-8"?>
438<grit
439 current_release="1"
440 latest_public_release="0"
441 output_all_resource_defines="false">
442 <outputs>
443 <output
444 filename="values-am/android_foo_strings.xml"
445 lang="am"
446 type="android" />
447 <!-- List output file for all other supported languages. See
448 //chrome/android/java/strings/android_chrome_strings.grd for the full
449 list. -->
450 ...
451 </outputs>
452 <translations>
453 <file lang="am" path="vr_translations/android_foo_strings_am.xtb" />
454 <!-- Here, too, list XTB files for all other supported languages. -->
455 ...
456 </translations>
457 <release allow_pseudo="false" seq="1">
458 <messages fallback_to_english="true">
459 <message name="IDS_BAR_IMPL_TEXT" desc="Magical string.">
460 impl
461 </message>
462 </messages>
463 </release>
464</grit>
465```
466
467Then, create a new GRD target and add it as a dependency on `java_resources` in
468`//chrome/android/features/foo/BUILD.gn`:
469
470```gn
471...
472java_strings_grd("java_strings_grd") {
473 defines = chrome_grit_defines
474 grd_file = "java/strings/android_foo_strings.grd"
475 outputs = [
476 "values-am/android_foo_strings.xml",
477 # Here, too, list output files for other supported languages.
478 ...
479 ]
480}
481...
482android_resources("java_resources") {
483 ...
484 deps = [":java_strings_grd"]
485 custom_package = "org.chromium.chrome.features.foo"
486}
487...
488```
489
490You can then access Foo's resources using the
491`org.chromium.chrome.features.foo.R` class. To do this change
492`//chrome/android/features/foo/java/src/org/chromium/chrome/features/foo/FooImpl.java`
493to:
494
495```java
496package org.chromium.chrome.features.foo;
497
498import org.chromium.base.ContextUtils;
499import org.chromium.base.Log;
Tibor Goldschwendt573cf3022019-05-10 17:23:30500import org.chromium.base.annotations.UsedByReflection;
Tibor Goldschwendt19364ba2019-04-10 15:59:55501import org.chromium.chrome.features.foo.R;
502
Tibor Goldschwendt573cf3022019-05-10 17:23:30503@UsedByReflection("FooModule")
Tibor Goldschwendt19364ba2019-04-10 15:59:55504public class FooImpl implements Foo {
505 @Override
506 public void bar() {
507 Log.i("FOO", ContextUtils.getApplicationContext().getString(
508 R.string.bar_impl_text));
509 }
510}
511```
512
513*** note
514**Warning:** While your module is emulated (see [below](#on-demand-install))
515your resources are only available through
516`ContextUtils.getApplicationContext()`. Not through activities, etc. We
517therefore recommend that you only access DFM resources this way. See
518[crbug/949729](https://bugs.chromium.org/p/chromium/issues/detail?id=949729)
519for progress on making this more robust.
520***
521
522
523### Module install
524
525So far, we have installed the Foo DFM as a true split (`-m foo` option on the
526install script). In production, however, we have to explicitly install the Foo
527DFM for users to get it. There are two install options: _on-demand_ and
528_deferred_.
529
530
531#### On-demand install
532
533On-demand requesting a module will try to download and install the
534module as soon as possible regardless of whether the user is on a metered
535connection or whether they have turned updates off in the Play Store app.
536
Tibor Goldschwendt573cf3022019-05-10 17:23:30537You can use the autogenerated module class to on-demand install the module like
538so:
Tibor Goldschwendt19364ba2019-04-10 15:59:55539
540```java
Tibor Goldschwendt573cf3022019-05-10 17:23:30541FooModule.install((success) -> {
542 if (success) {
543 FooModule.getImpl().bar();
544 }
Tibor Goldschwendt19364ba2019-04-10 15:59:55545});
546```
547
548**Optionally**, you can show UI telling the user about the install flow. For
Tibor Goldschwendt573cf3022019-05-10 17:23:30549this, add a function like the one below. Note, it is possible
Tibor Goldschwendt19364ba2019-04-10 15:59:55550to only show either one of the install, failure and success UI or any
551combination of the three.
552
553```java
554public static void installModuleWithUi(
555 Tab tab, OnModuleInstallFinishedListener onFinishedListener) {
556 ModuleInstallUi ui =
557 new ModuleInstallUi(
558 tab,
559 R.string.foo_module_title,
560 new ModuleInstallUi.FailureUiListener() {
561 @Override
562 public void onRetry() {
563 installModuleWithUi(tab, onFinishedListener);
564 }
565
566 @Override
567 public void onCancel() {
568 onFinishedListener.onFinished(false);
569 }
570 });
571 // At the time of writing, shows toast informing user about install start.
572 ui.showInstallStartUi();
Tibor Goldschwendt573cf3022019-05-10 17:23:30573 FooModule.install(
Tibor Goldschwendt19364ba2019-04-10 15:59:55574 (success) -> {
575 if (!success) {
576 // At the time of writing, shows infobar allowing user
577 // to retry install.
578 ui.showInstallFailureUi();
579 return;
580 }
581 // At the time of writing, shows toast informing user about
582 // install success.
583 ui.showInstallSuccessUi();
584 onFinishedListener.onFinished(true);
585 });
586}
587```
588
589To test on-demand install, "fake-install" the DFM. It's fake because
590the DFM is not installed as a true split. Instead it will be emulated by Chrome.
591Fake-install and launch Chrome with the following command:
592
593```shell
594$ $OUTDIR/bin/monochrome_public_bundle install -m base -f foo
Samuel Huang39c7db632019-05-15 14:57:18595$ $OUTDIR/bin/monochrome_public_bundle launch --args="--fake-feature-module-install"
Tibor Goldschwendt19364ba2019-04-10 15:59:55596```
597
598When running the install code, the Foo DFM module will be emulated.
599This will be the case in production right after installing the module. Emulation
600will last until Play Store has a chance to install your module as a true split.
601This usually takes about a day.
602
603*** note
604**Warning:** There are subtle differences between emulating a module and
605installing it as a true split. We therefore recommend that you always test both
606install methods.
607***
608
609
610#### Deferred install
611
612Deferred install means that the DFM is installed in the background when the
613device is on an unmetered connection and charging. The DFM will only be
614available after Chrome restarts. When deferred installing a module it will
615not be faked installed.
616
617To defer install Foo do the following:
618
619```java
Tibor Goldschwendt573cf3022019-05-10 17:23:30620FooModule.installDeferred();
Tibor Goldschwendt19364ba2019-04-10 15:59:55621```
622
623
624### Integration test APK and Android K support
625
626On Android K we still ship an APK. To make the Foo feature available on Android
627K add its code to the APK build. For this, add the `java` target to
628the `chrome_public_common_apk_or_module_tmpl` in
629`//chrome/android/chrome_public_apk_tmpl.gni` like so:
630
631```gn
632template("chrome_public_common_apk_or_module_tmpl") {
633 ...
634 target(_target_type, target_name) {
635 ...
636 if (_target_type != "android_app_bundle_module") {
637 deps += [
638 "//chrome/android/module/foo:java",
639 ]
640 }
641 }
642}
643```
644
645This will also add Foo's Java to the integration test APK. You may also have to
646add `java` as a dependency of `chrome_test_java` if you want to call into Foo
647from test code.