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