From e60e7624438a420ea7f03df84d2dcdb814d55230 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Mon, 25 Aug 2025 14:25:05 -0400 Subject: [PATCH] [Xamarin.Android.Tools.AndroidSdk] "Minor" SDK version support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Context: https://github.com/dotnet/android/pull/10438 9 months ago in [The First Developer Preview of Android 16][0]: > **Two Android API releases in 2025** > > * This preview is for the next major release of Android with a > planned launch in Q2 of 2025. This release is similar to all of > our API releases in the past, where we can have planned > behavior changes that are often tied to a targetSdkVersion. > > * … > > * We plan to have another release in Q4 of 2025 which also will > include new developer APIs. The Q2 major release will be the > only release in 2025 to include planned behavior changes that > could affect apps. The 3rd bullet point is a "25Q4 MINOR SDK RELEASE" , thus introducing the *concept* of a "minor" SDK version, with semantics: * [``][3]: > It's not possible to specify that an app either targets or > requires a minor SDK version. * [Using new APIs with major and minor releases][4]: > The new [`SDK_INT_FULL`][5] constant can be used for API checks… > > if (SDK_INT_FULL >= VERSION_CODES_FULL.[MAJOR or MINOR RELEASE]) { > // Use APIs introduced in a major or minor release > } > > You can also use the [`Build.getMinorSdkVersion()`][6] method to > get just the minor SDK version: > > minorSdkVersion = Build.getMinorSdkVersion(Build.VERSION_CODES_FULL.BAKLAVA); Update `AndroidVersion` and `AndroidVersions` to better support the concept of "minor SDK releases": * Add a new `AndroidVersion.VersionCodeFull` property, which is a `System.Version` -- not an `int` -- for which `Version.Major` matches `AndroidVersion.ApiLevel`. * Add a new internal `AndroidVersion.Ids` property, which is the= full set of "aliases" that should be checked when doing an "id" match. This simplifies `AndroidVersions` logic. `Ids` contains: `ApiLevel`, VersionCodeFull`, and `Id`. * Change `AndroidVersions.AlternateIds` into a set-only property which updates `AndroidVersion.Ids`. * Bump `$(LangVersion)`=9.0 to use target-typed `new()`. [0]: https://android-developers.googleblog.com/2024/11/the-first-developer-preview-android16.html [3]: https://developer.android.com/guide/topics/manifest/uses-sdk-element [4]: https://developer.android.com/about/versions/16/features#using-new [5]: https://developer.android.com/reference/android/os/Build.VERSION#SDK_INT_FULL [6]: https://developer.android.com/reference/android/os/Build#getMinorSdkVersion(int) --- .../AndroidVersion.cs | 35 +++++++++++-- .../AndroidVersions.cs | 14 ++++-- .../Xamarin.Android.Tools.AndroidSdk.csproj | 2 +- .../AndroidVersionTests.cs | 49 ++++++++++++++++++- .../AndroidVersionsTests.cs | 22 ++++++++- 5 files changed, 111 insertions(+), 11 deletions(-) diff --git a/src/Xamarin.Android.Tools.AndroidSdk/AndroidVersion.cs b/src/Xamarin.Android.Tools.AndroidSdk/AndroidVersion.cs index 1ae78dce..911f3c17 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/AndroidVersion.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/AndroidVersion.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Xml.Linq; @@ -9,6 +10,9 @@ public class AndroidVersion // Android API Level. *Usually* corresponds to $(AndroidSdkPath)/platforms/android-$(ApiLevel)/android.jar public int ApiLevel { get; private set; } + // Android API Level; includes "minor" version bumps, e.g. Android 16 QPR2 is "36.1" while ApiLevel=36 + public Version VersionCodeFull { get; private set; } + // Android API Level ID. == ApiLevel on stable versions, will be e.g. `N` for previews: $(AndroidSdkPath)/platforms/android-N/android.jar public string Id { get; private set; } @@ -27,26 +31,42 @@ public class AndroidVersion // Is this API level stable? Should be False for non-numeric Id values. public bool Stable { get; private set; } + internal HashSet Ids { get; } = new (); + // Alternate Ids for a given API level. Allows for historical mapping, e.g. API-11 has alternate ID 'H'. - internal string[]? AlternateIds { get; set; } + internal string[]? AlternateIds { + set => Ids.UnionWith (value); + } public AndroidVersion (int apiLevel, string osVersion, string? codeName = null, string? id = null, bool stable = true) + : this (new Version (apiLevel, 0), osVersion, codeName, id, stable) + { + } + + public AndroidVersion (Version versionCodeFull, string osVersion, string? codeName = null, string? id = null, bool stable = true) { + if (versionCodeFull == null) + throw new ArgumentNullException (nameof (versionCodeFull)); if (osVersion == null) throw new ArgumentNullException (nameof (osVersion)); - ApiLevel = apiLevel; - Id = id ?? ApiLevel.ToString (); + ApiLevel = versionCodeFull.Major; + VersionCodeFull = versionCodeFull; + Id = id ?? (versionCodeFull.Minor != 0 ? versionCodeFull.ToString () : ApiLevel.ToString ()); CodeName = codeName; OSVersion = osVersion; TargetFrameworkVersion = Version.Parse (osVersion); FrameworkVersion = "v" + osVersion; Stable = stable; + + Ids.Add (ApiLevel.ToString ()); + Ids.Add (VersionCodeFull.ToString ()); + Ids.Add (Id); } public override string ToString () { - return $"(AndroidVersion: ApiLevel={ApiLevel} Id={Id} OSVersion={OSVersion} CodeName='{CodeName}' TargetFrameworkVersion={TargetFrameworkVersion} Stable={Stable})"; + return $"(AndroidVersion: ApiLevel={ApiLevel} VersionCodeFull={VersionCodeFull} Id={Id} OSVersion={OSVersion} CodeName='{CodeName}' TargetFrameworkVersion={TargetFrameworkVersion} Stable={Stable})"; } public static AndroidVersion Load (Stream stream) @@ -76,8 +96,13 @@ static AndroidVersion Load (XDocument doc) var name = (string?) doc.Root?.Element ("Name") ?? throw new InvalidOperationException ("Missing Name element"); var version = (string?) doc.Root?.Element ("Version") ?? throw new InvalidOperationException ("Missing Version element"); var stable = (bool?) doc.Root?.Element ("Stable") ?? throw new InvalidOperationException ("Missing Stable element"); + var versionCodeFull = (string?) doc.Root?.Element ("VersionCodeFull"); + + var fullLevel = string.IsNullOrWhiteSpace (versionCodeFull) + ? new Version (level, 0) + : Version.Parse (versionCodeFull); - return new AndroidVersion (level, version.TrimStart ('v'), name, id, stable); + return new AndroidVersion (fullLevel, version.TrimStart ('v'), name, id, stable); } } } diff --git a/src/Xamarin.Android.Tools.AndroidSdk/AndroidVersions.cs b/src/Xamarin.Android.Tools.AndroidSdk/AndroidVersions.cs index e44f655b..ce159226 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/AndroidVersions.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/AndroidVersions.cs @@ -93,14 +93,13 @@ static bool MatchesFrameworkVersion (AndroidVersion version, string frameworkVer { return installedVersions.FirstOrDefault (v => MatchesId (v, id))?.ApiLevel ?? KnownVersions.FirstOrDefault (v => MatchesId (v, id))?.ApiLevel ?? + (Version.TryParse (id, out var versionCodeFull) ? (int?) versionCodeFull.Major : default (int?)) ?? (int.TryParse (id, out int apiLevel) ? apiLevel : default (int?)); } static bool MatchesId (AndroidVersion version, string id) { - return version.Id == id || - (version.AlternateIds?.Contains (id) ?? false) || - (version.ApiLevel.ToString () == id); + return version.Ids.Contains (id); } public string? GetIdFromApiLevel (int apiLevel) @@ -110,12 +109,21 @@ static bool MatchesId (AndroidVersion version, string id) apiLevel.ToString (); } + public string? GetIdFromVersionCodeFull (Version versionCodeFull) + { + return installedVersions.FirstOrDefault (v => v.VersionCodeFull == versionCodeFull)?.Id ?? + KnownVersions.FirstOrDefault (v => v.VersionCodeFull == versionCodeFull)?.Id ?? + versionCodeFull.ToString (); + } + // Sometimes, e.g. when new API levels are introduced, the "API level" is a letter, not a number, // e.g. 'API-H' for API-11, 'API-O' for API-26, etc. public string? GetIdFromApiLevel (string apiLevel) { if (int.TryParse (apiLevel, out var platform)) return GetIdFromApiLevel (platform); + if (Version.TryParse (apiLevel, out var versionCodeFull)) + return GetIdFromVersionCodeFull (versionCodeFull); return installedVersions.FirstOrDefault (v => MatchesId (v, apiLevel))?.Id ?? KnownVersions.FirstOrDefault (v => MatchesId (v, apiLevel))?.Id ?? apiLevel; diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Xamarin.Android.Tools.AndroidSdk.csproj b/src/Xamarin.Android.Tools.AndroidSdk/Xamarin.Android.Tools.AndroidSdk.csproj index 5fb3608b..9d16f700 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Xamarin.Android.Tools.AndroidSdk.csproj +++ b/src/Xamarin.Android.Tools.AndroidSdk/Xamarin.Android.Tools.AndroidSdk.csproj @@ -3,7 +3,7 @@ netstandard2.0 $(TargetFrameworks);$(DotNetTargetFramework) - 8.0 + 9.0 enable INTERNAL_NULLABLE_ATTRIBUTES true diff --git a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidVersionTests.cs b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidVersionTests.cs index c162b590..d215469f 100644 --- a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidVersionTests.cs +++ b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidVersionTests.cs @@ -14,6 +14,7 @@ public void Constructor_Exceptions () { Assert.Throws (() => new AndroidVersion (0, null)); Assert.Throws (() => new AndroidVersion (0, "not a number")); + Assert.Throws (() => new AndroidVersion ((Version) null, osVersion: "1.0")); } [Test] @@ -21,12 +22,33 @@ public void Constructor () { var v = new AndroidVersion (apiLevel: 1, osVersion: "2.3", codeName: "Four", id: "E", stable: false); Assert.AreEqual (1, v.ApiLevel); + Assert.AreEqual (new Version (1, 0), v.VersionCodeFull); Assert.AreEqual ("E", v.Id); Assert.AreEqual ("Four", v.CodeName); Assert.AreEqual ("2.3", v.OSVersion); Assert.AreEqual (new Version (2, 3), v.TargetFrameworkVersion); Assert.AreEqual ("v2.3", v.FrameworkVersion); Assert.AreEqual (false, v.Stable); + Assert.IsTrue (v.Ids.SetEquals (new [] { "1", "1.0", "E" }), $"Actual Ids: {{ {string.Join (", ", v.Ids)} }}"); + } + + [Test] + public void Constructor_NoId () + { + var v = new AndroidVersion (apiLevel: 1, osVersion: "2.3", codeName: "Four", stable: false); + Assert.AreEqual (1, v.ApiLevel); + Assert.AreEqual (new Version (1, 0), v.VersionCodeFull); + Assert.AreEqual ("1", v.Id); + Assert.AreEqual ("Four", v.CodeName); + Assert.AreEqual ("2.3", v.OSVersion); + Assert.AreEqual (new Version (2, 3), v.TargetFrameworkVersion); + Assert.AreEqual ("v2.3", v.FrameworkVersion); + Assert.AreEqual (false, v.Stable); + Assert.IsTrue (v.Ids.SetEquals (new [] { "1", "1.0" })); + + v = new AndroidVersion (new Version (2, 3), osVersion: "2.3", codeName: "Four", stable: false); + Assert.AreEqual ("2.3", v.Id); + Assert.IsTrue (v.Ids.SetEquals (new [] { "2", "2.3" }), $"Actual Ids: {{ {string.Join (", ", v.Ids)} }}"); } [Test] @@ -55,14 +77,39 @@ public void Load () v7.99.0 False "; - var v = AndroidVersion.Load (new MemoryStream (Encoding.UTF8.GetBytes (xml))); + var v = AndroidVersion.Load (new MemoryStream (Encoding.UTF8.GetBytes (xml))); Assert.AreEqual (26, v.ApiLevel); + Assert.AreEqual (new Version (26, 0), v.VersionCodeFull); + Assert.AreEqual ("O", v.Id); + Assert.AreEqual ("Android O", v.CodeName); + Assert.AreEqual ("7.99.0", v.OSVersion); + Assert.AreEqual (new Version (7, 99, 0), v.TargetFrameworkVersion); + Assert.AreEqual ("v7.99.0", v.FrameworkVersion); + Assert.AreEqual (false, v.Stable); + Assert.IsTrue (v.Ids.SetEquals (new [] { "26", "26.0", "O" }), $"Actual Ids: {{ {string.Join (", ", v.Ids)} }}"); + } + + [Test] + public void Load_VersionCodeFull_Replaces_Level () + { + var xml = @" + O + 26 + 27.1 + Android O + v7.99.0 + False +"; + var v = AndroidVersion.Load (new MemoryStream (Encoding.UTF8.GetBytes (xml))); + Assert.AreEqual (27, v.ApiLevel); + Assert.AreEqual (new Version (27, 1), v.VersionCodeFull); Assert.AreEqual ("O", v.Id); Assert.AreEqual ("Android O", v.CodeName); Assert.AreEqual ("7.99.0", v.OSVersion); Assert.AreEqual (new Version (7, 99, 0), v.TargetFrameworkVersion); Assert.AreEqual ("v7.99.0", v.FrameworkVersion); Assert.AreEqual (false, v.Stable); + Assert.IsTrue (v.Ids.SetEquals (new [] { "27", "27.1", "O" }), $"Actual Ids: {{ {string.Join (", ", v.Ids)} }}"); } } } diff --git a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidVersionsTests.cs b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidVersionsTests.cs index 1ebe8f2c..d509fe5a 100644 --- a/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidVersionsTests.cs +++ b/tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidVersionsTests.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.IO; - +using Microsoft.VisualStudio.TestPlatform.Utilities; using NUnit.Framework; namespace Xamarin.Android.Tools.Tests @@ -92,6 +92,7 @@ public void Constructor_FrameworkDirectories () "", " Z", " 127", + " 127.1", " Z", " v108.1.99", " False", @@ -138,6 +139,9 @@ static AndroidVersions CreateTestVersions () new AndroidVersion (apiLevel: 3, osVersion: "1.2", id: "C", stable: true), // Hides/shadows a Known Version new AndroidVersion (apiLevel: 14, osVersion: "4.0", id: "II", stable: false), + // Demonstrates new "minor" release support + new AndroidVersion (versionCodeFull: new Version (36, 0), osVersion: "16.0", id: "Baklava", stable: true), + new AndroidVersion (versionCodeFull: new Version (36, 1), osVersion: "16.1", id: "CANARY", stable: false), }); } @@ -157,6 +161,7 @@ public void GetApiLevelFromFrameworkVersion () Assert.AreEqual (null, versions.GetApiLevelFromFrameworkVersion ("1.3")); Assert.AreEqual (14, versions.GetApiLevelFromFrameworkVersion ("v4.0")); Assert.AreEqual (14, versions.GetApiLevelFromFrameworkVersion ("4.0")); + Assert.AreEqual (36, versions.GetApiLevelFromFrameworkVersion ("16.1")); // via KnownVersions Assert.AreEqual (4, versions.GetApiLevelFromFrameworkVersion ("v1.6")); @@ -177,6 +182,8 @@ public void GetApiLevelFromId () Assert.AreEqual (3, versions.GetApiLevelFromId ("3")); Assert.AreEqual (14, versions.GetApiLevelFromId ("14")); Assert.AreEqual (14, versions.GetApiLevelFromId ("II")); + Assert.AreEqual (36, versions.GetApiLevelFromId ("36")); + Assert.AreEqual (36, versions.GetApiLevelFromId ("CANARY")); Assert.AreEqual (null, versions.GetApiLevelFromId ("D")); @@ -202,6 +209,13 @@ public void GetIdFromApiLevel () Assert.AreEqual ("II", versions.GetIdFromApiLevel ("14")); Assert.AreEqual ("II", versions.GetIdFromApiLevel ("II")); + Assert.AreEqual ("Baklava", versions.GetIdFromApiLevel (36)); + Assert.AreEqual ("Baklava", versions.GetIdFromApiLevel ("36")); + Assert.AreEqual ("Baklava", versions.GetIdFromApiLevel ("36.0")); + Assert.AreEqual ("Baklava", versions.GetIdFromApiLevel ("Baklava")); + Assert.AreEqual ("CANARY", versions.GetIdFromApiLevel ("36.1")); + Assert.AreEqual ("CANARY", versions.GetIdFromApiLevel ("CANARY")); + Assert.AreEqual ("-1", versions.GetIdFromApiLevel (-1)); Assert.AreEqual ("-1", versions.GetIdFromApiLevel ("-1")); Assert.AreEqual ("D", versions.GetIdFromApiLevel ("D")); @@ -226,6 +240,7 @@ public void GetIdFromFrameworkVersion () Assert.AreEqual ("C", versions.GetIdFromFrameworkVersion ("1.2")); Assert.AreEqual ("II", versions.GetIdFromFrameworkVersion ("v4.0")); Assert.AreEqual ("II", versions.GetIdFromFrameworkVersion ("4.0")); + Assert.AreEqual ("CANARY", versions.GetIdFromFrameworkVersion ("16.1")); Assert.AreEqual (null, versions.GetIdFromFrameworkVersion ("v0.99")); Assert.AreEqual (null, versions.GetIdFromFrameworkVersion ("0.99")); @@ -245,6 +260,7 @@ public void GetFrameworkVersionFromApiLevel () Assert.AreEqual ("v1.1", versions.GetFrameworkVersionFromApiLevel (2)); Assert.AreEqual ("v1.2", versions.GetFrameworkVersionFromApiLevel (3)); Assert.AreEqual ("v4.0", versions.GetFrameworkVersionFromApiLevel (14)); + Assert.AreEqual ("v16.0", versions.GetFrameworkVersionFromApiLevel (36)); // via KnownVersions Assert.AreEqual ("v2.3", versions.GetFrameworkVersionFromApiLevel (10)); @@ -264,6 +280,10 @@ public void GetFrameworkVersionFromId () Assert.AreEqual ("v1.2", versions.GetFrameworkVersionFromId ("C")); Assert.AreEqual ("v4.0", versions.GetFrameworkVersionFromId ("14")); Assert.AreEqual ("v4.0", versions.GetFrameworkVersionFromId ("II")); + Assert.AreEqual ("v16.0", versions.GetFrameworkVersionFromId ("36")); + Assert.AreEqual ("v16.0", versions.GetFrameworkVersionFromId ("Baklava")); + Assert.AreEqual ("v16.1", versions.GetFrameworkVersionFromId ("36.1")); + Assert.AreEqual ("v16.1", versions.GetFrameworkVersionFromId ("CANARY")); // via KnownVersions Assert.AreEqual ("v3.0", versions.GetFrameworkVersionFromId ("11"));