From 6be8f494b88046dd004f085dfb594426bf7afba4 Mon Sep 17 00:00:00 2001 From: Olivier Notteghem Date: Thu, 31 Oct 2024 11:41:08 -0700 Subject: [PATCH] [Uber] Add full-stack Android App Bundle support for dynamic feature modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements end-to-end support for Android App Bundles (AAB) with dynamic feature modules in rules_android, covering all four artifact types: resources, bytecode/dex, native libraries, and assets. Resources Resource tables from all dynamic features are aggregated into the base app's AAPT2 resource compilation step so that the final bundle contains a single, unified R.txt. Each feature's proto-apk_ is built separately (via a dedicated android_feature_module_rule target) and merged into the base bundle at packaging time. Manifest entries from feature modules are filtered to prevent leaking into the base manifest, and manifest merging order is corrected to match Play's expectations. Bytecode / DEX Each feature module's deploy jar is passed through R8 alongside the base deploy jar. Before invocation, a filtering step (filter_feature_classes.sh) identifies all classes already present in the base dex output and strips them from each feature jar — preventing duplicate class definitions and keeping per-feature dex splits as small as possible. Proguard specs are collected transitively across all feature dependencies and supplied to R8 via --pg-conf, with --pg-compat enabled for compatibility. The resulting per-feature dex directories are zipped and embedded in the final AAB. Native libraries Native .so files are extracted from each feature's unsigned APK (lib/*), filtered to remove non-library entries, and merged into the feature's bundle zip alongside its resources and dex. Only the ABI slice matching the target device is delivered by Play at install time. Asset packs / is_asset_pack Feature modules declared as asset packs (is_asset_pack = True) are handled as a separate code path that skips dex compilation entirely and packages only the asset tree and manifest. Resource shrinking and obfuscation Two new flags on android_application — shrink_resources and obfuscate_resources — wire R8's resource shrinker and AAPT2's optimize step into the pipeline. On Uber Rider this reduces binary size by ~1.5 MB (unused resource pruning) and ~0.5 MB (path minification) respectively. Startup profile (PGO) An optional startup_profile argument threads a Baseline Profile through R8's --startup-profile flag for profile-guided optimization. This implementation has been rolled out to production across all Uber Android apps on the Play Store. Apps like Uber Rider ship with nearly a dozen dynamic feature modules — each carrying its own assets, resources, bytecode, and native libraries — and are built end-to-end using this pipeline today. Test Plan From examples/bundle/, run: # All four feature module types bazel build //features/assets:feature_module # asset-only feature, no dex bazel build //features/resources:feature_module # resource-only feature, no dex bazel build //features/dex:feature_module # feature with bytecode (guava dep) bazel build //features/native:feature_module # feature with native .so (ffmpeg-kit dep) # Full android_application bundles integrating feature modules bazel build //app:assets # base app + asset feature module bazel build //app:native # base app + native library feature module Each target exercises a different slice of the pipeline: - //features/assets — asset pack code path (is_asset_pack, no dex compilation) - //features/resources — resource-only feature, AAPT2 proto-apk_ generation - //features/dex — Java bytecode compilation + R8 filtering in feature mode - //features/native — native .so extraction and jni/ layout conversion in AAR - //app:assets — base app + feature manifest merging + bundle assembly - //app:native — end-to-end: base app + native libs + manifest merging + bundle Attributions Developed at Uber (Olivier Notteghem @oliviernotteghem ) in collaboration with Snap Inc (Mauricio Gonzalez - @mauriciogg ) from Snap's Android build infrastructure team. --- examples/bundle/.bazelrc | 14 + examples/bundle/.gitignore | 1 + examples/bundle/BUILD | 1 + examples/bundle/MODULE.bazel | 55 +++ examples/bundle/README.md | 12 + examples/bundle/WORKSPACE | 58 +++ examples/bundle/WORKSPACE.bzlmod | 1 + examples/bundle/app/AndroidManifest.xml | 22 + examples/bundle/app/BUILD | 31 ++ examples/bundle/app/BasicActivity.java | 59 +++ .../app/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 1678 bytes .../app/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 1283 bytes .../app/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 1817 bytes .../app/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 2137 bytes .../bundle/app/res/layout/basic_activity.xml | 23 + examples/bundle/app/res/menu/menu.xml | 8 + examples/bundle/app/res/values/dimens.xml | 5 + examples/bundle/app/res/values/strings.xml | 8 + examples/bundle/features/assets/BUILD | 18 + .../features/assets/src/AndroidManifest.xml | 13 + .../bundle/features/assets/src/assets.txt | 1 + examples/bundle/features/dex/BUILD | 22 + .../features/dex/src/AndroidManifest.xml | 22 + .../dex/src/main/JavaSampleActivity.java | 32 ++ .../bundle/features/dex/src/main/Unused.java | 27 ++ examples/bundle/features/native/BUILD | 21 + .../features/native/src/AndroidManifest.xml | 22 + examples/bundle/features/resources/BUILD | 19 + .../resources/src/AndroidManifest.xml | 13 + .../src/res/layout/activity_feature_java.xml | 31 ++ .../resources/src/res/values/strings.xml | 19 + providers/providers.bzl | 9 + rules/android_application/BUILD | 8 +- .../android_application_rule.bzl | 409 ++++++++++++++---- .../android_feature_module_rule.bzl | 113 ++++- rules/android_application/attrs.bzl | 59 ++- .../bundle_deploy.sh_template | 9 +- .../feature_module_validation.sh | 31 +- .../filter_feature_classes.sh | 43 ++ .../gen_android_feature_manifest.sh | 8 +- .../gen_priority_android_feature_manifest.sh | 20 +- rules/android_binary/impl.bzl | 19 +- rules/android_neverlink_aspect.bzl | 2 +- rules/busybox.bzl | 6 +- rules/r8.bzl | 193 +++++++++ rules/rules.bzl | 5 + toolchains/android/BUILD | 16 + toolchains/android/toolchain.bzl | 5 + 48 files changed, 1355 insertions(+), 158 deletions(-) create mode 100644 examples/bundle/.bazelrc create mode 100644 examples/bundle/.gitignore create mode 100644 examples/bundle/BUILD create mode 100644 examples/bundle/MODULE.bazel create mode 100644 examples/bundle/README.md create mode 100644 examples/bundle/WORKSPACE create mode 100644 examples/bundle/WORKSPACE.bzlmod create mode 100644 examples/bundle/app/AndroidManifest.xml create mode 100644 examples/bundle/app/BUILD create mode 100644 examples/bundle/app/BasicActivity.java create mode 100644 examples/bundle/app/res/drawable-hdpi/ic_launcher.png create mode 100644 examples/bundle/app/res/drawable-mdpi/ic_launcher.png create mode 100644 examples/bundle/app/res/drawable-xhdpi/ic_launcher.png create mode 100644 examples/bundle/app/res/drawable-xxhdpi/ic_launcher.png create mode 100644 examples/bundle/app/res/layout/basic_activity.xml create mode 100644 examples/bundle/app/res/menu/menu.xml create mode 100644 examples/bundle/app/res/values/dimens.xml create mode 100644 examples/bundle/app/res/values/strings.xml create mode 100644 examples/bundle/features/assets/BUILD create mode 100644 examples/bundle/features/assets/src/AndroidManifest.xml create mode 100644 examples/bundle/features/assets/src/assets.txt create mode 100644 examples/bundle/features/dex/BUILD create mode 100644 examples/bundle/features/dex/src/AndroidManifest.xml create mode 100644 examples/bundle/features/dex/src/main/JavaSampleActivity.java create mode 100644 examples/bundle/features/dex/src/main/Unused.java create mode 100644 examples/bundle/features/native/BUILD create mode 100644 examples/bundle/features/native/src/AndroidManifest.xml create mode 100644 examples/bundle/features/resources/BUILD create mode 100644 examples/bundle/features/resources/src/AndroidManifest.xml create mode 100644 examples/bundle/features/resources/src/res/layout/activity_feature_java.xml create mode 100644 examples/bundle/features/resources/src/res/values/strings.xml create mode 100755 rules/android_application/filter_feature_classes.sh create mode 100644 rules/r8.bzl diff --git a/examples/bundle/.bazelrc b/examples/bundle/.bazelrc new file mode 100644 index 000000000..254e2d178 --- /dev/null +++ b/examples/bundle/.bazelrc @@ -0,0 +1,14 @@ +# Flags needed while the Android rules are being migrated to Starlark. +common --experimental_google_legacy_api +common --experimental_enable_android_migration_apis +common --android_sdk=@androidsdk//:sdk +common:core_library_desugaring --desugar_java8_libs + +# Flags to enable mobile-install v3 +mobile-install --mode=skylark --mobile_install_aspect=@rules_android//mobile_install:mi.bzl --mobile_install_supported_rules=android_binary +# Required to invoke the Studio deployer jar +mobile-install --tool_java_runtime_version=17 +common --java_language_version=17 +common --java_runtime_version=17 +common --tool_java_language_version=17 +common --tool_java_runtime_version=17 diff --git a/examples/bundle/.gitignore b/examples/bundle/.gitignore new file mode 100644 index 000000000..63f1fef0e --- /dev/null +++ b/examples/bundle/.gitignore @@ -0,0 +1 @@ +*.lock diff --git a/examples/bundle/BUILD b/examples/bundle/BUILD new file mode 100644 index 000000000..a09fce916 --- /dev/null +++ b/examples/bundle/BUILD @@ -0,0 +1 @@ +# Empty build file to satisfy gazelle for rules_go. \ No newline at end of file diff --git a/examples/bundle/MODULE.bazel b/examples/bundle/MODULE.bazel new file mode 100644 index 000000000..988e813a3 --- /dev/null +++ b/examples/bundle/MODULE.bazel @@ -0,0 +1,55 @@ +module( + name = "bundle", +) + +bazel_dep(name = "rules_java", version = "7.4.0") +bazel_dep(name = "bazel_skylib", version = "1.3.0") + +bazel_dep( + name = "rules_android", + version = "0.5.1", +) + +local_path_override( + module_name = "rules_android", + path = "../../", +) + +remote_android_extensions = use_extension( + "@rules_android//bzlmod_extensions:android_extensions.bzl", + "remote_android_tools_extensions") +use_repo(remote_android_extensions, "android_tools") + +register_toolchains( + "@rules_android//toolchains/android:android_default_toolchain", + "@rules_android//toolchains/android_sdk:android_sdk_tools", +) + +android_sdk_repository_extension = use_extension("@rules_android//rules/android_sdk_repository:rule.bzl", "android_sdk_repository_extension") +use_repo(android_sdk_repository_extension, "androidsdk") + +register_toolchains("@androidsdk//:sdk-toolchain", "@androidsdk//:all") + +bazel_dep(name = "rules_jvm_external", version = "5.3") + +# Load the maven extension from rules_jvm_external +maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven") + +maven.install( + name = "maven", + aar_import_bzl_label = "@rules_android//rules:rules.bzl", + artifacts = [ + "com.google.guava:guava:32.1.2-android", + "com.facebook.soloader:soloader:0.12.1", + ], + repositories = [ + "https://maven.google.com", + "https://repo1.maven.org/maven2", + ], + use_starlark_android_rules = True, +) +use_repo(maven, "maven") + + + + diff --git a/examples/bundle/README.md b/examples/bundle/README.md new file mode 100644 index 000000000..0d923e32d --- /dev/null +++ b/examples/bundle/README.md @@ -0,0 +1,12 @@ +To build, ensure that the `ANDROID_HOME` environment variable is set to the path +to an Android SDK, and run: + +``` +bazel build app:assets +``` + +This will build application bundle containing a dynamic feature containing assets (named assets.txt). Verify with : + +``` +jar -tf bazel-bin/app/assets_unsigned.aab | grep assets.txt +``` diff --git a/examples/bundle/WORKSPACE b/examples/bundle/WORKSPACE new file mode 100644 index 000000000..a23815374 --- /dev/null +++ b/examples/bundle/WORKSPACE @@ -0,0 +1,58 @@ +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe") + +maybe( + http_archive, + name = "rules_jvm_external", + strip_prefix = "rules_jvm_external-fa73b1a8e4846cee88240d0019b8f80d39feb1c3", + sha256 = "7e13e48b50f9505e8a99cc5a16c557cbe826e9b68d733050cd1e318d69f94bb5", + url = "https://github.com/bazelbuild/rules_jvm_external/archive/fa73b1a8e4846cee88240d0019b8f80d39feb1c3.zip", +) + +maybe( + http_archive, + name = "bazel_skylib", + urls = [ + "https://github.com/bazelbuild/bazel-skylib/releases/download/1.0.3/bazel-skylib-1.0.3.tar.gz", + "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.0.3/bazel-skylib-1.0.3.tar.gz", + ], + sha256 = "1c531376ac7e5a180e0237938a2536de0c54d93f5c278634818e0efc952dd56c", +) +load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace") +bazel_skylib_workspace() + +local_repository( + name = "rules_android", + path = "../..", # rules_android's WORKSPACE relative to this inner workspace +) + +load("@rules_android//:prereqs.bzl", "rules_android_prereqs") +rules_android_prereqs() +load("@rules_android//:defs.bzl", "rules_android_workspace") +rules_android_workspace() + +load("@rules_android//rules:rules.bzl", "android_sdk_repository") +android_sdk_repository( + name = "androidsdk", +) + +register_toolchains( + "@rules_android//toolchains/android:android_default_toolchain", + "@rules_android//toolchains/android_sdk:android_sdk_tools", +) + +load("@rules_jvm_external//:defs.bzl", "maven_install") + +maven_install( + name = "maven", + aar_import_bzl_label = "@rules_android//rules:rules.bzl", + artifacts = [ + "com.google.guava:guava:32.1.2-android", + "com.arthenica:ffmpeg-kit-https:4.4.LTS", + ], + repositories = [ + "https://maven.google.com", + "https://repo1.maven.org/maven2", + ], + use_starlark_android_rules = True, +) diff --git a/examples/bundle/WORKSPACE.bzlmod b/examples/bundle/WORKSPACE.bzlmod new file mode 100644 index 000000000..df9ed0ec9 --- /dev/null +++ b/examples/bundle/WORKSPACE.bzlmod @@ -0,0 +1 @@ +workspace(name = "bundle") diff --git a/examples/bundle/app/AndroidManifest.xml b/examples/bundle/app/AndroidManifest.xml new file mode 100644 index 000000000..1a4a7a99d --- /dev/null +++ b/examples/bundle/app/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + diff --git a/examples/bundle/app/BUILD b/examples/bundle/app/BUILD new file mode 100644 index 000000000..aff62ba6a --- /dev/null +++ b/examples/bundle/app/BUILD @@ -0,0 +1,31 @@ +load("@rules_android//android:rules.bzl", "android_application", "android_library") + +android_application( + name = "assets", + manifest_values = { + "applicationId" : "com.examples.bundle.app", + "versionCode": "0", + }, + feature_modules = ["//features/assets:feature_module"], + manifest = "AndroidManifest.xml", + deps = [":lib"], +) + +android_application( + name = "native", + manifest_values = { + "applicationId" : "com.examples.bundle.app", + "versionCode": "0", + }, + feature_modules = ["//features/native:feature_module"], + manifest = "AndroidManifest.xml", + deps = [":lib"], +) + +android_library( + name = "lib", + srcs = ["BasicActivity.java"], + manifest = "AndroidManifest.xml", + resource_files = glob(["res/**"]), + deps = ["@maven//:com_google_guava_guava",] +) diff --git a/examples/bundle/app/BasicActivity.java b/examples/bundle/app/BasicActivity.java new file mode 100644 index 000000000..f051263ba --- /dev/null +++ b/examples/bundle/app/BasicActivity.java @@ -0,0 +1,59 @@ +// Copyright 2022 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.examples.bundle.app; + +import android.app.Activity; +import android.os.Bundle; +import android.view.Menu; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +/** + * The main activity of the Basic Sample App. + */ +public class BasicActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.basic_activity); + + final Button buttons[] = { + findViewById(R.id.button_id_fizz), findViewById(R.id.button_id_buzz), + }; + + for (Button b : buttons) { + b.setOnClickListener( + new View.OnClickListener() { + public void onClick(View v) { + TextView tv = findViewById(R.id.text_hello); + if (v.getId() == R.id.button_id_fizz) { + tv.setText("fizz"); + } else if (v.getId() == R.id.button_id_buzz) { + tv.setText("buzz "); + } + } + }); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu, menu); + return true; + } +} diff --git a/examples/bundle/app/res/drawable-hdpi/ic_launcher.png b/examples/bundle/app/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..6ab2adde210fde3d980a0d3d9c551dcf61a96a86 GIT binary patch literal 1678 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 Q1ysb~>FVdQ&MBb@0DAuQWB>pF literal 0 HcmV?d00001 diff --git a/examples/bundle/app/res/drawable-xhdpi/ic_launcher.png b/examples/bundle/app/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..014b0f1064d5d3a51451970d0bb39b954e805264 GIT binary patch literal 1817 zcmZ`&3se(l7XBj)nUGe3&>|=#C>U8Eq#%m~2zfuM;Ss=7dC5Bkwj_mtNF-rQWvi>Z zu+b_D%46NG-Lkt#6)9B_wOVRn6~q!(d5INufj})x+7Rp?XnW4?IXmb6|NZYb-@W&n zJ2Qu3qb098dpQFDUX@9O3h2}A3w40|J@NbY8;(kzoCiQ_6?Gv4fiWXXs*nSq-2ec7 z0DvDLgg*tKgayF!bO6{j0C=cReij!20BIn0XMzY4O8ZJm`>@h}EccpHd0i>{tNhI~ za(;tY&?pf!NcfG=#IEK3u1Twa$_wxTNLdMXQGd#^=l+zwbXW@e?bYmIKbC$ev&3G! z1iMoV4IW^aaXC|b`pvY@_CO^0)6}$!dwBIC*eJt^F%{Z+(q?#~2BvvVk=4Ek>aVMpF6 z5nbg=iE&;gkknp`_X8Q~btC}^A|#=Gw-2FF!mj1<8pP~l96^&r(Hs~2L4*_LL$BQG zw38fsI5h6UE>FWIp!NbPPf*|iqz?8e2}z#0YwQzSHi-OUkeUCwownBpo!C`CIH@ucdE}@g}(dj8DZn5kqP(){W&p_yrwWh`IO!SfOZG?^ZeuO?YMz?tn&(GUz z!`_IFu))K;&7w!p3P^y4qc#FEZ4{z{uvu`lf*=SSg>G??z$wr}j6zr}lyV&fK|GF9 z-Jl9m<_?Jv97MdOFrcWVyNdx>dELH9U#LFHh9DV}$wlNRD?Hq`AafH#@T+yu$b`Iv zw}+?hD3jfI?#O6c{AB9cOaV=lfWAFBWV+#VV>qrgvt^CJ@R6_cm9Kj@TnsNZpZZf> z_KT~MZ{N5bHZjz3JmgzzRmJJtp0(bkiu~T%Nab{m`lzBHEpcuEIq4G>P?4B8=TUce zoXIO_np)_I)UWFN_l2-uoKf4>@kLC3%dh7wvU|;Qk$QzKKeei1;BwN?ICHLL7liLN zKl6#o(}&y}XNI-hjMN*6eA76ygZSgV1;3Y^%&LZ-2@VhLW=@*QH9WvM%Omh_GIjo; z+7G`v9(;7uu0OokKik%<{f#&4F4_2`{^H1kuV_JEX74EO0K%Tjllq`9<>Krv(~J3D{#FV}5NMj%;MzEK$lBxQ zv@No%;e;Q^ahX16bZ>k*ri(T@cKfv5miB|b(?qWf!L=XjvJ?njuN^s-utE{ju9ALC zYC1DA$PB?pf~N|OvKfQH=KmOz7!R^03ykYx`@EX8?;O#E-WU6?|Ho^b*p9s5zQcw0 zZ<#d42Y-%xwBv%gW;F=cOx?T@xsUJ<3zSYrscA30!?Eq)@3+=VT4&}=vu?Rfp=F=g z20|}Y{BY!K3{_`S-u6V4FBdA!4m0NMCT3;lB3=4xG8u1hsBzRpePH0e zqKgXH-qo1aJ$=TP#w1#+N$2^6^c4d-=~ZN#q1^?qd1P&^MvfV-1UkIi%b^wFe01KQ ztD|q;3j$E-*aJ-f@}j{f!0VF?XXyUlb`XUyYNZ(6tW-Y5s`@w@bxPs_gv1wq#mFEvEl zI!Yt2m^GDaLRGvj*TVMGHthMohW3@2lfV9p^^}WKpLuNk`G#rjLUz%j^?1lP&(2Mt ztG&csho?`(Q}3TaUcp|w6yuCOeXdWO;rMeKXRI3?fqOAMse1pTX@T>q*h$?+p5yPX zxcH*n)x=dkI8aRYvvA)c_8Z6tG<3#eoMm25-OYGtl^UoAbo97xFg7Bgt!;ECW-A8; z*FWs2Vq%?T$)7-t8go?n=CRHkx%Im;Z2%$BzkF;CwCG3j(k6S#;9u#FiQdApGq44h6N|-WvD$`bW&f4%jw(AZx9tBXeEyzn4kmco rJH)GUF-=BM4$x>cTk{GFinB7*Ia^gl%DU+YFGvD1QMB-%{B*;QT66hG literal 0 HcmV?d00001 diff --git a/examples/bundle/app/res/drawable-xxhdpi/ic_launcher.png b/examples/bundle/app/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..20703a15c6809f4919a823ff4b5f571b02c5e88f GIT binary patch literal 2137 zcmZ`(c~p~E7JmUk5)czKmHi_S6qEvy%?Jr5A(*g85;j}v0z{y|AX`WRC=d_<#Q>us zRSra9O6-gas68%N9Y7GODBD4yDvN9~f)<}m`RyI}_^2gO{jzFOo&)mTTpsC#Ok7zVJlZ4!m9ROr+03iPm09pv; z&j64~0pL{>0AAGqn55R+6wm-bUFHXc`9TM9Z>G3UBz`Ow_dz8?{7541OaFPcN!TTf zxxeqy+>c>RJ0qIE@l|kGb=!UGI4r0rwu2ONzye@59z+3IDQ36~1Q%)uZ%v3a-rXTw znZRd)bASZ()K8SS4sdmW1OahG5Qqj|07MZ$C=QNyWq=9}yguyT_bdmNvnh75NhQOI zHYF6W-xM%_O&S^20`OH~HeUc>Hjd6pewY)0y+H;mkD4V42@Zwwe=QUa<qB?rTMt@{k>O?al4fZb8CzCskZ}!S`TUJq^TG;!P@__-RhnFVT&_l=R_Eie z8ns4~3t2ucPXiZ3av{S(Yzdch5Qj#aI!|tfl~FN)95yIHTXEq5m&*M%1Ax_SD;w3B zqTB~TjL3h7A7)C&WUZ6_-{#BU7pphz&t`^6hhGoGODw_(`cY5YJqLql^jM^`<~p9E z{F;1R1q;*f&srb^uqum^?=mbc-fxND8F^v;xwo67J8@@C{qj$XJy}!F20hK&%z9D} zFW#xHwAkO0aP?;?^;FsXFUwU{LXUfGX4cO#k}?*Oy*DJ5`eHU1o|z38tn8qsM}^+* zGv(k1F1mMg6`elWA*GV%Th#%*FNRFZEvtK8pCcc8J0t5OA4}_yHJkj~6d!LH`D9<- zcv;$%><@^lWxt*5xIW5w9w6Lxo$^Z`bU3uf^!y1NALNTUkNXAG?P1}3de;TLzgc< zt*Q0Aw{YvvO?daLQ5nX_Fg`Bc+O^#}_vPIN%<0qKOUm5I2g1O>a*OdRn7ZoUEl!^q zx)emaP_@x#+ZMBe$iQNe<4A4Q#?^%z4X|B_;dv!u0qmTwKW zl|Hh5J}VQ`mx;UIxArIU)@*A|Jyf=B@jrt;GdJoxtIpqlyEdDbG`Vb|tUI;$z-+Ab zz!O#XKTmwGDj(W2l(X)|(xOZE;?#FCd*j};icm{g>kU^0#Iy#kEgx>#ftQ6=)S zWv~Q8g|czvYP~bpuA5FN>BWCUCP^~oG>RF7?-StL*KI|x?drR>n0Dd~1lYaF=RHI> zvC%y2nkwynnt0$w$a7^Aw` zC+yKDrxPUa5LDt(E>9=>4x6{hE9(eYf3CkFUihI9|LZ%ubLBoCU+S#azH{ro@^;x| zZ`G>=_lwJ~=29Gbr}Dl#F+H+y(P`*-Nl(SOhp#Ur(wpQC{5)Ev9Lv8$6LF}@JfxXS z9N^&xjrCt$Dkj({a&z7^>KAGknBF?faOq_^iSL0lkU`i<%)EnM_%zDffDxXpkRRY* zLlk6u*fo3NSIohjg2o6iIQG^%ovD(x$SwK1YhyTc{=r%nXR9)}d_-UpcBymLL3i-= zY~}!JaG}dD`7ud)ljnpsI&44ZfHFGL_^elRW~6Y!-&;G@S*L#=c3k^@`4sARck`hS z+7VTz#bLLau~m&VH?aF1t}iWmT@QkP!Sy1)Z$$f|MX_GO^jIi>E5+5-iQ?+y>K3+n ztCzcn7sb<&Lh+(d4tdTE{3RecWnV&E*8dMsC53(u1I$(`gr>xaB+==yKq8SiC+ttk b5JsoQI;W(^pP8qbLnq+x7tFrMiYoXov?hQf literal 0 HcmV?d00001 diff --git a/examples/bundle/app/res/layout/basic_activity.xml b/examples/bundle/app/res/layout/basic_activity.xml new file mode 100644 index 000000000..f84199cb5 --- /dev/null +++ b/examples/bundle/app/res/layout/basic_activity.xml @@ -0,0 +1,23 @@ + + + + +