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(Resource.Id.installButton);
+ // create the IAssetPackManager instance.
+ manager = AssetPackManagerFactory.GetInstance (this);
+ // Setup the State Update Listener.
+ listener = new AssetPackStateUpdateListenerWrapper ();
+ listener.StateUpdate += (s, e) => {
+ var status = e.State.Status ();
+ System.Console.WriteLine (status);
+ };
+ // Setup to install the asset pack on a button click.
+ button.Click += (s, e) => {
+ var location = assetPackManager.GetPackLocation ("assetsfeature");
+ if (location == null)
+ {
+ assetPackManager.Fetch(new string[] { "assetsfeature" });
+ }
+ };
+ }
+
+ protected override void OnResume ()
+ {
+ // Register our listener
+ manager.RegisterListener (listener.Listener);
+ base.OnResume ();
+ }
+
+ protected override void OnPause()
+ {
+ // Unregister our listener. We don't want notifications
+ // when the app is not running.
+ manager.UnregisterListener (listener.Listener);
+ base.OnPause();
+ }
+ }
+}
+```
+
+### Installing Features
+
+To use Dynamic Features you need to add a custom `Application` class which
+derives from `SplitCompatApplication`. This is so the underlying application
+will include all the support code provided by a `SplitCompatApplication`.
+
+```csharp
+using System;
+using Android.App;
+using Android.Runtime;
+using Xamarin.Google.Android.Play.Core.SplitCompat;
+
+namespace DynamicFeatureExample
+{
+ [Application]
+ public class DynamicApplication : SplitCompatApplication
+ {
+ public DynamicApplication(IntPtr handle, JniHandleOwnership jniHandle)
+ : base(handle, jniHandle)
+ {
+ }
+
+ public override void OnCreate()
+ {
+ base.OnCreate();
+ }
+ }
+}
+```
+
+In order to download a feature we need to create an instance of a class implementing
+`ISplitInstallManager`. There are two of these available. The first is for your
+live application and can be created using the `SplitInstallManagerFactory`
+The second is for local testing and should be created using the `FakeSplitInstallManagerFactory`.
+You can use a `#if DEBUG` conditional compilation block to change which one you use.
+
+```csharp
+ISplitInstallManager manager;
+#if DEBUG
+var fakeManager = FakeSplitInstallManagerFactory.Create(this);
+manager = fakeManager;
+#else
+manager = SplitInstallManagerFactory.Create (this);
+#endif
+```
+
+Once you have an instance of the `ISplitInstallManager` the process of requesting
+the install of a feature is quite straight forward. You create an instance of a
+`SplitInstallRequest` via the `SplitInstallRequest.NewBuilder` method. Call
+`builder.AddModule("foo")` to add all the modules you want to install. Finally call
+`Build()` on that `request` and pass the result `manager.StartInstall` method.
+Here is some sample code.
+
+```csharp
+var builder = SplitInstallRequest.NewBuilder ();
+builder.AddModule ("assetsfeature");
+manager.StartInstall (builder.Build ());
+```
+
+This will start the installation of the `assetsfeature`. Note that the string passed
+in here is the same value as we defined for `FeatureSplitName` earlier in our
+Feature.
+
+Most of the Google samples use the `SplitInstallStateUpdatedListener` class to
+track the progress of the installation. This is where the problems start.
+`SplitInstallStateUpdatedListener` is one of those Java Generic classes which cause
+a build error if you try to use it. So what we have done is created a wrapper class
+in the `Xamarin.Google.Android.Play.Core` version `1.10.2` Nuget Package which you
+can use instead and still get the same functionality.
+
+To use this listener first create an instance of the `SplitInstallStateUpdatedListenerWrapper`.
+You can then add an `EventHander` for the `StatusUpdate` event to keep track of the
+download and install progress.
+
+```csharp
+listener = new SplitInstallStateUpdatedListenerWrapper ();
+listener.StateUpdate += (s, e) => {
+ var state = e.State.;
+ System.Console.WriteLine (state.Status());
+};
+```
+
+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 `SplitInstallStateUpdatedListener` type.
+But we do provide a property on the `SplitInstallStateUpdatedListenerWrapper` which will give
+you access to the underlying `SplitInstallStateUpdatedListener` class. So to register you can
+use the following code.
+
+```csharp
+manager.RegisterListener (listener.Listener);
+```
+
+To Unregsiter use this code.
+
+```csharp
+manager.UnregisterListener (listener.Listener);
+```
+
+With our listener registered we can now keep track of the download and install progress of our
+feature. Here is a full sample `Activity` which shows at a basic level how to install a feature
+based on a button click.
+
+
+```csharp
+using Android.App;
+using Android.OS;
+using Android.Runtime;
+using Android.Widget;
+using Java.Lang;
+using Xamarin.Google.Android.Play.Core.SplitInstall;
+using Xamarin.Google.Android.Play.Core.SplitInstall.Model;
+using Xamarin.Google.Android.Play.Core.SplitInstall.Testing;
+using Android.Content;
+
+namespace DynamicAssetsExample
+{
+ [Activity(Label = "@string/app_name", MainLauncher = true)]
+ public class MainActivity : Activity
+ {
+ ISplitInstallManager manager;
+ SplitInstallStateUpdatedListenerWrapper 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(Resource.Id.installButton);
+ // create the ISplitInstallManager instances.
+#if DEBUG
+ var fakeManager = FakeSplitInstallManagerFactory.Create(this);
+ manager = fakeManager;
+#else
+ manager = SplitInstallManagerFactory.Create (this);
+#endif
+ // Setup the Install State Update Listener.
+ listener = new SplitInstallStateUpdatedListenerWrapper ();
+ listener.StateUpdate += (s, e) => {
+ var status = e.State.Status ();
+ System.Console.WriteLine (status);
+ };
+ // Setup to install the feature on a button click.
+ button.Click += (s, e) => {
+ var builder = SplitInstallRequest.NewBuilder ();
+ builder.AddModule ("assetsfeature");
+ var installTask = manager.StartInstall (builder.Build ());
+ };
+ }
+
+ protected override void OnResume ()
+ {
+ // Register our listener
+ manager.RegisterListener (listener.Listener);
+ base.OnResume ();
+ }
+
+ protected override void OnPause()
+ {
+ // Unregister our listener. We don't want notifications
+ // when the app is not running.
+ manager.UnregisterListener (listener.Listener);
+ base.OnPause();
+ }
+ }
+}
+```
+
+## Testing a Dynamic Feature Locally
+
+In order to test Dynamic features on a local device you need to provide some additional
+arguments to `bundletool` during the build. The `--local-testing` argument will inject
+some additional metadata into the `aab` file which will cause your `features` to be
+deployed to a holding area on the device.
+
+Then when the `FakeSplitInstallManagerFactory` manager tries to install these features
+it will get them from this holding area.
+
+If you are developing using dynamic features this is the recommended arguments you need
+
+```xml
+aab
+true
+--local-testing
+```
+
+This will force you to always use `aab` files and embed the .net assemblies into the
+package. While this is not ideal it is currently the only way to debug dynamic features.
+The `AndroidBundleToolExtraArgs` will allow you to test downloading features on demand.
+
+
diff --git a/build-tools/installers/create-installers.targets b/build-tools/installers/create-installers.targets
index db38390c930..4195b7d58e2 100644
--- a/build-tools/installers/create-installers.targets
+++ b/build-tools/installers/create-installers.targets
@@ -136,6 +136,7 @@
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Application.targets" />
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Bindings.ClassParse.targets" />
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Bindings.Core.targets" />
+ <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.DynamicFeature.targets" />
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Build.Tasks.dll" />
<_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Build.Tasks.pdb" />
<_MSBuildFiles Include="@(_LocalizationLanguages->'$(MicrosoftAndroidSdkOutDir)%(Identity)\Microsoft.Android.Build.BaseTasks.resources.dll')" />
diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Aapt2.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Aapt2.targets
index d1bfde205de..3ad8127b0c1 100644
--- a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Aapt2.targets
+++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Aapt2.targets
@@ -19,6 +19,8 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved.
+
+
0
@@ -163,6 +165,7 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved.
OutputImportDirectory="$(_AndroidLibrayProjectIntermediatePath)"
OutputFile="$(ResgenTemporaryDirectory)\resources.apk"
PackageName="$(_AndroidPackage)"
+ PackageId="$(FeaturePackageId)"
JavaPlatformJarPath="$(JavaPlatformJarPath)"
JavaDesignerOutputDirectory="$(ResgenTemporaryDirectory)"
CompiledResourceFlatFiles="@(_CompiledFlatFiles)"
@@ -224,7 +227,9 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved.
AdditionalAndroidResourcePaths="@(_LibraryResourceDirectories)"
YieldDuringToolExecution="$(YieldDuringToolExecution)"
PackageName="$(_AndroidPackage)"
+ PackageId="$(FeaturePackageId)"
JavaPlatformJarPath="$(JavaPlatformJarPath)"
+ AdditionalApksToLink="@(_AdditionalApksToLink)"
VersionCodePattern="$(AndroidVersionCodePattern)"
VersionCodeProperties="$(AndroidVersionCodeProperties)"
SupportedAbis="@(_BuildTargetAbis)"
@@ -247,4 +252,126 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved.
+
+
+
+
+
+ $(IntermediateOutputPath)AndroidManifest.xml
+ $(ProjectName)
+
+ InstallTime
+ True
+
+ Feature
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_AndroidIntermediateAapt2OutputDirectory>$(IntermediateOutputPath)aapt2output
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.DynamicFeature.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.DynamicFeature.targets
new file mode 100644
index 00000000000..7aca5e8867b
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.DynamicFeature.targets
@@ -0,0 +1,121 @@
+
+
+
+
+
+ <_BuildDynamicFeaturesInParallel>False
+
+
+
+ <_DynamicFeaetureProjectMetadata Include="$(MSBuildProjectFullPath)">
+ true
+ $(MSBuildProjectDirectory)\$(_FeaturePackage)
+ $(FeatureType)
+ <_BaseZipIntermediate>$(IntermediateOutputPath)\$(MSBuildProjectName).zip
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_BuildArguments>
+ Configuration=$(Configuration);
+ BuildingFeature=True;
+ PackageName=$(_AndroidPackage);
+ AndroidPackageFormat=aab;
+ JavaPlatformJarPath=$(JavaPlatformJarPath);
+ Aapt2ToolPath=$(Aapt2ToolPath);Aapt2ToolExe=$(Aapt2ToolExe);
+ _TargetSdkVersion=$(_TargetSdkVersion);
+ _MinSdkVersion=$(_MinSdkVersion);
+ _AndroidIncludeRuntime=False;
+ _AppPackagedResources=$(MSBuildProjectDirectory)\$(_PackagedResources).ap_;
+ _AppAssemblyDirectory=$(MSBuildProjectDirectory)\$(MonoAndroidIntermediateAssemblyDir);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_FeaturePackage>$(OutputPath)$(MSBuildProjectName).zip
+ <_FeaturePackageTemp>$(IntermediateOutputPath)$(MSBuildProjectName).zip
+
+
+
+
+ $(_AndroidPackage).$(MSBuildProjectName)
+
+
+ <_AdditionalApksToLink Include="$(_AppPackagedResources)" />
+
+ <_AppAssemblies Include="$(_AppAssemblyDirectory)*.dll" />
+
+ %(_AppAssemblies.Filename)
+
+
+
+
+
+ _SetupFeature;
+ _SetupDesignTimeBuildForBuild;
+ _GenerateDynamicFeatureManifest;
+
+
+ ;_ConvertFeaturePackage
+
+
+ $(CleanDependsOn);
+ _CleanDynamicFeatures;
+
+
+
+
+
+ <_FeatureTargets>$(BeforeBuildDynamicFeatureDependsOnTargets)
+ <_FeatureTargets Condition="'$(FeatureType)' == 'Feature' And '@(Compile.Count())' != '0'" >$(_FeatureTargets);PackageForAndroid
+ <_FeatureTargets Condition="'$(FeatureType)' == 'AssetPack' " >$(_FeatureTargets);_CreateFeaturePackageWithAapt2
+ <_FeatureTargets>$(_FeatureTargets);$(AfterBuildDynamicFeatureDependsOnTargets)
+
+
+
+
\ No newline at end of file
diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets
index 346c89aa99b..daa7197fcd5 100644
--- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets
+++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets
@@ -58,6 +58,8 @@ properties that determine build ordering.
_ResolveAssemblies;
_ResolveSatellitePaths;
_CreatePackageWorkspace;
+ _CreateBaseApk;
+ _BuildDynamicFeatures;
_LinkAssemblies;
_GenerateJavaStubs;
_ManifestMerger;
@@ -69,7 +71,6 @@ properties that determine build ordering.
_CreateApplicationSharedLibraries;
_CompileDex;
$(_AfterCompileDex);
- _CreateBaseApk;
_PrepareAssemblies;
_ResolveSatellitePaths;
_CheckApkPerAbiFlag;
diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/Aapt2Convert.cs b/src/Xamarin.Android.Build.Tasks/Tasks/Aapt2Convert.cs
new file mode 100644
index 00000000000..0ab84337b05
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/Aapt2Convert.cs
@@ -0,0 +1,64 @@
+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 Aapt2Convert : Aapt2 {
+ public override string TaskPrefix => "A2C";
+
+ [Required]
+ public ITaskItem [] Files { get; set; }
+ [Required]
+ public ITaskItem OutputArchive { get; set; }
+ public string OutputFormat { get; set; } = "binary";
+ public string ExtraArgs { get; set; }
+
+ protected override int GetRequiredDaemonInstances ()
+ {
+ return Math.Min (1, DaemonMaxInstanceCount);
+ }
+
+ public async override System.Threading.Tasks.Task RunTaskAsync ()
+ {
+ RunAapt (GenerateCommandLineCommands (Files, 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");
+ }
+ }
+ }
+
+ protected string[] GenerateCommandLineCommands (IEnumerable files, ITaskItem output)
+ {
+ List cmd = new List ();
+ cmd.Add ("convert");
+ if (!string.IsNullOrEmpty (ExtraArgs))
+ cmd.Add (ExtraArgs);
+ if (MonoAndroidHelper.LogInternalExceptions)
+ cmd.Add ("-v");
+ if (!string.IsNullOrEmpty (OutputFormat)) {
+ cmd.Add ("--output-format");
+ cmd.Add (OutputFormat);
+ }
+ cmd.Add ($"-o");
+ cmd.Add (GetFullPath (output.ItemSpec));
+ foreach (var file in files) {
+ cmd.Add (GetFullPath (file.ItemSpec));
+ }
+ return cmd.ToArray ();
+ }
+ }
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/Aapt2Link.cs b/src/Xamarin.Android.Build.Tasks/Tasks/Aapt2Link.cs
index 4dd27c19321..b8eded2d20d 100644
--- a/src/Xamarin.Android.Build.Tasks/Tasks/Aapt2Link.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/Aapt2Link.cs
@@ -28,6 +28,8 @@ public class Aapt2Link : Aapt2 {
[Required]
public string JavaPlatformJarPath { get; set; }
+ public ITaskItem [] AdditionalApksToLink { get; set; }
+
public string PackageName { get; set; }
public ITaskItem [] AdditionalResourceArchives { get; set; }
@@ -56,6 +58,8 @@ public class Aapt2Link : Aapt2 {
public string OutputFile { get; set; }
+ public bool OutputToDirectory { get; set; } = false;
+
public string JavaDesignerOutputDirectory { get; set; }
public string UncompressedFileExtensions { get; set; }
@@ -76,8 +80,12 @@ public class Aapt2Link : Aapt2 {
public bool ProtobufFormat { get; set; }
+ public bool StaticLibrary { get; set; } = false;
+
public string ProguardRuleOutput { get; set; }
+ public string PackageId { get; set; }
+
AssemblyIdentityMap assemblyMap = new AssemblyIdentityMap ();
List tempFiles = new List ();
SortedSet rulesFiles = new SortedSet ();
@@ -116,9 +124,11 @@ public async override System.Threading.Tasks.Task RunTaskAsync ()
File.Delete (tmpfile);
}
// Delete the archive on failure
- if (!aaptResult && File.Exists (currentResourceOutputFile)) {
- LogDebugMessage ($"Link did not succeed. Deleting {currentResourceOutputFile}");
- File.Delete (currentResourceOutputFile);
+ if (!aaptResult) {
+ if (File.Exists (currentResourceOutputFile)) {
+ LogDebugMessage ($"Link did not succeed. Deleting {currentResourceOutputFile}");
+ File.Delete (currentResourceOutputFile);
+ }
}
}
}
@@ -256,6 +266,15 @@ string [] GenerateCommandLineCommands (string ManifestFile, string currentAbi, s
cmd.Add ("-I");
cmd.Add (GetFullPath (JavaPlatformJarPath));
+ if (AdditionalApksToLink != null) {
+ foreach (ITaskItem link in AdditionalApksToLink) {
+ if (!File.Exists (link.ItemSpec))
+ continue;
+ cmd.Add ("-I");
+ cmd.Add (link.ItemSpec);
+ }
+ }
+
if (!string.IsNullOrEmpty (ResourceSymbolsTextFile)) {
cmd.Add ("--output-text-symbols");
cmd.Add (GetFullPath (resourceSymbolsTextFileTemp));
@@ -264,6 +283,15 @@ string [] GenerateCommandLineCommands (string ManifestFile, string currentAbi, s
if (ProtobufFormat)
cmd.Add ("--proto-format");
+ if (StaticLibrary)
+ cmd.Add ("--static-lib");
+
+ if (!string.IsNullOrEmpty (PackageId)) {
+ cmd.Add ("--allow-reserved-package-id");
+ cmd.Add ("--package-id");
+ cmd.Add (PackageId);
+ }
+
if (!string.IsNullOrWhiteSpace (ExtraArgs)) {
foreach (Match match in exraArgSplitRegEx.Matches (ExtraArgs)) {
string value = match.Value.Trim (' ', '"', '\'');
@@ -302,6 +330,8 @@ string [] GenerateCommandLineCommands (string ManifestFile, string currentAbi, s
cmd.Add ("--proguard");
cmd.Add (GetFullPath (GetManifestRulesFile (manifestDir)));
}
+ if (OutputToDirectory)
+ cmd.Add ("--output-to-dir");
cmd.Add ("-o");
cmd.Add (GetFullPath (currentResourceOutputFile));
@@ -310,8 +340,15 @@ string [] GenerateCommandLineCommands (string ManifestFile, string currentAbi, s
bool ExecuteForAbi (string [] cmd, string currentResourceOutputFile)
{
+ string outputFile = currentResourceOutputFile;
+ if (OutputToDirectory) {
+ if (ProtobufFormat)
+ outputFile = Path.Combine (currentResourceOutputFile, "manifest", "AndroidManifest.xml");
+ else
+ outputFile = Path.Combine (currentResourceOutputFile, "AndroidManifest.xml");
+ }
lock (apks)
- apks.Add (currentResourceOutputFile, RunAapt (cmd, currentResourceOutputFile));
+ apks.Add (outputFile, RunAapt (cmd, outputFile));
return true;
}
diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/BuildApk.cs b/src/Xamarin.Android.Build.Tasks/Tasks/BuildApk.cs
index fbcaa1e0944..fe8b9b71ef3 100644
--- a/src/Xamarin.Android.Build.Tasks/Tasks/BuildApk.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/BuildApk.cs
@@ -89,6 +89,8 @@ public class BuildApk : AndroidTask
public bool IncludeWrapSh { get; set; }
+ public bool IncludeRuntime { get; set; } = true;
+
public string CheckedBuild { get; set; }
public string RuntimeConfigBinFilePath { get; set; }
@@ -201,7 +203,8 @@ void ExecuteWithAbi (string [] supportedAbis, string apkInputPath, string apkOut
apk.Flush ();
}
- AddRuntimeLibraries (apk, supportedAbis);
+ if (IncludeRuntime)
+ AddRuntimeLibraries (apk, supportedAbis);
apk.Flush();
AddNativeLibraries (files, supportedAbis);
AddAdditionalNativeLibraries (files, supportedAbis);
@@ -673,14 +676,16 @@ string GetArchiveFileName (ITaskItem item)
private void AddNativeLibraries (ArchiveFileList files, string [] supportedAbis)
{
- var frameworkLibs = FrameworkNativeLibraries.Select (v => new LibInfo {
- Path = v.ItemSpec,
- Link = v.GetMetadata ("Link"),
- Abi = GetNativeLibraryAbi (v),
- ArchiveFileName = GetArchiveFileName (v)
- });
-
- AddNativeLibraries (files, supportedAbis, frameworkLibs);
+ if (IncludeRuntime) {
+ var frameworkLibs = FrameworkNativeLibraries.Select (v => new LibInfo {
+ Path = v.ItemSpec,
+ Link = v.GetMetadata ("Link"),
+ Abi = GetNativeLibraryAbi (v),
+ ArchiveFileName = GetArchiveFileName (v)
+ });
+
+ AddNativeLibraries (files, supportedAbis, frameworkLibs);
+ }
var libs = NativeLibraries.Concat (BundleNativeLibraries ?? Enumerable.Empty ())
.Where (v => IncludeNativeLibrary (v))
diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/BuildBaseAppBundle.cs b/src/Xamarin.Android.Build.Tasks/Tasks/BuildBaseAppBundle.cs
index 400a58ee43d..84fde560b7a 100644
--- a/src/Xamarin.Android.Build.Tasks/Tasks/BuildBaseAppBundle.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/BuildBaseAppBundle.cs
@@ -33,19 +33,11 @@ public class BuildBaseAppBundle : BuildApk
///
protected override void FixupArchive (ZipArchiveEx zip)
{
- if (!zip.Archive.ContainsEntry ("AndroidManifest.xml")) {
+ if (!zip.MoveEntry ("AndroidManifest.xml", "manifest/AndroidManifest.xml")) {
Log.LogDebugMessage ($"No AndroidManifest.xml. Skipping Fixup");
return;
}
- var entry = zip.Archive.ReadEntry ("AndroidManifest.xml");
- Log.LogDebugMessage ($"Fixing up AndroidManifest.xml to be manifest/AndroidManifest.xml.");
- using (var stream = new MemoryStream ()) {
- entry.Extract (stream);
- stream.Position = 0;
- zip.Archive.AddEntry ("manifest/AndroidManifest.xml", stream);
- zip.Archive.DeleteEntry (entry);
- zip.Flush ();
- }
+ Log.LogDebugMessage ($"Fixed up AndroidManifest.xml to be manifest/AndroidManifest.xml.");
}
}
}
diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/CalculatePackageIdsForFeatures.cs b/src/Xamarin.Android.Build.Tasks/Tasks/CalculatePackageIdsForFeatures.cs
new file mode 100644
index 00000000000..a24bee175c7
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/CalculatePackageIdsForFeatures.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Xml;
+using System.Xml.XPath;
+
+using Microsoft.Build.Utilities;
+using Microsoft.Build.Framework;
+
+using Xamarin.Build;
+using Microsoft.Android.Build.Tasks;
+
+namespace Xamarin.Android.Tasks
+{
+ public class CalculatePackageIdsForFeatures : AndroidTask
+ {
+ public override string TaskPrefix => "CPI";
+
+ [Required]
+ public ITaskItem[] FeatureProjects { get; set; }
+
+ [Output]
+ public ITaskItem[] Output { get; set; }
+
+ public override bool RunTask ()
+ {
+ List output = new List ();
+ int packageId = 0x7f; // default package Id for the main app.
+ // we now decrement the package id and will use that for
+ // each "feature". This is so resources do not clash.
+ foreach (var feature in FeatureProjects) {
+ packageId--;
+ var item = new TaskItem (feature.ItemSpec);
+ bool isFeature = feature.GetMetadata("FeatureType") == "Feature";
+ string apkFileIntermediate = feature.GetMetadata ("_BaseZipIntermediate");
+ item.SetMetadata ("AdditionalProperties", $"AndroidApplication={isFeature.ToString ()};_BaseZipIntermediate={apkFileIntermediate};EmbedAssembliesIntoApk=true;FeaturePackageId=0x{packageId.ToString ("X")}");
+ output.Add (item);
+ }
+ Output = output.ToArray ();
+ return !Log.HasLoggedErrors;
+ }
+ }
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/CreateDynamicFeatureManifest.cs b/src/Xamarin.Android.Build.Tasks/Tasks/CreateDynamicFeatureManifest.cs
new file mode 100644
index 00000000000..7365415ed52
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/CreateDynamicFeatureManifest.cs
@@ -0,0 +1,137 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Xml;
+using System.Xml.Linq;
+using Microsoft.Build.Utilities;
+using Microsoft.Build.Framework;
+
+using Xamarin.Android.Tools;
+using Microsoft.Android.Build.Tasks;
+
+namespace Xamarin.Android.Tasks
+{
+ public class CreateDynamicFeatureManifest : AndroidTask
+ {
+ readonly XNamespace androidNS = "http://schemas.android.com/apk/res/android";
+ readonly XNamespace distNS = "http://schemas.android.com/apk/distribution";
+ readonly XNamespace toolsNS = "http://schemas.android.com/tools";
+
+ public override string TaskPrefix => "CDFM";
+
+ [Required]
+ public string FeatureSplitName { get; set; }
+
+ [Required]
+ public string FeatureDeliveryType { get; set; }
+
+ [Required]
+ public string FeatureType { get; set; }
+
+ [Required]
+ public string PackageName { get; set; }
+
+ [Required]
+ public ITaskItem OutputFile { get; set; }
+
+ public string FeatureTitleResource { get; set; }
+
+ public string MinSdkVersion { get; set; }
+
+ public string TargetSdkVersion { get; set; }
+
+ public bool IsFeatureSplit { get; set; } = false;
+ public bool IsInstant { get; set; } = false;
+ public bool HasCode { get; set; } = false;
+
+ public override bool RunTask ()
+ {
+ XDocument doc = new XDocument ();
+ switch (FeatureType) {
+ case "AssetPack":
+ GenerateAssetPackManifest (doc);
+ break;
+ default:
+ GenerateFeatureManifest (doc);
+ break;
+ }
+ doc.Save (OutputFile.ItemSpec);
+ return !Log.HasLoggedErrors;
+ }
+
+ void GenerateFeatureManifest (XDocument doc) {
+ XAttribute featureTitleResource = null;
+ if (!string.IsNullOrEmpty (FeatureTitleResource))
+ featureTitleResource = new XAttribute (distNS + "title", FeatureTitleResource);
+ XElement usesSdk = new XElement ("uses-sdk");
+ if (!string.IsNullOrEmpty (MinSdkVersion))
+ usesSdk.Add (new XAttribute (androidNS + "minSdkVersion", MinSdkVersion));
+ if (!string.IsNullOrEmpty (MinSdkVersion))
+ usesSdk.Add (new XAttribute (androidNS + "targetSdkVersion", TargetSdkVersion));
+ doc.Add (new XElement ("manifest",
+ new XAttribute(XNamespace.Xmlns + "android", androidNS),
+ new XAttribute(XNamespace.Xmlns + "tools", toolsNS),
+ new XAttribute(XNamespace.Xmlns + "dist", distNS),
+ new XAttribute (androidNS + "versionCode", "1"),
+ new XAttribute (androidNS + "versionName", "1.0"),
+ new XAttribute ("package", PackageName),
+ new XAttribute ("featureSplit", FeatureSplitName),
+ new XAttribute (androidNS + "isFeatureSplit", IsFeatureSplit),
+ new XElement (distNS + "module",
+ featureTitleResource,
+ new XAttribute (distNS + "instant", IsInstant),
+ new XElement (distNS + "delivery",
+ GetDistribution ()
+ ),
+ new XElement (distNS + "fusing",
+ new XAttribute (distNS + "include", true)
+ )
+ ),
+ usesSdk,
+ new XElement ("application",
+ new XAttribute (androidNS + "hasCode", HasCode),
+ new XAttribute (toolsNS + "replace", "android:hasCode")
+ )
+ )
+ );
+ }
+
+ void GenerateAssetPackManifest (XDocument doc) {
+ doc.Add (new XElement ("manifest",
+ new XAttribute (XNamespace.Xmlns + "android", androidNS),
+ new XAttribute (XNamespace.Xmlns + "tools", toolsNS),
+ new XAttribute (XNamespace.Xmlns + "dist", distNS),
+ new XAttribute ("package", PackageName),
+ new XAttribute ("split", FeatureSplitName),
+ new XElement (distNS + "module",
+ new XAttribute (distNS + "type", "asset-pack"),
+ new XElement (distNS + "delivery",
+ GetDistribution ()
+ ),
+ new XElement (distNS + "fusing",
+ new XAttribute (distNS + "include", true)
+ )
+ )
+ )
+ );
+ }
+
+ XElement GetDistribution ()
+ {
+ XElement distribution;
+ switch (FeatureDeliveryType)
+ {
+ case "OnDemand":
+ distribution = new XElement (distNS + "on-demand");
+ break;
+ case "InstallTime":
+ default:
+ distribution = new XElement (distNS + "install-time");
+ break;
+ }
+ return distribution;
+ }
+ }
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/InstallApkSet.cs b/src/Xamarin.Android.Build.Tasks/Tasks/InstallApkSet.cs
index 2fa52781738..469d51f0420 100644
--- a/src/Xamarin.Android.Build.Tasks/Tasks/InstallApkSet.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/InstallApkSet.cs
@@ -35,9 +35,7 @@ internal override CommandLineBuilder GetCommandLineBuilder ()
// --modules: List of modules to be installed, or "_ALL_" for all modules.
// Defaults to modules installed during first install, i.e. not on-demand.
// Xamarin.Android won't support on-demand modules yet.
- if (Modules == null)
- cmd.AppendSwitchIfNotNull ("--modules ", "_ALL_");
- else
+ if (Modules != null)
cmd.AppendSwitchIfNotNull ("--modules ", $"\"{string.Join ("\",\"", Modules)}\"");
return cmd;
diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/ReadAndroidManifest.cs b/src/Xamarin.Android.Build.Tasks/Tasks/ReadAndroidManifest.cs
index c8143247d22..ce2031db9bb 100644
--- a/src/Xamarin.Android.Build.Tasks/Tasks/ReadAndroidManifest.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tasks/ReadAndroidManifest.cs
@@ -38,11 +38,18 @@ public class ReadAndroidManifest : AndroidTask
[Output]
public bool IsTestOnly { get; set; } = false;
+ [Output]
+ public string MinSdkVersion { get; set; }
+
+ [Output]
+ public string TargetSdkVersion { get; set; }
+
public override bool RunTask ()
{
var androidNs = AndroidAppManifest.AndroidXNamespace;
var manifest = AndroidAppManifest.Load (ManifestFile, MonoAndroidHelper.SupportedVersions);
var app = manifest.Document.Element ("manifest")?.Element ("application");
+ var usesSdk = manifest.Document.Element ("manifest")?.Element ("uses-sdk");
if (app != null) {
string text = app.Attribute (androidNs + "extractNativeLibs")?.Value;
@@ -74,6 +81,10 @@ public override bool RunTask ()
}
UsesLibraries = libraries.ToArray ();
}
+ if (usesSdk != null) {
+ MinSdkVersion = usesSdk.Attribute (androidNs + "minSdkVersion")?.Value;
+ TargetSdkVersion = usesSdk.Attribute (androidNs + "targetSdkVersion")?.Value;
+ }
return !Log.HasLoggedErrors;
}
diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/DynamicFeatureTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/DynamicFeatureTests.cs
new file mode 100644
index 00000000000..7adcf177780
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/DynamicFeatureTests.cs
@@ -0,0 +1,162 @@
+using NUnit.Framework;
+using System.IO;
+using System.Text;
+using Xamarin.ProjectTools;
+
+namespace Xamarin.Android.Build.Tests
+{
+ [Category ("Node-3")]
+ [Parallelizable (ParallelScope.Children)]
+ public class DynamicFeatureTests : BaseTest
+ {
+ [Test]
+ [Category ("SmokeTests")]
+ public void BuildDynamicAssetFeature ([Values (true, false)] bool isRelease) {
+
+ if (!Builder.UseDotNet)
+ Assert.Ignore ("Dynamic Features not supported on Legacy Projects.");
+ var path = Path.Combine ("temp", TestName);
+ var feature1 = new XamarinAndroidLibraryProject () {
+ ProjectName = "Feature1",
+ IsRelease = isRelease,
+ OtherBuildItems = {
+ new AndroidItem.AndroidAsset ("Assets\\asset3.txt") {
+ TextContent = () => "Asset3",
+ Encoding = Encoding.ASCII,
+ },
+ }
+ };
+ // we don't need any of this stuff!
+ feature1.Sources.Clear ();
+ feature1.AndroidResources.Clear ();
+ feature1.SetProperty ("FeatureType", "AssetPack");
+ var app = new XamarinAndroidApplicationProject {
+ ProjectName = "MyApp",
+ IsRelease = isRelease,
+ };
+ app.SetProperty ("AndroidPackageFormat", "aab");
+ var reference = new BuildItem ("ProjectReference", $"..\\{feature1.ProjectName}\\{feature1.ProjectName}.csproj");
+ app.References.Add (reference);
+ using (var libBuilder = CreateDllBuilder (Path.Combine (path, feature1.ProjectName))) {
+ libBuilder.Save (feature1);
+ using (var appBuilder = CreateApkBuilder (Path.Combine (path, app.ProjectName))) {
+ Assert.IsTrue (appBuilder.Build (app), $"{app.ProjectName} should succeed");
+ // Check the final aab has the required feature files in it.
+ var aab = Path.Combine (Root, appBuilder.ProjectDirectory,
+ app.OutputPath, $"{app.PackageName}.aab");
+ using (var zip = ZipHelper.OpenZip (aab)) {
+ Assert.IsTrue (zip.ContainsEntry ("feature1/assets/asset3.txt"), "aab should contain feature1/assets/asset3.txt");
+ Assert.IsTrue (zip.ContainsEntry ("feature1/assets.pb"), "aab should contain feature1/assets.pb");
+ Assert.IsFalse (zip.ContainsEntry ("feature1/resources.pb"), "aab should not contain feature1/resources.pb");
+ }
+ }
+ }
+ }
+
+ [Test]
+ [Category ("SmokeTests")]
+ public void BuildDynamicActivityFeature ([Values (true, false)] bool isRelease) {
+
+ if (!Builder.UseDotNet)
+ Assert.Ignore ("Dynamic Features not supported on Legacy Projects.");
+ var path = Path.Combine ("temp", TestName);
+ var assetFeature = new XamarinAndroidLibraryProject () {
+ ProjectName = "AssetFeature",
+ IsRelease = isRelease,
+ OtherBuildItems = {
+ new AndroidItem.AndroidAsset ("Assets\\asset3.txt") {
+ TextContent = () => "Asset3",
+ Encoding = Encoding.ASCII,
+ },
+ }
+ };
+ // we don't need any of this stuff!
+ assetFeature.Sources.Clear ();
+ assetFeature.AndroidResources.Clear ();
+ assetFeature.SetProperty ("FeatureType", "AssetPack");
+
+ var feature1 = new XamarinAndroidLibraryProject () {
+ ProjectName = "Feature1",
+ IsRelease = isRelease,
+ };
+ feature1.Sources.Clear ();
+ feature1.AndroidResources.Clear ();
+ // Add an activity which the main app can call.
+ feature1.Sources.Add (new BuildItem.Source ("FeatureActivity.cs") {
+ TextContent = () => @"using System;
+using Android.App;
+using Android.Content;
+using Android.Runtime;
+using Android.Views;
+using Android.Widget;
+using Android.OS;
+
+namespace Feature1
+{
+ [Register (""" + feature1.ProjectName + @".FeatureActivity""), Activity (Label = ""Feature1"", MainLauncher = false, Icon = ""@drawable/icon"")]
+ public class FeatureActivity : Activity {
+ protected override void OnCreate (Bundle bundle)
+ {
+ base.OnCreate (bundle);
+ SetContentView (Resource.Layout.FeatureActivity);
+ }
+ }
+}
+",
+ });
+ feature1.AndroidResources.Add (new AndroidItem.AndroidResource ("Resources\\Layout\\FeatureActivity.xml") {
+ TextContent = () => @"
+
+
+
+",
+ });
+ feature1.SetProperty ("FeatureType", "Feature");
+ feature1.SetProperty ("FeatureTitleResource", "@string/feature1");
+ var app = new XamarinAndroidApplicationProject {
+ ProjectName = "MyApp",
+ IsRelease = isRelease,
+ OtherBuildItems = {
+ new AndroidItem.AndroidResource (() => "Resources\\values\\string1.xml") {
+ TextContent = () => @"
+ Feature1
+ ",
+ },
+ }
+ };
+ app.SetProperty ("AndroidPackageFormat", "aab");
+ var reference = new BuildItem ("ProjectReference", $"..\\{assetFeature.ProjectName}\\{assetFeature.ProjectName}.csproj");
+ app.References.Add (reference);
+ reference = new BuildItem ("ProjectReference", $"..\\{feature1.ProjectName}\\{feature1.ProjectName}.csproj");
+ app.References.Add (reference);
+ using (var libBuilder = CreateDllBuilder (Path.Combine (path, assetFeature.ProjectName))) {
+ libBuilder.Save (assetFeature);
+ using (var libBuilder1 = CreateDllBuilder (Path.Combine (path, feature1.ProjectName))) {
+ libBuilder1.Save (feature1);
+ using (var appBuilder = CreateApkBuilder (Path.Combine (path, app.ProjectName))) {
+ Assert.IsTrue (appBuilder.Build (app), $"{app.ProjectName} should succeed");
+ // Check the final aab has the required feature files in it.
+ var aab = Path.Combine (Root, appBuilder.ProjectDirectory,
+ app.OutputPath, $"{app.PackageName}.aab");
+ using (var zip = ZipHelper.OpenZip (aab)) {
+ Assert.IsTrue (zip.ContainsEntry ($"feature1/root/assemblies/{feature1.ProjectName}.dll"), $"aab should contain feature1/root/assemblies/{feature1.ProjectName}.dll");
+ Assert.IsFalse (zip.ContainsEntry ("feature1/root/assemblies/System.dll"), "aab should not contain feature1/root/assemblies/System.dll");
+ Assert.IsFalse (zip.ContainsEntry ("feature1/assets.pb"), "aab should contain feature1/assets.pb");
+ Assert.IsTrue (zip.ContainsEntry ("feature1/resources.pb"), "aab should contain feature1/resources.pb");
+ }
+ }
+ }
+ }
+ Assert.Fail ();
+ }
+ }
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/CreateDynamicFeatureManifestTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/CreateDynamicFeatureManifestTests.cs
new file mode 100644
index 00000000000..952b0272a9a
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/CreateDynamicFeatureManifestTests.cs
@@ -0,0 +1,69 @@
+using NUnit.Framework;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Xml;
+using System.Xml.Linq;
+using System.Xml.XPath;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Xamarin.Android.Tasks;
+using Xamarin.Tools.Zip;
+using TaskItem = Microsoft.Build.Utilities.TaskItem;
+
+namespace Xamarin.Android.Build.Tests
+{
+ [TestFixture]
+ [Category ("Node-2")]
+ public class CreateDynamicFeatureManifestTests : BaseTest
+ {
+ [Test]
+ [TestCase ("OnDemand", "", "")]
+ [TestCase ("OnDemand", "21", "")]
+ [TestCase ("OnDemand", "21", "30")]
+ [TestCase ("InstallTime", "", "")]
+ [TestCase ("InstallTime", "21", "")]
+ [TestCase ("InstallTime", "21", "30")]
+ [TestCase ("", "", "")]
+ public void CreateDynamicFeatureManifestIsCreated (string deliveryType, string minSdk, string targetSdk)
+ {
+ string path = Path.Combine (Root, "temp", TestName);
+ Directory.CreateDirectory(path);
+ string manifest = Path.Combine(path, "AndroidManifest.xml");
+ var task = new CreateDynamicFeatureManifest {
+ BuildEngine = new MockBuildEngine (TestContext.Out),
+ FeatureDeliveryType = deliveryType,
+ FeatureSplitName = "MyFeature",
+ FeatureTitleResource = "@strings/myfeature",
+ PackageName = "com.feature.test",
+ OutputFile = new TaskItem (manifest),
+ MinSdkVersion = minSdk,
+ TargetSdkVersion = targetSdk,
+ };
+ Assert.IsTrue (task.Execute (), "task.Execute() should have succeeded.");
+ XDocument doc = XDocument.Load (manifest);
+ var nsResolver = new XmlNamespaceManager (new NameTable ());
+ nsResolver.AddNamespace ("android", "http://schemas.android.com/apk/res/android");
+ nsResolver.AddNamespace ("dist", "http://schemas.android.com/apk/distribution");
+ bool hasMinSdk = !string.IsNullOrEmpty (minSdk);
+ Assert.AreEqual (hasMinSdk, doc.XPathSelectElements ($"//manifest/uses-sdk[@android:minSdkVersion='{minSdk}']", nsResolver).Any (),
+ $"minSdkVersion should {(hasMinSdk ? "" : "not")} be set.");
+ bool hasTargetSdk = !string.IsNullOrEmpty (targetSdk);
+ Assert.AreEqual (hasMinSdk, doc.XPathSelectElements ($"//manifest/uses-sdk[@android:targetSdkVersion='{targetSdk}']", nsResolver).Any (),
+ $"minSdkVersion should {(hasTargetSdk ? "" : "not")} be set.");
+ string expected = "";
+ switch (deliveryType) {
+ case "OnDemand":
+ expected = "dist:on-demand";
+ break;
+ case "InstallTime":
+ default:
+ expected = "dist:install-time";
+ break;
+ }
+ Assert.IsTrue (doc.XPathSelectElements ($"//manifest/dist:module/dist:delivery/{expected}", nsResolver).Any (), $"Delivery type should be set to {expected}");
+ Directory.Delete (path, recursive: true);
+ }
+ }
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs
index 8a1b8996794..6b24e55ad85 100644
--- a/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs
@@ -31,11 +31,13 @@ internal class ManifestDocument
{
public static XNamespace AndroidXmlNamespace = "http://schemas.android.com/apk/res/android";
public static XNamespace AndroidXmlToolsNamespace = "http://schemas.android.com/tools";
+ public static XNamespace AndroidXmlDistNamespace = "http://schemas.android.com/apk/distribution";
const int maxVersionCode = 2100000000;
static XNamespace androidNs = AndroidXmlNamespace;
static XNamespace androidToolsNs = AndroidXmlToolsNamespace;
+ static XNamespace androidDistNs = AndroidXmlDistNamespace;
static readonly XName versionCodeAttributeName = androidNs + "versionCode";
XDocument doc;
@@ -111,7 +113,8 @@ public string VersionCode {
return "1";
}
set {
- doc.Root.SetAttributeValue (versionCodeAttributeName, versionCode = value);
+ if (!IsAssetPack())
+ doc.Root.SetAttributeValue (versionCodeAttributeName, versionCode = value);
}
}
@@ -143,6 +146,12 @@ public string GetTargetSdk ()
return targetAttr.Value;
}
+ public bool IsAssetPack ()
+ {
+ var type = doc.Root.Element (androidDistNs + "module")?.Attribute (androidDistNs + "type");
+ return type?.Value == "asset-pack";
+ }
+
public ManifestDocument (string templateFilename) : base ()
{
Assemblies = new List ();
@@ -1047,6 +1056,8 @@ internal static string ReplacePlaceholders (string [] placeholders, string text,
public void SetAbi (string abi)
{
+ if (IsAssetPack())
+ return;
int code = 1;
if (!string.IsNullOrEmpty (VersionCode)) {
code = Convert.ToInt32 (VersionCode);
@@ -1061,6 +1072,9 @@ public bool ValidateVersionCode (out string error, out string errorCode)
{
int code;
error = errorCode = string.Empty;
+ if (IsAssetPack()) {
+ return true;
+ }
if (!int.TryParse (VersionCode, out code)) {
error = string.Format (Properties.Resources.XA0003, VersionCode);
errorCode = "XA0003";
@@ -1076,6 +1090,8 @@ public bool ValidateVersionCode (out string error, out string errorCode)
public void CalculateVersionCode (string currentAbi, string versionCodePattern, string versionCodeProperties)
{
+ if (IsAssetPack())
+ return;
var regex = new Regex ("\\{(?([A-Za-z]+)):?[D0-9]*[\\}]");
var kvp = new Dictionary ();
foreach (var item in versionCodeProperties?.Split (new char [] { ';', ':' }) ?? Array.Empty ()) {
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveEx.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveEx.cs
index 90a8371aad7..54e510218ad 100644
--- a/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveEx.cs
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/ZipArchiveEx.cs
@@ -124,6 +124,22 @@ public void RemoveFile (string folder, string file)
zip.DeleteEntry ((ulong)index);
}
+ public bool MoveEntry (string from, string to)
+ {
+ if (!zip.ContainsEntry (from)) {
+ return false;
+ }
+ var entry = zip.ReadEntry (from);
+ using (var stream = new MemoryStream ()) {
+ entry.Extract (stream);
+ stream.Position = 0;
+ zip.AddEntry (to, stream);
+ zip.DeleteEntry (entry);
+ Flush ();
+ }
+ return true;
+ }
+
public void AddDirectory (string folder, string folderInArchive, CompressionMethod method = CompressionMethod.Default)
{
if (!string.IsNullOrEmpty (folder)) {
diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.targets
index fbd0fd842c7..406adec8fb0 100644
--- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.targets
+++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.targets
@@ -109,6 +109,10 @@
Designer
PreserveNewest
+
+ PreserveNewest
+ Xamarin.Android.DynamicFeature.targets
+
Designer
PreserveNewest
diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets
index 330b30f9a7e..7b807d35051 100644
--- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets
+++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets
@@ -298,6 +298,7 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved.
False
False
+ <_AndroidIncludeRuntime Condition=" '$(_AndroidIncludeRuntime)' == '' ">True
<_AndroidEnablePreloadAssembliesDefault Condition=" '$(UsingAndroidNETSdk)' == 'true' ">False
<_AndroidEnablePreloadAssembliesDefault Condition=" '$(UsingAndroidNETSdk)' != 'true' ">True
@@ -377,6 +378,7 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved.
+
@@ -604,8 +606,8 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved.
- $(IntermediateOutputPath)android\bin\$(_AndroidPackage).apk
- <_BaseZipIntermediate>$(IntermediateOutputPath)android\bin\base.zip
+ $(IntermediateOutputPath)android\bin\$(_AndroidPackage).apk
+ <_BaseZipIntermediate Condition=" '$(_BaseZipIntermediate)' == '' ">$(IntermediateOutputPath)android\bin\base.zip
<_AppBundleIntermediate>$(IntermediateOutputPath)android\bin\$(_AndroidPackage).aab
<_ApkSetIntermediate>$(IntermediateOutputPath)android\bin\$(_AndroidPackage).apks
<_UniversalApkSetIntermediate>$(IntermediateOutputPath)android\bin\$(_AndroidPackage)-Universal.apks
@@ -1605,6 +1607,8 @@ because xbuild doesn't support framework reference assemblies.
+
+
.so;$(AndroidStoreUncompressedFileExtensions)
@@ -2136,9 +2140,14 @@ because xbuild doesn't support framework reference assemblies.
CheckedBuild="$(_AndroidCheckedBuild)"
RuntimeConfigBinFilePath="$(_BinaryRuntimeConfigPath)"
ExcludeFiles="@(AndroidPackagingOptionsExclude)"
+<<<<<<< HEAD
ZipFlushFilesLimit="$(_ZipFlushFilesLimit)"
ZipFlushSizeLimit="$(_ZipFlushSizeLimit)"
UseAssemblyStore="$(AndroidUseAssemblyStore)">
+=======
+ UseAssemblyStore="$(AndroidUseAssemblyStore)"
+ IncludeRuntime="$(_AndroidIncludeRuntime)">
+>>>>>>> 10b353371 ([Xamarin.Andorid.Build.Tasks] First Pass on Dynamic Asset Features)
+ UseAssemblyStore="$(AndroidUseAssemblyStore)"
+ IncludeRuntime="$(_AndroidIncludeRuntime)">