|
| 1 | +# Android Asset Packs |
| 2 | + |
| 3 | +Google Android began supporting splitting up the app package into multiple |
| 4 | +packs with the introduction of the `aab` package format. This format allows |
| 5 | +the developer to split the app up into multiple `packs`. Each `pack` can be |
| 6 | +downloaded to the device either at install time or on demand. This allows |
| 7 | +application developers to save space and install time by only installing |
| 8 | +the required parts of the app initially, then installing other `packs` |
| 9 | +as required. |
| 10 | + |
| 11 | +There are two types of `pack`: Feature packs and Asset packs. |
| 12 | +*Feature* pack contains *non-native* Java code and other resources. |
| 13 | +Code in these types of `pack` can be launched via the `Context.StartActivity()` |
| 14 | +API call. At this time due to various constraints .NET Android cannot support |
| 15 | +Feature packs. |
| 16 | + |
| 17 | +*Asset* packs contain *only* |
| 18 | +[`@(AndroidAsset)`](~/android/deploy-test/building-apps/build-items.md#androidasset) items. |
| 19 | +It *cannot* contain any code or other resources. This type of `pack` can be |
| 20 | +installed at install-time, fast-follow or ondemand. It is most useful for apps |
| 21 | +which contain a lot of Assets, such as Games or Multi Media applications. |
| 22 | +See the [Android Asset Delivery documentation](https://developer.android.com/guide/playcore/asset-delivery) |
| 23 | +for details on how this all works. |
| 24 | + |
| 25 | +## Asset Pack Specification |
| 26 | + |
| 27 | +We want to provide our users the ability to use `Asset` packs without relying |
| 28 | +on [Alternative Methods](#alternativemethods) |
| 29 | + |
| 30 | +Add support for new `%(AndroidAsset.AssetPack)` item metadata, which |
| 31 | +allows the build system to split up the assets into packs automatically: |
| 32 | + |
| 33 | +```xml |
| 34 | +<ItemGroup> |
| 35 | + <AndroidAsset Include="Asset/data.xml" /> |
| 36 | + <AndroidAsset Include="Asset/movie.mp4" AssetPack="assets1" /> |
| 37 | + <AndroidAsset Include="Asset/movie2.mp4" AssetPack="assets1" /> |
| 38 | +</ItemGroup> |
| 39 | +``` |
| 40 | + |
| 41 | +The default value for `%(AndroidAsset.AssetPack)` is `base`, which will |
| 42 | +cause the asset to be included in the main application package. |
| 43 | + |
| 44 | +As auto import of items is now common, we need a way for a user to add |
| 45 | +this additional attribute to auto included items. This can be done by |
| 46 | +using `Update`: |
| 47 | + |
| 48 | +```xml |
| 49 | +<ItemGroup> |
| 50 | + <AndroidAsset Update="Asset/movie.mp4" AssetPack="assets1" /> |
| 51 | + <AndroidAsset Update="Asset/movie2.mp4" AssetPack="assets1" /> |
| 52 | + <AndroidAsset Update="Asset/movie3.mp4" AssetPack="assets2" /> |
| 53 | +</ItemGroup> |
| 54 | +``` |
| 55 | + |
| 56 | +`%(AndroidAsset.DeliveryType)` item metadata can be specified to control what |
| 57 | +*type* of asset pack is produced. Valid values are: |
| 58 | + |
| 59 | + * `InstallTime`: Asset pack will be delivered when the app is installed. |
| 60 | + This is the default value for assets not in the base package. |
| 61 | + * `FastFollow`: Asset pack will be downloaded automatically as soon as the app is installed. |
| 62 | + * `OnDemand`: Asset pack will be downloaded while the app is running. |
| 63 | + |
| 64 | +The `DeliveryType` for a given asset pack is based on the *first* |
| 65 | +`@(AndroidAsset.DeliveryType)` value encountered for a `%(AssetPack)` name. |
| 66 | + |
| 67 | +Consider the following example, in which `Asset/movie2.mp4` and `Asset/movie3.mp4` |
| 68 | +are both in the `assets1` pack, which will have a `%(DeliveryType)` of `InstallTime` |
| 69 | +(the first encountered value "wins"). `Asset/movie1.mp4` will be in the base package, |
| 70 | +while `Asset/movie4.mp4` will be in the "asset2" asset pack. |
| 71 | + |
| 72 | +```xml |
| 73 | +<ItemGroup> |
| 74 | + <AndroidAsset Update="Asset/movie1.mp4" /> |
| 75 | + <AndroidAsset Update="Asset/movie2.mp4" AssetPack="assets1" DeliveryType="InstallTime" /> |
| 76 | + <AndroidAsset Update="Asset/movie3.mp4" AssetPack="assets1" DeliveryType="FastFollow" /> |
| 77 | + <AndroidAsset Update="Asset/movie4.mp4" AssetPack="assets2" /> |
| 78 | +</ItemGroup> |
| 79 | +``` |
| 80 | + |
| 81 | +See Google's [documentation](https://developer.android.com/guide/playcore/asset-delivery#asset-updates) for details on what each of the `DeliveryType` values do. |
| 82 | + |
| 83 | +If however you have a large number of assets it might be cleaner in the |
| 84 | +`.csproj` to make use of the `base` value for the `%(AssetPack)` attribute. |
| 85 | +In this scenario you update *all* assets to be in a single asset pack then use |
| 86 | +`AssetPack="base"` metadata to declare which specific assets end up in the base |
| 87 | +aab file. With this you can use wildcards to move most assets into the asset pack: |
| 88 | + |
| 89 | +```xml |
| 90 | +<ItemGroup> |
| 91 | + <AndroidAsset Update="Assets/*" AssetPack="assets1" /> |
| 92 | + <AndroidAsset Update="Assets/movie.mp4" AssetPack="base" /> |
| 93 | + <AndroidAsset Update="Assets/some.png" AssetPack="base" /> |
| 94 | +</ItemGroup> |
| 95 | +``` |
| 96 | + |
| 97 | +In this example, `movie.mp4` and `some.png` will end up in the `base` aab file, |
| 98 | +but *all the other assets* will end up in the `assets1` asset pack. |
| 99 | + |
| 100 | +At this time the `@(AndroidAsset)` build action does not support `%(AssetPack)` |
| 101 | +or `%(DeliveryType)` Metadata in Library Projects. |
| 102 | + |
| 103 | +NOTE: `AssetPacks` are only used when the |
| 104 | +[`$(AndroidPackageFormat)`](~/android/deploy-test/building-apps/build-properties.md#debugsymbols) |
| 105 | +property is set to `aab` (the default for Release). |
| 106 | +When using the `apk` setting the assets will be placed inside the `apk`. |
| 107 | + |
| 108 | +## Release Configuration |
| 109 | + |
| 110 | +In order for the application to function correctly we need to inform the `R8` |
| 111 | +linker which Java classes we need to keep. To do this we need to add the |
| 112 | +following lines to a `ProGuard.cfg` file which is in the root of our project folder: |
| 113 | + |
| 114 | +``` |
| 115 | +-keep com.google.android.play.* |
| 116 | +``` |
| 117 | + |
| 118 | +Alternatively you can create a file called `ProGuard.cfg` and use the |
| 119 | +[@(ProguardConfiguration)](~/android/deploy-test/building-apps/build-items.md#proguardconfiguration) |
| 120 | +build action. Adding these lines will ensure that all the required Java components are not linked |
| 121 | +away during the Release build. |
| 122 | + |
| 123 | +## Testing and Debugging |
| 124 | + |
| 125 | +In order to test your asset packs in the `Debug` configuration, you will need to |
| 126 | +make some changes to your `.csproj`. Firstly we need to change the |
| 127 | +`$(AndroidPackageFormat)` to `aab`. It will be `aab` by default for `Release` builds, |
| 128 | +but will default to `apk` for `Debug` builds. Setting the `AndroidPackageFormat` to `aab` |
| 129 | +will disable fast deployment, so it is advised that you only do this when you need to test |
| 130 | +your `AssetPacks`. |
| 131 | + |
| 132 | +To test your asset packs add the following to the first `<PropertyGroup/>` in your `.csproj`. |
| 133 | + |
| 134 | +```xml |
| 135 | +<AndroidPackageFormat>aab</AndroidPackageFormat> |
| 136 | +<AndroidBundleToolExtraArgs Condition=" '$(Configuration)' == 'Debug' ">--local-testing $(AndroidBundleToolExtraArgs)</AndroidBundleToolExtraArgs> |
| 137 | +``` |
| 138 | + |
| 139 | +The `--local-testing` argument tells the `bundletool` application to install all the asset packs |
| 140 | +in a local cache on the device. `InstallTime` packs will be installed during the app installation process. |
| 141 | + |
| 142 | +`FastFollow` packs behave like `OnDemand` packs. They will not automatically installed when the app |
| 143 | +is sideloaded. You will need to request them manually when the game starts. |
| 144 | + |
| 145 | +For more details see [https://developer.android.com/guide/playcore/asset-delivery/test](https://developer.android.com/guide/playcore/asset-delivery/test). |
| 146 | + |
| 147 | +## Implementation Details |
| 148 | + |
| 149 | +There are a few changes we need to make in order to support this feature. |
| 150 | +One of the issues we will hit is the build times when dealing with large assets. |
| 151 | +Current the assets which are to be included in the `aab` are ***copied*** |
| 152 | +into the `$(IntermediateOutputPath)assets` directory. This folder is |
| 153 | +then passed to `aapt2` for the build process. |
| 154 | + |
| 155 | +The new system adds a new directory `$(IntermediateOutputPath)assetpacks`. |
| 156 | +This directory would contain a subdirectory for each `pack` that the |
| 157 | +user wants to include. |
| 158 | + |
| 159 | +```dotnetcli |
| 160 | +assetpacks/ |
| 161 | + assets1/ |
| 162 | + assets/ |
| 163 | + movie2.mp4 |
| 164 | + assets2/ |
| 165 | + assets/ |
| 166 | + movie3.mp4 |
| 167 | +``` |
| 168 | + |
| 169 | +All the building of the `pack` zip file would take place in these subfolders. |
| 170 | +The name of the pack will be based on the main "packagename" with the asset pack |
| 171 | +name appended to the end. e.g `com.microsoft.assetpacksample.assets1`. |
| 172 | + |
| 173 | +During the build process we identify all the `AndroidAsset` items which |
| 174 | +have an `AssetPack` attribute. These files are then copied to the |
| 175 | +new `$(IntermediateOutputPath)assetpacks` directory rather than the |
| 176 | +existing `$(IntermediateOutputPath)assets` directory. This allows us to |
| 177 | +continue to support the normal `AndroidAsset` behavior while adding the |
| 178 | +new system. |
| 179 | + |
| 180 | +Once we have collected and copied all the assets we then use the new |
| 181 | +`<GetAssetPacks/>` Task to figure out which asset packs we need to create. |
| 182 | +We then call the `<CreateDynamicFeatureManifest/>` task to create a required |
| 183 | +`AndroidManifest.xml` file for the asset pack. This file will end |
| 184 | +up in the same `$(IntermediateOutputPath)assetpacks` directory. |
| 185 | +We call this Task `<CreateDynamicFeatureManifest/>` because it can be used |
| 186 | +to create any feature pack if and when we get to implement full feature |
| 187 | +packs. |
| 188 | + |
| 189 | +```dotnetcli |
| 190 | +assetpacks/ |
| 191 | + assets1/ |
| 192 | + AndroidManifest.xml |
| 193 | + assets/ |
| 194 | + movie2.mp4 |
| 195 | + assets2/ |
| 196 | + AndroidManifest.xml |
| 197 | + assets/ |
| 198 | + movie3.mp4 |
| 199 | +``` |
| 200 | + |
| 201 | +We can then call `aapt2` to build these packs into `.zip` files. A new |
| 202 | +task `<Aapt2LinkAssetPack/>` task takes care of this. This is a special version |
| 203 | +of `Aapt2Link` which implements linking for asset packs only. |
| 204 | +It also takes care of a few problems which `aapt2` introduces. For some |
| 205 | +reason the zip file that is created has the `AndroidManifest.xml` file |
| 206 | +in the wrong place. It creates it in the root of the zip file, but the |
| 207 | +`bundletool` expects it to be in a `manifest` directory. |
| 208 | +`bundletool` will error out if its not in the right place. |
| 209 | +So `<Aapt2LinkAssetPack/>` takes care of this for us. It also removes a |
| 210 | +`resources.pb` which gets added. Again, `bundletool` will error if this |
| 211 | +file is in the zip file. |
| 212 | + |
| 213 | +Once the zip files have been created they are then added to the |
| 214 | +`@(AndroidAppBundleModules)` ItemGroup. This will ensure that when the |
| 215 | +final `.aab` file is generated they are included as asset packs. |
| 216 | + |
| 217 | +## Alternative Methods |
| 218 | + |
| 219 | +An alternative method is available on [github](https://github.com/infinitespace-studios/MauiAndroidAssetPackExample). |
| 220 | +This method allows developers to place additional assets in a special |
| 221 | +[NoTargets](https://github.com/microsoft/MSBuildSdks/blob/main/src/NoTargets/README.md) project. |
| 222 | +This project is built just after the final `aab` is produced. It builds a zip |
| 223 | +file which is then added to the `@(Modules)` ItemGroup in the main application. |
| 224 | +This zip is then included into the |
| 225 | +final app as an additional feature. |
| 226 | + |
| 227 | +Using a separate project like in the hack is one way to go. It does have some |
| 228 | +issues though. |
| 229 | + |
| 230 | + 1. It is a `special` type of project. It requires a `global.json` which imports |
| 231 | + the `NoTargets` sdk. |
| 232 | + 2. There is no IDE support for building this type of project. |
| 233 | + |
| 234 | +Having the user go through a number of hoops to implement this for |
| 235 | +.NET Android or .NET MAUI is not ideal. |
0 commit comments