From 0033d2f821d9934e80ae1d852104de5f815fdd97 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 21 Jul 2021 15:54:06 -0500 Subject: [PATCH] [One .NET] implement $(SupportedPlatformOSVersion) for minSdkVersion Context: https://github.com/xamarin/xamarin-android/issues/6107#issuecomment-884391615 In .NET 6, we are wanting `AndroidManifest.xml` in project templates to no longer contain: This is because we can define these values such as: net6.0-android30 21.0 There is not really a reason to put this information in two places. `$(SupportedPlatformOSVersion)` is also required for enabling C# compiler warnings such as: MainActivity.cs(28,4): warning CA1416: This call site is reachable on: 'Android' 21.0 and later. 'View.AccessibilityTraversalAfter.get' is only supported on: 'android' 22.0 and later. This is an example of calling an API from 22, with a `$(SupportedPlatformOSVersion)` for 21. If `` is still *used* in `AndroidManifest.xml` in a project, it will be preferred as the final source of truth. One other improvement is that `$(SupportedPlatformOSVersion)` is expected to include `.0`, or you get the warning: UnnamedProject.AssemblyInfo.cs(21,12): warning CA1418: Version '21' is not valid for platform 'Android'. Use a version with 2-4 parts for this platform. Android unique in that this value is always an integer. In order for users to be able to specify `21`, I added a check in `Microsoft.Android.Sdk.DefaultProperties.targets` so that we append `.0` when needed. I added some tests around this scenario. --- .../.template.config/template.json | 9 +++++ .../android-bindinglib/AndroidBinding1.csproj | 1 + .../android/.template.config/template.json | 7 ++++ .../android/AndroidApp1.csproj | 1 + .../android/AndroidManifest.xml | 1 - .../androidlib/.template.config/template.json | 9 +++++ .../androidlib/AndroidLib1.csproj | 1 + .../Android/Xamarin.Android.Designer.targets | 1 + ...soft.Android.Sdk.DefaultProperties.targets | 2 + src/Xamarin.Android.Build.Tasks/Tasks/Aapt.cs | 2 +- .../Tasks/Aapt2Link.cs | 2 +- .../Tasks/GenerateJavaStubs.cs | 11 +++++- .../Xamarin.Android.Build.Tests/XASdkTests.cs | 37 ++++++++++++++++++- .../Android/KnownProperties.cs | 1 + .../Android/XASdkProject.cs | 30 ++++----------- .../Utilities/ManifestDocument.cs | 24 +++++++----- .../Xamarin.Android.Common.targets | 1 + 17 files changed, 102 insertions(+), 38 deletions(-) diff --git a/src/Microsoft.Android.Templates/android-bindinglib/.template.config/template.json b/src/Microsoft.Android.Templates/android-bindinglib/.template.config/template.json index fa0ce3e3110..97403c7632a 100644 --- a/src/Microsoft.Android.Templates/android-bindinglib/.template.config/template.json +++ b/src/Microsoft.Android.Templates/android-bindinglib/.template.config/template.json @@ -15,5 +15,14 @@ "primaryOutputs": [ { "path": "AndroidBinding1.csproj" } ], + "symbols": { + "supportedOSVersion": { + "type": "parameter", + "description": "Overrides $(SupportedOSPlatformVersion) in the project", + "datatype": "string", + "replaces": "SUPPORTED_OS_PLATFORM_VERSION", + "defaultValue": "21" + } + }, "defaultName": "AndroidBinding1" } \ No newline at end of file diff --git a/src/Microsoft.Android.Templates/android-bindinglib/AndroidBinding1.csproj b/src/Microsoft.Android.Templates/android-bindinglib/AndroidBinding1.csproj index 7cf29802739..5a6a74ace70 100644 --- a/src/Microsoft.Android.Templates/android-bindinglib/AndroidBinding1.csproj +++ b/src/Microsoft.Android.Templates/android-bindinglib/AndroidBinding1.csproj @@ -1,6 +1,7 @@ net6.0-android + SUPPORTED_OS_PLATFORM_VERSION AndroidBinding1 \ No newline at end of file diff --git a/src/Microsoft.Android.Templates/android/.template.config/template.json b/src/Microsoft.Android.Templates/android/.template.config/template.json index 0945e22e66c..b04d7f07d2d 100644 --- a/src/Microsoft.Android.Templates/android/.template.config/template.json +++ b/src/Microsoft.Android.Templates/android/.template.config/template.json @@ -28,6 +28,13 @@ "description": "Overrides the package name in the AndroidManifest.xml", "datatype": "string", "replaces": "com.companyname.AndroidApp1" + }, + "supportedOSVersion": { + "type": "parameter", + "description": "Overrides $(SupportedOSPlatformVersion) in the project", + "datatype": "string", + "replaces": "SUPPORTED_OS_PLATFORM_VERSION", + "defaultValue": "21" } }, "defaultName": "AndroidApp1" diff --git a/src/Microsoft.Android.Templates/android/AndroidApp1.csproj b/src/Microsoft.Android.Templates/android/AndroidApp1.csproj index a46e175509f..1fd328a5c21 100644 --- a/src/Microsoft.Android.Templates/android/AndroidApp1.csproj +++ b/src/Microsoft.Android.Templates/android/AndroidApp1.csproj @@ -1,6 +1,7 @@ net6.0-android + SUPPORTED_OS_PLATFORM_VERSION AndroidApp1 Exe diff --git a/src/Microsoft.Android.Templates/android/AndroidManifest.xml b/src/Microsoft.Android.Templates/android/AndroidManifest.xml index 3ce625444d4..c669bdfaa58 100644 --- a/src/Microsoft.Android.Templates/android/AndroidManifest.xml +++ b/src/Microsoft.Android.Templates/android/AndroidManifest.xml @@ -3,7 +3,6 @@ android:versionCode="1" android:versionName="1.0" package="com.companyname.AndroidApp1"> - \ No newline at end of file diff --git a/src/Microsoft.Android.Templates/androidlib/.template.config/template.json b/src/Microsoft.Android.Templates/androidlib/.template.config/template.json index 53991ab827a..061c49cc1b2 100644 --- a/src/Microsoft.Android.Templates/androidlib/.template.config/template.json +++ b/src/Microsoft.Android.Templates/androidlib/.template.config/template.json @@ -15,5 +15,14 @@ "primaryOutputs": [ { "path": "AndroidLib1.csproj" } ], + "symbols": { + "supportedOSVersion": { + "type": "parameter", + "description": "Overrides $(SupportedOSPlatformVersion) in the project", + "datatype": "string", + "replaces": "SUPPORTED_OS_PLATFORM_VERSION", + "defaultValue": "21" + } + }, "defaultName": "AndroidLib1" } \ No newline at end of file diff --git a/src/Microsoft.Android.Templates/androidlib/AndroidLib1.csproj b/src/Microsoft.Android.Templates/androidlib/AndroidLib1.csproj index 687d1512882..5eb4df59b23 100644 --- a/src/Microsoft.Android.Templates/androidlib/AndroidLib1.csproj +++ b/src/Microsoft.Android.Templates/androidlib/AndroidLib1.csproj @@ -1,6 +1,7 @@ net6.0-android + SUPPORTED_OS_PLATFORM_VERSION AndroidLib1 \ No newline at end of file diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Designer.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Designer.targets index 7eee40439d2..4b8f8388493 100644 --- a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Designer.targets +++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Designer.targets @@ -99,6 +99,7 @@ Copyright (C) 2016 Xamarin. All rights reserved. FrameworkDirectories="$(_XATargetFrameworkDirectories);$(_XATargetFrameworkDirectories)Facades" AcwMapFile="$(_AcwMapFile)" SupportedAbis="$(_BuildTargetAbis)" + SupportedOSPlatformVersion="$(SupportedOSPlatformVersion)" InstantRunEnabled="$(_InstantRunEnabled)"> <_GetChildProjectCopyToPublishDirectoryItems>false true + + $(SupportedOSPlatformVersion).0 diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/Aapt.cs b/src/Xamarin.Android.Build.Tasks/Tasks/Aapt.cs index 045dfc2e2f4..e8b09e8cf33 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/Aapt.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/Aapt.cs @@ -263,7 +263,7 @@ protected string GenerateCommandLineCommands (string ManifestFile, string curren Directory.CreateDirectory (manifestDir); manifestFile = Path.Combine (manifestDir, Path.GetFileName (ManifestFile)); ManifestDocument manifest = new ManifestDocument (ManifestFile); - manifest.SdkVersion = AndroidSdkPlatform; + manifest.TargetSdkVersion = AndroidSdkPlatform; if (!string.IsNullOrEmpty (VersionCodePattern)) { try { manifest.CalculateVersionCode (currentAbi, VersionCodePattern, VersionCodeProperties); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/Aapt2Link.cs b/src/Xamarin.Android.Build.Tasks/Tasks/Aapt2Link.cs index 74578f992bc..cf3b0fc3384 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/Aapt2Link.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/Aapt2Link.cs @@ -139,7 +139,7 @@ string [] GenerateCommandLineCommands (string ManifestFile, string currentAbi, s Directory.CreateDirectory (manifestDir); string manifestFile = Path.Combine (manifestDir, Path.GetFileName (ManifestFile)); ManifestDocument manifest = new ManifestDocument (ManifestFile); - manifest.SdkVersion = AndroidSdkPlatform; + manifest.TargetSdkVersion = AndroidSdkPlatform; if (!string.IsNullOrEmpty (VersionCodePattern)) { try { manifest.CalculateVersionCode (currentAbi, VersionCodePattern, VersionCodeProperties); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs index c6b76468af3..973d180ece8 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs @@ -82,6 +82,8 @@ public class GenerateJavaStubs : AndroidTask public string CheckedBuild { get; set; } + public string SupportedOSPlatformVersion { get; set; } + [Output] public string [] GeneratedBinaryTypeMaps { get; set; } @@ -259,6 +261,12 @@ void Run (DirectoryAssemblyResolver res) Log.LogCodedError ("XA4215", Properties.Resources.XA4215_Details, kvp.Key, typeName); } + // NOTE: $(SupportedOSPlatformVersion) will potentially be 21.0 + string minSdkVersion = null; + if (Version.TryParse (SupportedOSPlatformVersion, out var version)) { + minSdkVersion = version.Major.ToString (); + } + // Step 3 - Merge [Activity] and friends into AndroidManifest.xml var manifest = new ManifestDocument (ManifestTemplate) { PackageName = PackageName, @@ -267,7 +275,8 @@ void Run (DirectoryAssemblyResolver res) Placeholders = ManifestPlaceholders, Resolver = res, SdkDir = AndroidSdkDir, - SdkVersion = AndroidSdkPlatform, + TargetSdkVersion = AndroidSdkPlatform, + MinSdkVersion = minSdkVersion, Debug = Debug, MultiDex = MultiDex, NeedsInternet = NeedsInternet, diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs index 3212f882e2a..0fc2e67c499 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs @@ -1,7 +1,7 @@ using System; using System.IO; using System.Linq; -using System.Reflection; +using System.Xml.Linq; using Mono.Cecil; using NUnit.Framework; using Xamarin.Android.Tasks; @@ -448,6 +448,15 @@ public void DotNetBuild (string runtimeIdentifiers, bool isRelease) } } + // Check AndroidManifest.xml + var manifestPath = Path.Combine (intermediateOutputPath, "android", "AndroidManifest.xml"); + FileAssert.Exists (manifestPath); + var manifest = XDocument.Load (manifestPath); + XNamespace ns = "http://schemas.android.com/apk/res/android"; + var uses_sdk = manifest.Root.Element ("uses-sdk"); + Assert.AreEqual ("21", uses_sdk.Attribute (ns + "minSdkVersion").Value); + Assert.AreEqual ("30", uses_sdk.Attribute (ns + "targetSdkVersion").Value); + bool expectEmbeddedAssembies = !(CommercialBuildAvailable && !isRelease); var apkPath = Path.Combine (outputPath, $"{proj.PackageName}.apk"); FileAssert.Exists (apkPath); @@ -468,6 +477,32 @@ public void DotNetBuild (string runtimeIdentifiers, bool isRelease) } } + [Test] + public void SupportedOSPlatformVersion ([Values (21, 30)] int minSdkVersion) + { + var proj = new XASdkProject { + SupportedOSPlatformVersion = minSdkVersion.ToString (), + }; + // Call AccessibilityTraversalAfter from API level 22 + // https://developer.android.com/reference/android/view/View#getAccessibilityTraversalAfter() + proj.MainActivity = proj.DefaultMainActivity.Replace ("button.Click", "button.AccessibilityTraversalAfter.ToString ();\nbutton.Click"); + + var dotnet = CreateDotNetBuilder (proj); + Assert.IsTrue (dotnet.Build (), "`dotnet build` should succeed"); + + if (minSdkVersion < 22) { + StringAssertEx.Contains ("warning CA1416", dotnet.LastBuildOutput, "Should get warning about Android 22 API"); + } else { + dotnet.AssertHasNoWarnings (); + } + + var manifestPath = Path.Combine (FullProjectDirectory, proj.IntermediateOutputPath, "android", "AndroidManifest.xml"); + FileAssert.Exists (manifestPath); + var manifest = XDocument.Load (manifestPath); + XNamespace ns = "http://schemas.android.com/apk/res/android"; + Assert.AreEqual (minSdkVersion.ToString (), manifest.Root.Element ("uses-sdk").Attribute (ns + "minSdkVersion").Value); + } + [Test] [Category ("SmokeTests")] public void DotNetBuildXamarinForms ([Values (true, false)] bool useInterpreter) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/KnownProperties.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/KnownProperties.cs index 3ac6af77f8a..43c92db5284 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/KnownProperties.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/KnownProperties.cs @@ -17,6 +17,7 @@ public static class KnownProperties public const string RuntimeIdentifier = "RuntimeIdentifier"; public const string RuntimeIdentifiers = "RuntimeIdentifiers"; public const string PublishTrimmed = "PublishTrimmed"; + public const string SupportedOSPlatformVersion = "SupportedOSPlatformVersion"; public const string Deterministic = "Deterministic"; public const string BundleAssemblies = "BundleAssemblies"; diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/XASdkProject.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/XASdkProject.cs index 38c5b73357a..6882724244f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/XASdkProject.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/XASdkProject.cs @@ -56,8 +56,7 @@ public XASdkProject (string outputType = "Exe", [CallerMemberName] string packag { Sdk = "Microsoft.NET.Sdk"; TargetFramework = "net6.0-android"; - - TargetSdkVersion = AndroidSdkResolver.GetMaxInstalledPlatform ().ToString (); + SupportedOSPlatformVersion = "21"; PackageName = $"com.xamarin.{(packageName ?? ProjectName).ToLower ()}"; JavaPackageName = JavaPackageName ?? PackageName.ToLowerInvariant (); GlobalPackagesFolder = FileSystemUtils.FindNugetGlobalPackageFolder (); @@ -89,34 +88,19 @@ public XASdkProject (string outputType = "Exe", [CallerMemberName] string packag public string AndroidManifest { get; set; } = default_android_manifest; /// - /// Defaults to AndroidSdkResolver.GetMaxInstalledPlatform () - /// - public string TargetSdkVersion { get; set; } - - /// - /// Defaults to API 19 + /// Defaults to 21.0 /// - public string MinSdkVersion { get; set; } = "19"; + public string SupportedOSPlatformVersion { + get { return GetProperty (KnownProperties.SupportedOSPlatformVersion); } + set { SetProperty (KnownProperties.SupportedOSPlatformVersion, value); } + } public virtual string ProcessManifestTemplate () { - var uses_sdk = new StringBuilder (""); - return AndroidManifest .Replace ("${PROJECT_NAME}", ProjectName) .Replace ("${PACKAGENAME}", PackageName) - .Replace ("${USES_SDK}", uses_sdk.ToString ()); + .Replace ("${USES_SDK}", ""); } public override string ProcessSourceTemplate (string source) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs index e1baadddd05..a1ec85f5780 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ManifestDocument.cs @@ -84,7 +84,8 @@ internal class ManifestDocument public List Assemblies { get; set; } public DirectoryAssemblyResolver Resolver { get; set; } public string SdkDir { get; set; } - public string SdkVersion { get; set; } + public string TargetSdkVersion { get; set; } + public string MinSdkVersion { get; set; } public bool Debug { get; set; } public bool MultiDex { get; set; } public bool NeedsInternet { get; set; } @@ -118,7 +119,7 @@ public string GetMinimumSdk () { var minAttr = doc.Root.Element ("uses-sdk")?.Attribute (androidNs + "minSdkVersion"); if (minAttr == null) { int minSdkVersion; - if (!int.TryParse (SdkVersionName, out minSdkVersion)) + if (!int.TryParse (MinSdkVersionName, out minSdkVersion)) minSdkVersion = defaultMinSdkVersion; return Math.Min (minSdkVersion, defaultMinSdkVersion).ToString (); } @@ -129,7 +130,7 @@ public string GetTargetSdk () { var targetAttr = doc.Root.Element ("uses-sdk")?.Attribute (androidNs + "targetSdkVersion"); if (targetAttr == null) { - return SdkVersionName; + return TargetSdkVersionName; } return targetAttr.Value; } @@ -150,9 +151,12 @@ public ManifestDocument (string templateFilename) : base () } } - string SdkVersionName { - get { return MonoAndroidHelper.SupportedVersions.GetIdFromApiLevel (SdkVersion); } - } + string TargetSdkVersionName => MonoAndroidHelper.SupportedVersions.GetIdFromApiLevel (TargetSdkVersion); + + string MinSdkVersionName => + string.IsNullOrEmpty (MinSdkVersion) ? + TargetSdkVersionName : + MonoAndroidHelper.SupportedVersions.GetIdFromApiLevel (MinSdkVersion); string ToFullyQualifiedName (string typeName) { @@ -293,8 +297,8 @@ public IList Merge (TaskLoggingHelper log, TypeDefinitionCache cache, Li if (!manifest.Elements ("uses-sdk").Any ()) { manifest.AddFirst ( new XElement ("uses-sdk", - new XAttribute (androidNs + "minSdkVersion", SdkVersionName), - new XAttribute (androidNs + "targetSdkVersion", SdkVersionName))); + new XAttribute (androidNs + "minSdkVersion", MinSdkVersionName), + new XAttribute (androidNs + "targetSdkVersion", TargetSdkVersionName))); } // If no minSdkVersion is specified, set it to TargetFrameworkVersion @@ -302,7 +306,7 @@ public IList Merge (TaskLoggingHelper log, TypeDefinitionCache cache, Li if (uses.Attribute (androidNs + "minSdkVersion") == null) { int minSdkVersion; - if (!int.TryParse (SdkVersionName, out minSdkVersion)) + if (!int.TryParse (MinSdkVersionName, out minSdkVersion)) minSdkVersion = XABuildConfig.NDKMinimumApiAvailable; minSdkVersion = Math.Min (minSdkVersion, XABuildConfig.NDKMinimumApiAvailable); uses.SetAttributeValue (androidNs + "minSdkVersion", minSdkVersion.ToString ()); @@ -313,7 +317,7 @@ public IList Merge (TaskLoggingHelper log, TypeDefinitionCache cache, Li if (tsv != null) targetSdkVersion = tsv.Value; else { - targetSdkVersion = SdkVersionName; + targetSdkVersion = TargetSdkVersionName; uses.AddBeforeSelf (new XComment ("suppress UsesMinSdkAttributes")); } diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 5752f8eef53..e7c8c57d736 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1449,6 +1449,7 @@ because xbuild doesn't support framework reference assemblies. FrameworkDirectories="$(_XATargetFrameworkDirectories);$(_XATargetFrameworkDirectories)Facades" AcwMapFile="$(_AcwMapFile)" SupportedAbis="@(_BuildTargetAbis)" + SupportedOSPlatformVersion="$(SupportedOSPlatformVersion)" SkipJniAddNativeMethodRegistrationAttributeScan="$(_SkipJniAddNativeMethodRegistrationAttributeScan)" CheckedBuild="$(_AndroidCheckedBuild)">