blob: 70c8401f843947c32c98cff029776a126f0b52ad [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
73 <!-- Remove hasCode="false" when adding Java code. -->
74 <application hasCode="false" />
75</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
80`//chrome/android/features/module_names_to_package_ids.gni`:
81
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")
103import("//chrome/android/features/module_names_to_package_ids.gni")
104
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
137`monochrome_public_bundle_tmpl` template and add it to the bundle target:
138
139```gn
140...
141import("modules/foo/foo_module_tmpl.gni")
142...
143template("monochrome_public_bundle_tmpl") {
144 ...
145 foo_module_tmpl("${target_name}__foo_bundle_module") {
146 manifest_package = manifest_package
147 module_name = "Foo" + _bundle_name
148 base_module_target = ":$_base_module_target_name"
149 version_code = monochrome_version_code
150 version_name = chrome_version_name
151 uncompress_shared_libraries = true
152 }
153 ...
154 android_app_bundle(target_name) {
155 ...
156 extra_modules += [
157 {
158 name = "foo"
159 module_target = ":${target_name}__foo_bundle_module"
160 },
161 ]
162 }
163}
164```
165
166The next step is to add Foo to the list of feature modules for UMA recording.
167For this, add `foo` to the `AndroidFeatureModuleName` in
168`//tools/metrics/histograms/histograms.xml`:
169
170```xml
171<histogram_suffixes name="AndroidFeatureModuleName" ...>
172 ...
173 <suffix name="foo" label="Super Duper Foo Module" />
174 ...
175</histogram_suffixes>
176```
177
178<!--- TODO(tiborg): Add info about install UI. -->
179Lastly, give your module a title that Chrome and Play can use for the install
180UI. To do this, add a string to
181`//chrome/android/java/strings/android_chrome_strings.grd`:
182
183```xml
184...
185<message name="IDS_FOO_MODULE_TITLE"
186 desc="Text shown when the Foo module is referenced in install start, success,
187 failure UI (e.g. in IDS_MODULE_INSTALL_START_TEXT, which will expand to
188 'Installing Foo for Chrome…').">
189 Foo
190</message>
191...
192```
193
194Congrats! You added the DFM Foo to Monochrome. That is a big step but not very
195useful so far. In the next sections you'll learn how to add code and resources
196to it.
197
198
199### Building and installing modules
200
201Before we are going to jump into adding content to Foo, let's take a look on how
202to build and deploy the Monochrome bundle with the Foo DFM. The remainder of
203this guide assumes the environment variable `OUTDIR` is set to a properly
204configured GN build directory (e.g. `out/Debug`).
205
206To build and install the Monochrome bundle to your connected device, run:
207
208```shell
209$ autoninja -C $OUTDIR monochrome_public_bundle
210$ $OUTDIR/bin/monochrome_public_bundle install -m base -m foo
211```
212
213This will install Foo alongside the rest of Chrome. The rest of Chrome is called
214_base_ module in the bundle world. The Base module will always be put on the
215device when initially installing Chrome.
216
217*** note
218**Note:** You have to specify `-m base` here to make it explicit which modules
219will be installed. If you only specify `-m foo` the command will fail. It is
220also possible to specify no modules. In that case, the script will install the
221set of modules that the Play Store would install when first installing Chrome.
222That may be different than just specifying `-m base` if we have non-on-demand
223modules.
224***
225
226You can then check that the install worked with:
227
228```shell
229$ adb shell dumpsys package org.chromium.chrome | grep splits
230> splits=[base, config.en, foo]
231```
232
233Then try installing the Monochrome bundle without your module and print the
234installed modules:
235
236```shell
237$ $OUTDIR/bin/monochrome_public_bundle install -m base
238$ adb shell dumpsys package org.chromium.chrome | grep splits
239> splits=[base, config.en]
240```
241
242
243### Adding java code
244
245To make Foo useful, let's add some Java code to it. This section will walk you
246through the required steps.
247
Tibor Goldschwendt573cf3022019-05-10 17:23:30248First, define a module interface for Foo. This is accomplished by adding the
249`@ModuleInterface` annotation to the Foo interface. This annotation
250automatically creates a `FooModule` class that can be used later to install and
251access the module. To do this, add the following in the new file
Tibor Goldschwendt19364ba2019-04-10 15:59:55252`//chrome/android/features/foo/public/java/src/org/chromium/chrome/features/foo/Foo.java`:
253
254```java
255package org.chromium.chrome.features.foo;
256
Tibor Goldschwendt573cf3022019-05-10 17:23:30257import org.chromium.components.module_installer.ModuleInterface;
258
Tibor Goldschwendt19364ba2019-04-10 15:59:55259/** Interface to call into Foo feature. */
Tibor Goldschwendt573cf3022019-05-10 17:23:30260@ModuleInterface(module = "foo", impl = "org.chromium.chrome.features.FooImpl")
Tibor Goldschwendt19364ba2019-04-10 15:59:55261public interface Foo {
262 /** Magical function. */
263 void bar();
264}
265```
266
267*** note
268**Note:** To reflect the separation from "Chrome browser" code, features should
269be defined in their own package name, distinct from the chrome package - i.e.
270`org.chromium.chrome.features.<feature-name>`.
271***
272
273Next, define an implementation that goes into the module in the new file
274`//chrome/android/features/foo/java/src/org/chromium/chrome/features/foo/FooImpl.java`:
275
276```java
277package org.chromium.chrome.features.foo;
278
279import org.chromium.base.Log;
Tibor Goldschwendt573cf3022019-05-10 17:23:30280import org.chromium.base.annotations.UsedByReflection;
Tibor Goldschwendt19364ba2019-04-10 15:59:55281
Tibor Goldschwendt573cf3022019-05-10 17:23:30282@UsedByReflection("FooModule")
Tibor Goldschwendt19364ba2019-04-10 15:59:55283public class FooImpl implements Foo {
284 @Override
285 public void bar() {
286 Log.i("FOO", "bar in module");
287 }
288}
289```
290
Tibor Goldschwendt19364ba2019-04-10 15:59:55291You can then use this provider to access the module if it is installed. To test
292that, instantiate Foo and call `bar()` somewhere in Chrome:
293
294```java
Tibor Goldschwendt573cf3022019-05-10 17:23:30295if (FooModule.isInstalled()) {
296 FooModule.getImpl().bar();
Tibor Goldschwendt19364ba2019-04-10 15:59:55297} else {
298 Log.i("FOO", "module not installed");
299}
300```
301
Tibor Goldschwendt573cf3022019-05-10 17:23:30302The interface has to be available regardless of whether the Foo DFM is present.
303Therefore, put those classes into the base module. For this create a list of
304those Java files in
Tibor Goldschwendt19364ba2019-04-10 15:59:55305`//chrome/android/features/foo/public/foo_public_java_sources.gni`:
306
307```gn
308foo_public_java_sources = [
309 "//chrome/android/features/foo/public/java/src/org/chromium/chrome/features/foo/Foo.java",
Tibor Goldschwendt19364ba2019-04-10 15:59:55310]
311```
312
313Then add this list to `chrome_java in //chrome/android/BUILD.gn`:
314
315```gn
316...
317import("modules/foo/public/foo_public_java_sources.gni")
318...
319android_library("chrome_java") {
320 ...
321 java_files += foo_public_java_sources
322}
323...
324```
325
326The actual implementation, however, should go into the Foo DFM. For this
327purpose, create a new file `//chrome/android/features/foo/BUILD.gn` and make a
328library with the module Java code in it:
329
330```gn
331import("//build/config/android/rules.gni")
332
333android_library("java") {
334 # Define like ordinary Java Android library.
335 java_files = [
336 "java/src/org/chromium/chrome/features/foo/FooImpl.java",
337 # Add other Java classes that should go into the Foo DFM here.
338 ]
339 # Put other Chrome libs into the classpath so that you can call into the rest
340 # of Chrome from the Foo DFM.
341 classpath_deps = [
342 "//base:base_java",
343 "//chrome/android:chrome_java",
344 # etc.
345 # Also, you'll need to depend on any //third_party or //components code you
346 # are using in the module code.
347 ]
348}
349```
350
351Then, add this new library as a dependency of the Foo module target in
352`//chrome/android/features/foo/foo_module_tmpl.gni`:
353
354```gn
355android_app_bundle_module(target_name) {
356 ...
357 deps = [
358 "//chrome/android/module/foo:java",
359 ]
360}
361```
362
363Finally, tell Android that your module is now containing code. Do that by
364removing the `hasCode="false"` attribute from the `<application>` tag in
365`//chrome/android/features/foo/java/AndroidManifest.xml`. You should be left
366with an empty tag like so:
367
368```xml
369...
370 <application />
371...
372```
373
374Rebuild and install `monochrome_public_bundle`. Start Chrome and run through a
375flow that tries to executes `bar()`. Depending on whether you installed your
376module (`-m foo`) "`bar in module`" or "`module not installed`" is printed to
377logcat. Yay!
378
379
380### Adding native code
381
382Coming soon (
383[crbug/874564](https://bugs.chromium.org/p/chromium/issues/detail?id=874564)).
384
385You can already add third party native code or native Chrome code that has no
386dependency on other Chrome code. To add such code add it as a loadable module to
387the bundle module target in `//chrome/android/features/foo/foo_module_tmpl.gni`:
388
389```gn
390...
391template("foo_module_tmpl") {
392 ...
393 android_app_bundle_module(target_name) {
394 ...
395 loadable_modules = [ "//path/to/lib.so" ]
396 }
397}
398```
399
400
401### Adding android resources
402
403In this section we will add the required build targets to add Android resources
404to the Foo DFM.
405
406First, add a resources target to `//chrome/android/features/foo/BUILD.gn` and
407add it as a dependency on Foo's `java` target in the same file:
408
409```gn
410...
411android_resources("java_resources") {
412 # Define like ordinary Android resources target.
413 ...
414 custom_package = "org.chromium.chrome.features.foo"
415}
416...
417android_library("java") {
418 ...
419 deps = [
420 ":java_resources",
421 ]
422}
423```
424
425To add strings follow steps
426[here](http://dev.chromium.org/developers/design-documents/ui-localization) to
427add new Java GRD file. Then create
428`//chrome/android/features/foo/java/strings/android_foo_strings.grd` as follows:
429
430```xml
431<?xml version="1.0" encoding="UTF-8"?>
432<grit
433 current_release="1"
434 latest_public_release="0"
435 output_all_resource_defines="false">
436 <outputs>
437 <output
438 filename="values-am/android_foo_strings.xml"
439 lang="am"
440 type="android" />
441 <!-- List output file for all other supported languages. See
442 //chrome/android/java/strings/android_chrome_strings.grd for the full
443 list. -->
444 ...
445 </outputs>
446 <translations>
447 <file lang="am" path="vr_translations/android_foo_strings_am.xtb" />
448 <!-- Here, too, list XTB files for all other supported languages. -->
449 ...
450 </translations>
451 <release allow_pseudo="false" seq="1">
452 <messages fallback_to_english="true">
453 <message name="IDS_BAR_IMPL_TEXT" desc="Magical string.">
454 impl
455 </message>
456 </messages>
457 </release>
458</grit>
459```
460
461Then, create a new GRD target and add it as a dependency on `java_resources` in
462`//chrome/android/features/foo/BUILD.gn`:
463
464```gn
465...
466java_strings_grd("java_strings_grd") {
467 defines = chrome_grit_defines
468 grd_file = "java/strings/android_foo_strings.grd"
469 outputs = [
470 "values-am/android_foo_strings.xml",
471 # Here, too, list output files for other supported languages.
472 ...
473 ]
474}
475...
476android_resources("java_resources") {
477 ...
478 deps = [":java_strings_grd"]
479 custom_package = "org.chromium.chrome.features.foo"
480}
481...
482```
483
484You can then access Foo's resources using the
485`org.chromium.chrome.features.foo.R` class. To do this change
486`//chrome/android/features/foo/java/src/org/chromium/chrome/features/foo/FooImpl.java`
487to:
488
489```java
490package org.chromium.chrome.features.foo;
491
492import org.chromium.base.ContextUtils;
493import org.chromium.base.Log;
Tibor Goldschwendt573cf3022019-05-10 17:23:30494import org.chromium.base.annotations.UsedByReflection;
Tibor Goldschwendt19364ba2019-04-10 15:59:55495import org.chromium.chrome.features.foo.R;
496
Tibor Goldschwendt573cf3022019-05-10 17:23:30497@UsedByReflection("FooModule")
Tibor Goldschwendt19364ba2019-04-10 15:59:55498public class FooImpl implements Foo {
499 @Override
500 public void bar() {
501 Log.i("FOO", ContextUtils.getApplicationContext().getString(
502 R.string.bar_impl_text));
503 }
504}
505```
506
507*** note
508**Warning:** While your module is emulated (see [below](#on-demand-install))
509your resources are only available through
510`ContextUtils.getApplicationContext()`. Not through activities, etc. We
511therefore recommend that you only access DFM resources this way. See
512[crbug/949729](https://bugs.chromium.org/p/chromium/issues/detail?id=949729)
513for progress on making this more robust.
514***
515
516
517### Module install
518
519So far, we have installed the Foo DFM as a true split (`-m foo` option on the
520install script). In production, however, we have to explicitly install the Foo
521DFM for users to get it. There are two install options: _on-demand_ and
522_deferred_.
523
524
525#### On-demand install
526
527On-demand requesting a module will try to download and install the
528module as soon as possible regardless of whether the user is on a metered
529connection or whether they have turned updates off in the Play Store app.
530
Tibor Goldschwendt573cf3022019-05-10 17:23:30531You can use the autogenerated module class to on-demand install the module like
532so:
Tibor Goldschwendt19364ba2019-04-10 15:59:55533
534```java
Tibor Goldschwendt573cf3022019-05-10 17:23:30535FooModule.install((success) -> {
536 if (success) {
537 FooModule.getImpl().bar();
538 }
Tibor Goldschwendt19364ba2019-04-10 15:59:55539});
540```
541
542**Optionally**, you can show UI telling the user about the install flow. For
Tibor Goldschwendt573cf3022019-05-10 17:23:30543this, add a function like the one below. Note, it is possible
Tibor Goldschwendt19364ba2019-04-10 15:59:55544to only show either one of the install, failure and success UI or any
545combination of the three.
546
547```java
548public static void installModuleWithUi(
549 Tab tab, OnModuleInstallFinishedListener onFinishedListener) {
550 ModuleInstallUi ui =
551 new ModuleInstallUi(
552 tab,
553 R.string.foo_module_title,
554 new ModuleInstallUi.FailureUiListener() {
555 @Override
556 public void onRetry() {
557 installModuleWithUi(tab, onFinishedListener);
558 }
559
560 @Override
561 public void onCancel() {
562 onFinishedListener.onFinished(false);
563 }
564 });
565 // At the time of writing, shows toast informing user about install start.
566 ui.showInstallStartUi();
Tibor Goldschwendt573cf3022019-05-10 17:23:30567 FooModule.install(
Tibor Goldschwendt19364ba2019-04-10 15:59:55568 (success) -> {
569 if (!success) {
570 // At the time of writing, shows infobar allowing user
571 // to retry install.
572 ui.showInstallFailureUi();
573 return;
574 }
575 // At the time of writing, shows toast informing user about
576 // install success.
577 ui.showInstallSuccessUi();
578 onFinishedListener.onFinished(true);
579 });
580}
581```
582
583To test on-demand install, "fake-install" the DFM. It's fake because
584the DFM is not installed as a true split. Instead it will be emulated by Chrome.
585Fake-install and launch Chrome with the following command:
586
587```shell
588$ $OUTDIR/bin/monochrome_public_bundle install -m base -f foo
589$ $OUTDIR/bin/monochrome_public_bundle launch \
590 --args="--fake-feature-module-install"
591```
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.