Skip to content

Commit

Permalink
ff
Browse files Browse the repository at this point in the history
  • Loading branch information
dellis1972 committed Jan 12, 2024
1 parent 87341cc commit 804967b
Show file tree
Hide file tree
Showing 13 changed files with 694 additions and 62 deletions.
143 changes: 143 additions & 0 deletions Documentation/guides/AndroidAssetPacks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Android Asset Packs

Google Android began supporting splitting up the app package into multiple
packs with the introduction of the `aab` package format. This format allows
the developer to split the app up into multiple `packs`. Each `pack` can be
downloaded to the device either at install time or on demand. This allows
application developers to save space and install time by only installing the
required parts of the app initially. Then installing other `packs` as required.

There are two types of `pack`. The first is a `Feature` pack, this type of pack
contains code and other resources. Code in these types of `pack` can be launched via
the `StartActivity` API call. At this time due to various constraints .net Android
cannot support `Feature` packs.

The second type of `pack` is the `Asset` pack. This type of pack ONLY contains
`AndroidAsset` items. It CANNOT contain any code or other resources. This type of
`feature` pack can be installed at install-time, fast-follow or ondemand. It is most useful for
apps which contain allot of `Assets`, such as Games or Multi Media applications.
See the [documentation](https://developer.android.com/guide/playcore/asset-delivery) for details on how this all works.
.Net Android does not have any official support for this type of pack. However a
hack is available via the excellent @infinitespace-studios on [github](https://github.com/infinitespace-studios/MauiAndroidAssetPackExample). This hack allows
developers to place additional assets in a special `NoTargets` project. This project is
built just after the final `aab` is produced. It builds a zip file which is then added
to the `@(Modules)` ItemGroup in the main application. This zip is then included into
the final app as an additional feature.

## Asset Pack Specification

We want to provide our users the ability to use `Asset` packs without having to implement
the hack provided by @infinitespace-studios. Using a separate project like in the hack
is one way to go. It does have some issues though.

1. It is a `special` type of project. It requires a `global.json` which imports the
`NoTargets` sdk.
2. There is no IDE support for building this type of project.

Having the user go through a number of hoops to implement this for .net Android or .net Maui is not
ideal. We need a simpler method.

The proposal is to make use of additional metadata on `AndroidAsset` Items to allow the
build system to split up the assets into packs automatically. So it is proposed that we
implement support for something like this

```xml
<ItemGroup>
<AndroidAsset Include="Asset/data.xml" />
<AndroidAsset Include="Asset/movie.mp4" AssetPack="assets1" />
<AndroidAsset Include="Asset/movie2.mp4" AssetPack="assets1" />
</ItemGroup>
```

In this case the additional `AssetPack` attribute is used to tell the build system which
pack to place this asset in. Since auto import of items is common now we need
a way for a user to add this additional attribute to auto included items. Fortunately we are
able to use the following.

```xml
<ItemGroup>
<AndroidAsset Update="Asset/movie.mp4" AssetPack="assets1" />
<AndroidAsset Update="Asset/movie2.mp4" AssetPack="assets1" />
<AndroidAsset Update="Asset/movie3.mp4" AssetPack="assets2" />
</ItemGroup>
```

This code uses the `Update` attribute to tell MSBuild that we are going to update a specific
item. Note in the sample we do NOT need to include an `Update` for the `data.xml`, since this
is auto imported it will still end up in the main feature in the aab.

Additional attributes can be used to control what type of asset pack is produced.
The only extra one supported at this time is `DeliveryType`, this can have a value
of `InstallTime`, `FastFollow` or `OnDemand`. Additional attributes do not need to
be included on ALL items. Any one will do, only the `AssetPack` attribute will be
needed. See Google's [documentation](https://developer.android.com/guide/playcore/asset-delivery#asset-updates) for details on what each item does.

```xml
<ItemGroup>
<AndroidAsset Update="Asset/movie.mp4" AssetPack="assets1" DeliveryType="InstallTime" />
<AndroidAsset Update="Asset/movie2.mp4" AssetPack="assets1" />
<AndroidAsset Update="Asset/movie3.mp4" AssetPack="assets2" />
</ItemGroup>
```

If the `AssetPack` attribute is not present, the default behavior will be to include the
asset in the main application package.

## Implementation Details

There are a few changes we need to make in order to support this feature. One of the issues we
will hit is the build times when dealing with large assets. Current the assets which are to be
included in the `aab` are COPIED into the `$(IntermediateOutputPath)assets` directory. This
folder is then passed to `aapt2` for the build process.

The new system adds a new directory `$(IntermediateOutputPath)assetpacks`. This directory
would contain a subdirectory for each `pack` that the user wants to include.

```dotnetcli
assetpacks/
assets1/
assets/
movie1.mp4
feature2/
assets/
movie2.mp4
```

All the building of the `pack` zip file would take place in these subfolders.
The name of the pack will be based on the main "packagename" with the asset pack
name appended to the end. e.g `com.microsoft.assetpacksample.assets1`.

During the build process we identify ALL the `AndroidAsset` items which define an
`AssetPack` attribute. These files are then copied to the new `$(IntermediateOutputPath)assetpacks`
directory rather than the existing `$(IntermediateOutputPath)assets` directory.
This allows us to continue to support the normal `AndroidAsset` behavior while adding
the new system.

Once we have collected and copied all the assets we then use the new `GetAssetPacks` Task to
figure out which asset packs we need to create. We then call the `CreateDynamicFeatureManifest`
to create a required `AndroidManifest.xml` file for the asset pack. This file will end
up in the same `$(IntermediateOutputPath)assetpacks` directory.

```dotnetcli
assetpacks/
assets1/
AndroidManifest.xml
assets/
movie1.mp4
feature2/
AndroidManifest.xml
assets/
movie2.mp4
```

We can then call `aapt2` to build these packs into `.zip` files. A new task `Aapt2LinkAssetPack`
takes care of this. This is a special version of `Aapt2Link` which implements linking for asset packs only.
It also takes care of a few problems which `aapt2` introduces. For some reason the zip file
that is created has the `AndroidManifest.xml` file in the wrong place. It creates it in the
root of the zip file, but the `bundletool` expects it to be in a `manifest` directory.
`bundletool` will error out if its not in the right place. So `Aapt2LinkAssetPack` takes
care of this for us. It also removes a `resources.pb` which gets added. Again, `bundletool`
will error if this file is in the zip file.

Once the zip files have been created they are then added to the `` ItemGroup. This will ensure that
when the final `.aab` file is generated they are included as asset packs.
16 changes: 16 additions & 0 deletions Documentation/guides/building-apps/build-items.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@ or library project is built.
Supports [Android Assets](https://developer.android.com/guide/topics/resources/providing-resources#OriginalFiles),
files that would be included in the `assets` folder in a Java Android project.

The `AndroidAsset` ItemGroup also supports additional metadata for generating [Asset Packs](https://developer.android.com/guide/playcore/asset-delivery). Adding the `AssetPack` attribute to and `AndroidAsset` will automatically generate an asset pack of that name. This feature is only supported when using the `.aab`
`AndroidPackageFormat`. The following example will place `movie2.mp4` and `movie3.mp4` in separate asset packs.

```xml
<ItemGroup>
<AndroidAsset Update="Asset/movie.mp4" />
<AndroidAsset Update="Asset/movie2.mp4" AssetPack="assets1" />
<AndroidAsset Update="Asset/movie3.mp4" AssetPack="assets2" />
</ItemGroup>
```

This feature can be used to include large files in your application which would normally exceed the max
package size limits of Google Play.

The additional metadata is only supported on .NET Android 9 and above.

## AndroidAarLibrary

The Build action of `AndroidAarLibrary` should be used to directly
Expand Down
1 change: 1 addition & 0 deletions build-tools/installers/create-installers.targets
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Aapt2.targets" />
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Analysis.targets" />
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Application.targets" />
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Assets.targets" />
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Bindings.ClassParse.targets" />
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Bindings.Core.targets" />
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Bindings.Maven.targets" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<!--
***********************************************************************************************
Xamarin.Android.Assets.targets
WARNING: DO NOT MODIFY this file unless you are knowledgeable about MSBuild and have
created a backup copy. Incorrect changes to this file will make it
impossible to load or build your projects from the command-line or the IDE.
This file imports the version- and platform-specific targets for the project importing
this file. This file also defines targets to produce an error if the specified targets
file does not exist, but the project is built anyway (command-line or IDE build).
Copyright (C) 2010-2011 Novell. All rights reserved.
Copyright (C) 2011-2012 Xamarin. All rights reserved.
***********************************************************************************************
-->

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

<UsingTask TaskName="Xamarin.Android.Tasks.AndroidComputeResPaths" AssemblyFile="Xamarin.Android.Build.Tasks.dll" />
<UsingTask TaskName="Xamarin.Android.Tasks.Aapt2LinkAssetPack" AssemblyFile="Xamarin.Android.Build.Tasks.dll" />
<UsingTask TaskName="Xamarin.Android.Tasks.GetAssetPacks" AssemblyFile="Xamarin.Android.Build.Tasks.dll" />
<UsingTask TaskName="Xamarin.Android.Tasks.RemoveUnknownFiles" AssemblyFile="Xamarin.Android.Build.Tasks.dll" />
<UsingTask TaskName="Xamarin.Android.Tasks.CreateDynamicFeatureManifest" AssemblyFile="Xamarin.Android.Build.Tasks.dll" />

<!-- Assets build properties -->
<PropertyGroup>
<MonoAndroidAssetsDirIntermediate>$(IntermediateOutputPath)assets\</MonoAndroidAssetsDirIntermediate>
<MonoAndroidAssetPacksDirIntermediate>$(IntermediateOutputPath)assetpacks\packs</MonoAndroidAssetPacksDirIntermediate>
<MonoAndroidAssetsPrefix Condition="'$(MonoAndroidAssetsPrefix)' == ''">Assets</MonoAndroidAssetsPrefix>
</PropertyGroup>

<!-- Assets Build -->

<Target Name="UpdateAndroidAssets"
DependsOnTargets="$(CoreResolveReferencesDependsOn);_ComputeAndroidAssetsPaths;_CalculateAssetPacks;_GenerateAndroidAssetsDir" />

<Target Name="_ComputeAndroidAssetsPaths">
<AndroidComputeResPaths
ResourceFiles="@(AndroidAsset)"
IntermediateDir="$(MonoAndroidAssetsDirIntermediate)"
AssetPackIntermediateDir="$(MonoAndroidAssetPacksDirIntermediate)"
Prefixes="$(MonoAndroidAssetsPrefix)"
ProjectDir="$(ProjectDir)"
>
<Output ItemName="_AndroidAssetsDest" TaskParameter="IntermediateFiles" />
<Output ItemName="_AndroidResolvedAssets" TaskParameter="ResolvedResourceFiles" />
</AndroidComputeResPaths>
</Target>

<Target Name="_GenerateAndroidAssetsDir"
Inputs="@(_AndroidMSBuildAllProjects);@(_AndroidResolvedAssets)"
Outputs="@(_AndroidAssetsDest)">
<ItemGroup>
<_AssetDirectories Include="$(MonoAndroidAssetsDirIntermediate)" />
<_AssetDirectories Include="@(_AssetPacks->'%(AssetPackDirectory)')" />
</ItemGroup>
<MakeDir Directories="$(MonoAndroidAssetsDirIntermediate);$(MonoAndroidAssetPacksDirIntermediate)" />
<Copy SourceFiles="@(_AndroidResolvedAssets)" DestinationFiles="@(_AndroidAssetsDest)" SkipUnchangedFiles="true" />
<RemoveUnknownFiles Files="@(_AndroidAssetsDest)" Directories="@(_AssetDirectories)" RemoveDirectories="true" FileType="AndroidAsset" />
<Touch Files="@(_AndroidAssetsDest)" />
<ItemGroup>
<FileWrites Include="@(_AndroidAssetsDest)" />
</ItemGroup>
</Target>

<Target Name="_CalculateAssetPacks"
Condition=" '$(AndroidPackageFormat)' == 'aab' And '$(AndroidApplication)' == 'true' "
>
<!-- Enumerate the assetpacks directory and build a pack per top level directory -->
<GetAssetPacks Assets="@(AndroidAsset)">
<Output ItemName="_AndroidAsset" TaskParameter="AssetPacks" />
</GetAssetPacks>
<ItemGroup>
<_AssetPacks Include="@(_AndroidAsset)">
<AssetPackDirectory>$(MonoAndroidAssetPacksDirIntermediate)\%(_AndroidAsset.AssetPack)\assets</AssetPackDirectory>
<AssetPackOutput>$(MonoAndroidAssetPacksDirIntermediate)\%(_AndroidAsset.AssetPack).zip</AssetPackOutput>
<ManifestFile>$(MonoAndroidAssetPacksDirIntermediate)\%(_AndroidAsset.AssetPack)\AndroidManifest.xml</ManifestFile>
<DeliveryType Condition=" '%(_AndroidAsset.DeliveryType)' == '' ">InstallTime</DeliveryType>
</_AssetPacks>
</ItemGroup>
</Target>

<Target Name="_CreateAssetPackManifests"
Condition=" '$(AndroidPackageFormat)' == 'aab' And '$(AndroidApplication)' == 'true' "
DependsOnTargets="UpdateAndroidAssets;_CalculateAssetPacks"
Inputs="@(_AssetPacks)"
Outputs="@(_AssetPacks->'%(ManifestFile)')">

<CreateDynamicFeatureManifest
FeatureSplitName="%(_AssetPacks.AssetPack)"
FeatureDeliveryType="%(_AssetPacks.DeliveryType)"
FeatureType="AssetPack"
PackageName="$(_AndroidPackage).%(_AssetPacks.AssetPack)"
OutputFile="%(_AssetPacks.ManifestFile)"
/>
<ItemGroup>
<FileWrites Include="%(_AssetPacks.ManifestFile)" />
</ItemGroup>
</Target>

<Target Name="_BuildAssetPacks"
AfterTargets="_CreateBaseApk"
DependsOnTargets="_CreateAssetPackManifests"
Condition=" '$(AndroidPackageFormat)' == 'aab' And '$(AndroidApplication)' == 'true' "
Inputs="@(_AssetPacks)"
Outputs="@(_AssetPacks->'%(AssetPackOutput)')">

<Aapt2LinkAssetPack
DaemonMaxInstanceCount="$(Aapt2DaemonMaxInstanceCount)"
DaemonKeepInDomain="$(_Aapt2DaemonKeepInDomain)"
OutputArchive="%(_AssetPacks.AssetPackOutput)"
AssetDirectory="$(MonoAndroidAssetPacksDirIntermediate)\%(_AssetPacks.AssetPack)\assets"
Manifest="%(_AssetPacks.ManifestFile)"
PackageName="$(_AndroidPackage).%(_AssetPacks.AssetPack)"
ToolPath="$(Aapt2ToolPath)"
ToolExe="$(Aapt2ToolExe)"
/>
<ItemGroup>
<AndroidAppBundleModules Include="%(_AssetPacks.AssetPackOutput)" />
<FileWrites Include="$%(_AssetPacks.AssetPackOutput)" />
</ItemGroup>
</Target>

</Project>
73 changes: 73 additions & 0 deletions src/Xamarin.Android.Build.Tasks/Tasks/Aapt2LinkAssetPack.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Xml;
using System.Xml.Linq;
using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using Xamarin.Android.Tools;
using Microsoft.Android.Build.Tasks;

namespace Xamarin.Android.Tasks {

public class Aapt2LinkAssetPack : Aapt2 {
public override string TaskPrefix => "A2LAP";

[Required]
public ITaskItem Manifest { get; set; }

[Required]
public ITaskItem AssetDirectory { get; set; }

[Required]
public string PackageName { get; set; }

[Required]
public ITaskItem OutputArchive { get; set; }

public string OutputFormat { get; set; } = "proto";

protected override int GetRequiredDaemonInstances ()
{
return Math.Min (1, DaemonMaxInstanceCount);
}

public async override System.Threading.Tasks.Task RunTaskAsync ()
{
RunAapt (GenerateCommandLineCommands (Manifest, AssetDirectory, OutputArchive), OutputArchive.ItemSpec);
ProcessOutput ();
if (OutputFormat == "proto" && File.Exists (OutputArchive.ItemSpec)) {
// move the manifest to the right place.
using (var zip = new ZipArchiveEx (OutputArchive.ItemSpec, File.Exists (OutputArchive.ItemSpec) ? FileMode.Open : FileMode.Create)) {
zip.MoveEntry ("AndroidManifest.xml", "manifest/AndroidManifest.xml");
zip.Archive.DeleteEntry ("resources.pb");
}
}
}

protected string[] GenerateCommandLineCommands (ITaskItem manifest, ITaskItem assetDirectory, ITaskItem output)
{
//link --manifest AndroidManifest.xml --proto-format --custom-package $(Package) -A $(AssetsDirectory) -o $(_TempOutputFile)
List<string> cmd = new List<string> ();
cmd.Add ("link");
if (MonoAndroidHelper.LogInternalExceptions)
cmd.Add ("-v");
cmd.Add ("--manifest");
cmd.Add (GetFullPath (manifest.ItemSpec));
if (OutputFormat == "proto") {
cmd.Add ("--proto-format");
}
cmd.Add ("--custom-package");
cmd.Add (PackageName);
cmd.Add ("-A");
cmd.Add (GetFullPath (assetDirectory.ItemSpec));
cmd.Add ($"-o");
cmd.Add (GetFullPath (output.ItemSpec));
return cmd.ToArray ();
}
}
}
Loading

0 comments on commit 804967b

Please sign in to comment.