From 990e24ed9d1a9c5ce45f9245ea701c25838725eb Mon Sep 17 00:00:00 2001 From: Dean Ellis Date: Tue, 27 Apr 2021 10:48:41 +0100 Subject: [PATCH] [Xamarin.Andorid.Build.Tasks] First Pass on Dynamic Asset Features Context #4810 This is the first implementation for supporting building Dynamic Feature Assets modules for Android. This idea is to have these "features" as standard Xamarin Android Library projects. The `ProjectReference` will need to use the following ```xml false true ``` We set `ReferenceOutputAssembly` to `false` to ensure that the assembly for the feature is NOT included in the final aab file. The new meta Data `AndroidDynamicFeature` allows the build system to pick up project references which are "features". As part of the final packaging step of the main app we will gather up all the `ProjectRefernce` items which have `AndroidDynamicFeature` set to `true` (and maybe `ReferenceOutputAssembly` set to `false`). This will be done by the `_BuildDynamicFeatures` which will run just after `_CreateBaseApk`. It will call `_GetDynamicFeatureOutputs` for each `ProjectReference` which will collect the `output` files for each feature. It will then call the `BuildDynamicFeature` target via the `MSBuild` task for each `ProjectReference`. The `BuildDynamicFeature` is the target responsible for collecting all the assets and packaging them using `aapt2` up into a zip. Once all the `BuildDynamicFeature` calls are complete the created `zip` files will be added to the `AndroidAppBundleModules` and then included in the final `aab` file. It might seem odd that the feature projects are built after the main app. However this is required because the feature needs to use the `packaged_resources` file as an input to `aapt2` when building the feature. This is why the `_BuildDynamicFeatures` happens AFTER `_CreateBaseApk`. It is only at that point that the final `packaged_resources` file exists. One of the very weird things is that the feature zip needs to be built using the `aapt2` `--static-lib` flag. As a result we need to call `aapt2 convert` on the final zip. This is because it is in the `apk` `binary` format and needs to be converted over to the `aab` `proto` format. So there is a new `Aapt2Convert` task which handles that job. It also makes sure the `AndroidManifest.xml` file is in the right place when converting to `proto` format. A basic project example using .net 6 for a feature would look like this. ```xml net6.0-android ``` As you can see it is just a normal library project. At this time is CANNOT contain any `AndroidResource` items such as drawables or layouts. It must only contain `AndroidAsset` items. So we probably should have a new template for a `Dynamic Feature` which just creates the `csproj` and the `Assets` folder. One sticking point is probably the `AndroidManifest.xml` file which we need for a `feature`. There is a sample ```xml ``` The interesting parts are all the additional `dist` elements. What we can probably do is auto generate this during the `BuildDynamicFeature`. However we need to think carefully about this since if we plan to have code and `Activities` in the feature at some point, those will also need to end up in the `AndroidManifest.xml`. For additional information on the Play Core Dynamic Features check the following links. [1] https://developer.android.com/guide/playcore/asset-delivery [2] https://developer.android.com/guide/playcore/feature-delivery --- Documentation/guides/DynamicFeatures.md | 461 ++++++++++++++++++ .../installers/create-installers.targets | 1 + .../Android/Xamarin.Android.Aapt2.targets | 128 +++++ .../Xamarin.Android.DynamicFeature.targets | 115 +++++ .../Microsoft.Android.Sdk.BuildOrder.targets | 3 +- .../Tasks/Aapt2Convert.cs | 64 +++ .../Tasks/Aapt2Link.cs | 45 +- .../Tasks/BuildApk.cs | 23 +- .../Tasks/BuildBaseAppBundle.cs | 12 +- .../Tasks/CalculatePackageIdsForFeatures.cs | 45 ++ .../Tasks/CreateDynamicFeatureManifest.cs | 137 ++++++ .../Tasks/InstallApkSet.cs | 4 +- .../Tasks/ReadAndroidManifest.cs | 11 + .../DynamicFeatureTests.cs | 142 ++++++ .../CreateDynamicFeatureManifestTests.cs | 69 +++ .../Utilities/ManifestDocument.cs | 18 +- .../Utilities/ZipArchiveEx.cs | 16 + .../Xamarin.Android.Build.Tasks.targets | 4 + .../Xamarin.Android.Common.targets | 10 +- .../Tests/BundleToolTests.cs | 2 +- 20 files changed, 1279 insertions(+), 31 deletions(-) create mode 100644 Documentation/guides/DynamicFeatures.md create mode 100644 src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.DynamicFeature.targets create mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/Aapt2Convert.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/CalculatePackageIdsForFeatures.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/CreateDynamicFeatureManifest.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/DynamicFeatureTests.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/CreateDynamicFeatureManifestTests.cs diff --git a/Documentation/guides/DynamicFeatures.md b/Documentation/guides/DynamicFeatures.md new file mode 100644 index 00000000000..161279e3b9d --- /dev/null +++ b/Documentation/guides/DynamicFeatures.md @@ -0,0 +1,461 @@ +# Android Dynamic Feature Delivery + +_NOTE: This document is very likely to change, as the the implementation +of Dynamic Features matures._ + +Android Dynamic Features are a way of splitting up your application +into smaller parts which users can download on-demand. This can be +useful in scenarios such as Games or apps with low usage parts. +An example would be a Support module which does not get used very +often, rather than including this code in the main app it could be +a dynamic feature which is downloaded when the user requests support. +Alternatively think of Game levels in a game. On first install only +the first few levels are installed to save download size. But later +the additional levels can be downloaded while the user is not playing +the game. + +Dynamic Features can ONLY be used when targeting the Google Play +Store and using `PackageFormat` set to `aab`. Using `apk` will +NOT work. + +## Xamarin Dynamic Asset/Feature Delivery + +The Xamarin implementation of this will start by focusing on asset +delivery. This is where the `AndroidAsset` items can be split out +into a separate module for downloading later. + +_NOTE: In the future we plan to support feature or code delivery +however there are significant challenges to supporting that in our +runtime. So this will be done after the initial rollout of asset +delivery._ + +The new Dynamic Feature Module will be based around a normal +Xamarin Android class library. The kind you get when calling + +``` +dotnet new androidlib +``` + +Although we plan to provide a template. The new Module will generally +be nothing different from a normal class library project. The +current restrictions are that it only contains `AndroidAsset` items. +No `AndroidResource` items can be included and any C# code in the +library will NOT end up in the application. + +The one additional file is a special `AndroidManifest.xml` file which +will be generated as part of the module build process. This manifest +file needs a few special elements which need to be provided by +the following MSBuild properties. + +* FeatureSplitName: (string) The name of the feature. You will use this in code to + install the feature. Defaults to `ProjectName`. +* IsFeatureSplit: (bool) Defines if this feature is a "feature split". Defaults to `true`. +* FeatureDeliveryType: (Enum) The type of delivery mechanism to use. Valid values are + OnDemand or InstallTime. Defaults to InstallTime. +* FeatureType: (Enum) The type of feature this is. Valid values are + Feature or AssetPack. Defaults to Feature. +* FeatureTitleResource: (string) This MUST be a valid @string which is present in your + application strings.xml file. For example `@strings/assetsfeature`. + Note the actual value of the resource can be 50 characters long and can be localized. + It is this resource which is used to let the users know which feature is being + downloaded. So it should be descriptive. + This Item does NOT have default and will need to be provided. + +These properties will have default values based on things like the Project name. + +Here is a example of an Asset Feature Module for .net 6. + +```xml + + + net6.0-android + + + assetsfeature + Asset + true + OnDemand + + +``` + +This is defining an OnDemand Asset Delivery package. +The follow code is defining a Dynamic Feature Module. + +```xml + + + net6.0-android + + + examplefeature + Feature + true + OnDemand + @strings/examplefeature + + +``` + +In order to reference a feature you can use a normal `ProjectReference` MSBuild +Item. We will detect the use of `FeatureSplitName` in each project and will +remove that project from the normal build process. + +Features will then build later in the build process, after the main app +has built its base resource package but before the linker runs. + +As part of the build system all `Features` will include a `Reference` to +the main application `Assembly`. So there is no need to add any `PackageReference` +items to any NuGet which is already being used by main app as they will be +automatically referenced when we build the feature. + +## Downloading a Dynamic Feature at runtime + +In order to download features you need to reference the +`Xamarin.Google.Android.Play.Core` Nuget Package from your main application. +You can do this by adding the following Package References. + +```xml + + +``` + +Note that `Xamarin.GooglePlayServices.Base` is also required. The `Version` numbers +here will change as new versions are being released all the time. The minimum +required `Xamarin.Google.Android.Play.Core` is `1.10.2`. + +This is because some of the classes we need to use in the `Play.Core` API use Java +Generics. The Xamarin Android Binding system has a problem with these types of +classes. So some of the API will either be missing or will produce build errors. +In `Xamarin.Google.Android.Play.Core` `1.10.2` we added a set of `Wrapper` classes +which expose a non Generic API in C# which will allow users to use the required +`Listener` classes. + +### Installing Asset Packs + +If you have created an `InstallTime` asset pack, there is no additional work to do. +The pack will be installed at the same time the app is installed. As a result the +`Assets` will be available via the normal `AssetManager` API. + +If you create an `OnDemand` asset pack, you will need to manually start the installation +and monitor its progress. To do this you need to use the `IAssetPackManager`. This can +be created using + +```csharp +IAssetPackManager manager = AssetPackManagerFactory.GetInstance (this); +``` + +You can then use the following code to check if the Asset Pack was installed. If it was +not, then you can start the installation with the `IAssetPackManager.Fetch` method. + +```csharp +var location = assetPackManager.GetPackLocation ("assetsfeature"); +if (location == null) +{ + assetPackManager.Fetch(new string[] { "assetsfeature" }); +} +``` + +Note the argument passed in there is the same as the `FeatureSplitName` MSbuild property. +In order to monitor the progress of the download google provided a `AssetPackStateUpdateListener` +class. However this class uses Generics and if you use it directly it will result in +java build errors. So the `Xamarin.Google.Android.Play.Core` version `1.10.2` Nuget provides +a `AssetPackStateUpdateListenerWrapper` class which exposes a C# event based API to make +things easier. + +```csharp +listener = new AssetPackStateUpdateListenerWrapper(); +listener.StateUpdate += (s, e) => { + var status = e.State.Status(); + switch (status) + { + case AssetPackStatus.Pending: + break; + // more case statements here + } +}; +``` + +The `StateUpdate` event will allow you to get the download status if any Asset Packs which +are being downloaded. +In order for this listener to work we need to register it with the `IAssetPackManager` we +created earlier. We do this in the `OnResume` and `OnPause` methods in the activity. + +To get the `manager` object to use this listener we need to call the `RegisterListener` method. +We also need to call the `UnregisterListener` as well when we are finish or if the app is paused. +Because we are using a wrapper class we cannot just pass our `listener` to the `RegisterListener` +method directly. This is because it is expecting a `AssetPackStateUpdateListener` type. +But we do provide a property on the `AssetPackStateUpdateListenerWrapper` which will give +you access to the underlying `AssetPackStateUpdateListener` class. So to register you can +use the following code. + +```csharp +protected override void OnResume() +{ + // regsiter our Listener Wrapper with the IAssetPackManager so we get feedback. + manager.RegisterListener(listener.Listener); + base.OnResume(); +} + +protected override void OnPause() +{ + manager.UnregisterListener(listener.Listener); + base.OnPause(); +} +``` + +With the `listener` registered you will now get status updates during a download. +Here is a sample activity. + +```csharp +using Android.App; +using Android.OS; +using Android.Runtime; +using Android.Widget; +using Java.Lang; +using Xamarin.Google.Android.Play.Core.AssetPacks; +using Xamarin.Google.Android.Play.Core.AssetPacks.Model; +using Android.Content; + +namespace DynamicAssetsExample +{ + [Activity(Label = "@string/app_name", MainLauncher = true)] + public class MainActivity : Activity + { + IAssetPackManager manager; + AssetPackStateUpdateListenerWrapper listener; + protected override void OnCreate(Bundle savedInstanceState) + { + base.OnCreate(savedInstanceState); + + // Set our view from the "main" layout resource + SetContentView(Resource.Layout.activity_main); + var button = FindViewById