diff --git a/build-tools/manifest-attribute-codegen/manifest-definition.xml b/build-tools/manifest-attribute-codegen/manifest-definition.xml
index 085709f4e28..ef209cd9b45 100644
--- a/build-tools/manifest-attribute-codegen/manifest-definition.xml
+++ b/build-tools/manifest-attribute-codegen/manifest-definition.xml
@@ -95,6 +95,7 @@
+
manifest
@@ -219,6 +220,7 @@
+
provider
@@ -263,6 +265,7 @@
+
application
@@ -350,6 +353,7 @@
+
application
@@ -416,6 +420,16 @@
+
+
+
+
+
+
+
+
+
+
intent-filter
@@ -580,6 +594,7 @@
+
processes
@@ -620,6 +635,7 @@
+
application
@@ -636,4 +652,8 @@
install-constraints
+
+ intent-filter
+
+
diff --git a/build-tools/manifest-attribute-codegen/metadata.xml b/build-tools/manifest-attribute-codegen/metadata.xml
index 4f0d7d3d4ad..fab099f92ab 100644
--- a/build-tools/manifest-attribute-codegen/metadata.xml
+++ b/build-tools/manifest-attribute-codegen/metadata.xml
@@ -16,6 +16,7 @@
+
@@ -49,7 +50,6 @@
-
@@ -65,6 +65,7 @@
+
@@ -121,6 +122,7 @@
+
@@ -147,6 +149,7 @@
+
@@ -303,6 +306,11 @@
+
+
+
+
+
@@ -326,6 +334,7 @@
+
@@ -366,6 +375,7 @@
+
diff --git a/src/Mono.Android/Android.App/PropertyAttribute.Partial.cs b/src/Mono.Android/Android.App/PropertyAttribute.Partial.cs
new file mode 100644
index 00000000000..ccdcc7112c0
--- /dev/null
+++ b/src/Mono.Android/Android.App/PropertyAttribute.Partial.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Android.App;
+
+public sealed partial class PropertyAttribute
+{
+ public PropertyAttribute (string name)
+ {
+ Name = name;
+ }
+}
diff --git a/src/Mono.Android/Android.App/PropertyAttribute.cs b/src/Mono.Android/Android.App/PropertyAttribute.cs
new file mode 100644
index 00000000000..7f56f00c651
--- /dev/null
+++ b/src/Mono.Android/Android.App/PropertyAttribute.cs
@@ -0,0 +1,54 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by 'manifest-attribute-codegen'.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+using System;
+
+namespace Android.App;
+
+[Serializable]
+[AttributeUsage (AttributeTargets.Assembly | AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+public sealed partial class PropertyAttribute : Attribute {
+ public string Name { get; private set; }
+
+ public string? Resource { get; set; }
+
+ public string? Value { get; set; }
+
+#if XABT_MANIFEST_EXTENSIONS
+ static Xamarin.Android.Manifest.ManifestDocumentElement mapping = new ("property");
+
+ static PropertyAttribute ()
+ {
+ mapping.Add (
+ member: "Name",
+ attributeName: "name",
+ getter: self => self.Name,
+ setter: null
+ );
+ mapping.Add (
+ member: "Resource",
+ attributeName: "resource",
+ getter: self => self.Resource,
+ setter: (self, value) => self.Resource = (string?) value
+ );
+ mapping.Add (
+ member: "Value",
+ attributeName: "value",
+ getter: self => self.Value,
+ setter: (self, value) => self.Value = (string?) value
+ );
+
+ AddManualMapping ();
+ }
+
+ static partial void AddManualMapping ();
+#endif // XABT_MANIFEST_EXTENSIONS
+}
diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj
index e83fdb5fefa..b9399979b79 100644
--- a/src/Mono.Android/Mono.Android.csproj
+++ b/src/Mono.Android/Mono.Android.csproj
@@ -81,6 +81,7 @@
+
@@ -152,6 +153,7 @@
+
diff --git a/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt
index a31234ea301..266193a8b8c 100644
--- a/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt
+++ b/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt
@@ -728,6 +728,13 @@ Android.App.Notification.TvExtender.TvExtender(Android.App.Notification! notif)
Android.App.NotificationChannel.VibrationEffect.get -> Android.OS.VibrationEffect?
Android.App.NotificationChannel.VibrationEffect.set -> void
Android.App.PictureInPictureUiState.IsTransitioningToPip.get -> bool
+Android.App.PropertyAttribute
+Android.App.PropertyAttribute.Name.get -> string!
+Android.App.PropertyAttribute.PropertyAttribute(string! name) -> void
+Android.App.PropertyAttribute.Resource.get -> string?
+Android.App.PropertyAttribute.Resource.set -> void
+Android.App.PropertyAttribute.Value.get -> string?
+Android.App.PropertyAttribute.Value.set -> void
Android.App.SdkSandbox.AppOwnedSdkSandboxInterface
Android.App.SdkSandbox.AppOwnedSdkSandboxInterface.AppOwnedSdkSandboxInterface(string! name, long version, Android.OS.IBinder! binder) -> void
Android.App.SdkSandbox.AppOwnedSdkSandboxInterface.DescribeContents() -> int
diff --git a/src/Xamarin.Android.Build.Tasks/Mono.Android/PropertyAttribute.Partial.cs b/src/Xamarin.Android.Build.Tasks/Mono.Android/PropertyAttribute.Partial.cs
new file mode 100644
index 00000000000..f6b59eda257
--- /dev/null
+++ b/src/Xamarin.Android.Build.Tasks/Mono.Android/PropertyAttribute.Partial.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Xml.Linq;
+using Mono.Cecil;
+
+using Java.Interop.Tools.Cecil;
+
+using Xamarin.Android.Manifest;
+
+namespace Android.App {
+
+ partial class PropertyAttribute {
+
+ ICollection specified;
+
+ public static IEnumerable FromCustomAttributeProvider (ICustomAttributeProvider type, TypeDefinitionCache cache)
+ {
+ IEnumerable attrs = type.GetCustomAttributes ("Android.App.PropertyAttribute");
+ if (!attrs.Any ())
+ yield break;
+ foreach (CustomAttribute attr in attrs) {
+ var self = new PropertyAttribute ((string) attr.ConstructorArguments [0].Value);
+ self.specified = mapping.Load (self, attr, cache);
+ self.specified.Add ("Name");
+ yield return self;
+ }
+ }
+
+ public XElement ToElement (string packageName, TypeDefinitionCache cache)
+ {
+ return mapping.ToElement (this, specified, packageName, cache);
+ }
+ }
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/ManifestTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/ManifestTest.cs
index 9ddfaf95bf7..1083a16e6b7 100644
--- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/ManifestTest.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/ManifestTest.cs
@@ -1,19 +1,20 @@
using System;
-using System.Linq;
-using NUnit.Framework;
-using Xamarin.ProjectTools;
+using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;
-using Xamarin.Tools.Zip;
-using System.Collections.Generic;
-using Xamarin.Android.Tasks;
-using Xamarin.Android.Tools;
using Android.App;
-using Mono.Cecil;
-using System.Reflection;
+using Android.Content;
using Java.Interop.Tools.Cecil;
+using Mono.Cecil;
+using NUnit.Framework;
+using Xamarin.Android.Tasks;
+using Xamarin.Android.Tools;
+using Xamarin.ProjectTools;
+using Xamarin.Tools.Zip;
+using PropertyAttribute = Android.App.PropertyAttribute;
namespace Xamarin.Android.Build.Tests
{
@@ -1211,5 +1212,122 @@ public void IntentFilterDataPathsTest ()
StringAssertEx.AreMultiLineEqual (expected, xml);
}
+
+ static readonly string expected_activity = """
+
+
+
+ """;
+
+ static readonly string expected_application = """
+
+
+
+
+ """;
+
+ static readonly string expected_receiver = """
+
+
+
+ """;
+
+ static readonly string expected_provider = """
+
+
+
+ """;
+
+ static readonly string expected_service = """
+
+
+
+ """;
+
+ static readonly object [] PropertyElementTestSource = [
+ new object [] {typeof (TestActivity), expected_activity },
+ new object [] {typeof (TestApplication), expected_application },
+ new object [] {typeof (TestBroadcastReceiver), expected_receiver },
+ new object [] {typeof (TestContentProvider), expected_provider },
+ new object [] {typeof (TestService), expected_service },
+ ];
+
+ [Test]
+ [TestCaseSource (nameof (PropertyElementTestSource))]
+ public void PropertyTest (Type type, string expected)
+ {
+ var cache = new TypeDefinitionCache ();
+ var asm = AssemblyDefinition.ReadAssembly (type.Assembly.Location);
+ var td = asm.MainModule.GetType ($"Xamarin.Android.Build.Tests.ManifestTest/{type.Name}");
+
+ var manifest = new ManifestDocument (null) {
+ PackageName = "dummy.packageid",
+ VersionResolver = new MockVersionResolver (),
+ };
+
+ manifest.Merge (null, cache, [td], null, false, null, null);
+
+ var sb = new StringWriter ();
+ manifest.Save (null, sb);
+
+ var xml = sb.ToString ();
+ StringAssertEx.AreMultiLineContains (expected, xml);
+ }
+
+ [Activity (Name = "TestActivity")]
+ [Property ("test-activity-property", Value = "test-activity-property-value")]
+ public class TestActivity : global::Android.App.Activity
+ {
+ }
+
+ [Application (Name = "TestApplication")]
+ [Property ("test-application-property", Value = "test-application-property-value")]
+ public class TestApplication : global::Android.App.Application
+ {
+ }
+
+ [BroadcastReceiver (Name = "TestBroadcastReceiver")]
+ [Property ("test-broadcast-property", Value = "test-broadcast-property-value")]
+ public class TestBroadcastReceiver : global::Android.Content.BroadcastReceiver
+ {
+ }
+
+ [ContentProvider (["foo"], Name = "TestContentProvider")]
+ [Property ("test-provider-property", Value = "test-provider-property-value")]
+ public class TestContentProvider : global::Android.Content.ContentProvider
+ {
+ }
+
+ [Service (Name = "TestService")]
+ [Property ("test-service-property", Value = "test-service-property-value")]
+ public class TestService : global::Android.App.Service
+ {
+ }
+
+ class MockVersionResolver : IVersionResolver
+ {
+ public int? GetApiLevelFromId (string id) => 99;
+
+ public string GetIdFromApiLevel (string apiLevel) => "API-99";
+ }
}
}
+
+// Dummy types because we don't want to take a dependency on Mono.Android or Java.Interop
+namespace Android.App
+{
+ public class Activity : Java.Interop.IJavaPeerable { }
+ public class Application : Java.Interop.IJavaPeerable { }
+ public class Service : Java.Interop.IJavaPeerable { }
+}
+
+namespace Android.Content
+{
+ public class ContentProvider : Java.Interop.IJavaPeerable { }
+ public class BroadcastReceiver : Java.Interop.IJavaPeerable { }
+}
+
+namespace Java.Interop
+{
+ public interface IJavaPeerable { }
+}
diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/BuildHelper.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/BuildHelper.cs
index 7c79ceef2c9..e860acb6287 100644
--- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/BuildHelper.cs
+++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/BuildHelper.cs
@@ -102,6 +102,15 @@ public static void AreMultiLineEqual (string expected, string actual)
Assert.AreEqual (expected, actual);
}
+
+ // Checks if actual contains expected after normalizing string line endings and removing whitespace
+ public static void AreMultiLineContains (string expected, string actual)
+ {
+ expected = expected.ReplaceLineEndings ().Replace (" ", "").Replace ("\t", "");
+ actual = actual.ReplaceLineEndings ().Replace (" ", "").Replace ("\t", "");
+
+ Assert.IsTrue (actual.Contains (expected));
+ }
}
}
diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs
index 7965c8c13f7..c9a40e43025 100644
--- a/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs
+++ b/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs
@@ -94,6 +94,7 @@ internal class ManifestDocument
public bool ForceExtractNativeLibs { get; set; }
public bool ForceDebuggable { get; set; }
public string VersionName { get; set; }
+ public IVersionResolver VersionResolver { get; set; } = new MonoAndroidHelperVersionResolver ();
string versionCode;
@@ -159,12 +160,12 @@ public ManifestDocument (string templateFilename) : base ()
}
}
- string TargetSdkVersionName => MonoAndroidHelper.SupportedVersions.GetIdFromApiLevel (TargetSdkVersion);
+ string TargetSdkVersionName => VersionResolver.GetIdFromApiLevel (TargetSdkVersion);
string MinSdkVersionName =>
string.IsNullOrEmpty (MinSdkVersion) ?
TargetSdkVersionName :
- MonoAndroidHelper.SupportedVersions.GetIdFromApiLevel (MinSdkVersion);
+ VersionResolver.GetIdFromApiLevel (MinSdkVersion);
string ToFullyQualifiedName (string typeName)
{
@@ -325,7 +326,7 @@ public IList Merge (TaskLoggingHelper log, TypeDefinitionCache cache, Li
uses.AddBeforeSelf (new XComment ("suppress UsesMinSdkAttributes"));
}
- int? tryTargetSdkVersion = MonoAndroidHelper.SupportedVersions.GetApiLevelFromId (targetSdkVersion);
+ int? tryTargetSdkVersion = VersionResolver.GetApiLevelFromId (targetSdkVersion);
if (!tryTargetSdkVersion.HasValue)
throw new InvalidOperationException (string.Format ("The targetSdkVersion ({0}) is not a valid API level", targetSdkVersion));
int targetSdkVersionValue = tryTargetSdkVersion.Value;
@@ -579,6 +580,10 @@ XElement CreateApplicationElement (XElement manifest, string applicationClass, L
Assemblies.SelectMany (path => MetaDataAttribute.FromCustomAttributeProvider (Resolver.GetAssembly (path), cache))
.Where (attr => attr != null)
.ToList ();
+ var properties =
+ Assemblies.SelectMany (path => PropertyAttribute.FromCustomAttributeProvider (Resolver.GetAssembly (path), cache))
+ .Where (attr => attr != null)
+ .ToList ();
var usesLibraryAttr =
Assemblies.SelectMany (path => UsesLibraryAttribute.FromCustomAttributeProvider (Resolver.GetAssembly (path), cache))
.Where (attr => attr != null);
@@ -601,6 +606,7 @@ XElement CreateApplicationElement (XElement manifest, string applicationClass, L
typeAttr.Add (aa);
metadata.AddRange (MetaDataAttribute.FromCustomAttributeProvider (t, cache));
+ properties.AddRange (PropertyAttribute.FromCustomAttributeProvider (t, cache));
typeUsesLibraryAttr.AddRange (UsesLibraryAttribute.FromCustomAttributeProvider (t, cache));
}
@@ -638,6 +644,7 @@ XElement CreateApplicationElement (XElement manifest, string applicationClass, L
else
needManifestAdd = false;
application.Add (metadata.Select (md => md.ToElement (PackageName, cache)));
+ application.Add (properties.Select (md => md.ToElement (PackageName, cache)));
if (needManifestAdd)
manifest.Add (application);
@@ -775,12 +782,14 @@ XElement ToElement (TypeDefinition type, string name, Func metadata = MetaDataAttribute.FromCustomAttributeProvider (type, cache);
IEnumerable intents = IntentFilterAttribute.FromTypeDefinition (type, cache);
+ var properties = PropertyAttribute.FromCustomAttributeProvider (type, cache);
XElement element = toElement (attr);
if (element.Attribute (attName) == null)
element.Add (new XAttribute (attName, name));
element.Add (metadata.Select (md => md.ToElement (PackageName, cache)));
element.Add (intents.Select (intent => intent.ToElement (PackageName)));
+ element.Add (properties.Select (md => md.ToElement (PackageName, cache)));
if (update != null)
update (attr, element);
return element;
@@ -795,6 +804,7 @@ XElement ToProviderElement (TypeDefinition type, string name, TypeDefinitionCach
IEnumerable metadata = MetaDataAttribute.FromCustomAttributeProvider (type, cache);
IEnumerable grants = GrantUriPermissionAttribute.FromTypeDefinition (type, cache);
IEnumerable intents = IntentFilterAttribute.FromTypeDefinition (type, cache);
+ var properties = PropertyAttribute.FromCustomAttributeProvider (type, cache);
XElement element = attr.ToElement (PackageName, cache);
if (element.Attribute (attName) == null)
@@ -802,6 +812,7 @@ XElement ToProviderElement (TypeDefinition type, string name, TypeDefinitionCach
element.Add (metadata.Select (md => md.ToElement (PackageName, cache)));
element.Add (grants.Select (intent => intent.ToElement (PackageName, cache)));
element.Add (intents.Select (intent => intent.ToElement (PackageName)));
+ element.Add (properties.Select (md => md.ToElement (PackageName, cache)));
return element;
}
@@ -1106,4 +1117,20 @@ public void CalculateVersionCode (string currentAbi, string versionCodePattern,
VersionCode = versionCode.TrimStart ('0');
}
}
+
+ // Allow these methods to be mocked for testing instead of always calling a static class
+ public interface IVersionResolver
+ {
+ string? GetIdFromApiLevel (string apiLevel);
+ int? GetApiLevelFromId (string id);
+ }
+
+ class MonoAndroidHelperVersionResolver : IVersionResolver
+ {
+ public string? GetIdFromApiLevel (string apiLevel)
+ => MonoAndroidHelper.SupportedVersions.GetIdFromApiLevel (apiLevel);
+
+ public int? GetApiLevelFromId (string id)
+ => MonoAndroidHelper.SupportedVersions.GetApiLevelFromId (id);
+ }
}
diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj
index 3de33989a42..eeb74021822 100644
--- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj
+++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj
@@ -92,6 +92,12 @@
Mono.Android\MetaDataAttribute.Partial.cs
+
+ Mono.Android\PropertyAttribute.cs
+
+
+ Mono.Android\PropertyAttribute.Partial.cs
+
Mono.Android\GrantUriPermissionAttribute.cs