Skip to content

Commit 31609ca

Browse files
authored
[Xamarin.Android.Tools.AndroidSdk] "Minor" SDK version support (#261)
Context: dotnet/android#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: * [`<uses-sdk/>`][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)
1 parent c4cb3db commit 31609ca

File tree

5 files changed

+111
-11
lines changed

5 files changed

+111
-11
lines changed

src/Xamarin.Android.Tools.AndroidSdk/AndroidVersion.cs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.IO;
34
using System.Xml.Linq;
45

@@ -9,6 +10,9 @@ public class AndroidVersion
910
// Android API Level. *Usually* corresponds to $(AndroidSdkPath)/platforms/android-$(ApiLevel)/android.jar
1011
public int ApiLevel { get; private set; }
1112

13+
// Android API Level; includes "minor" version bumps, e.g. Android 16 QPR2 is "36.1" while ApiLevel=36
14+
public Version VersionCodeFull { get; private set; }
15+
1216
// Android API Level ID. == ApiLevel on stable versions, will be e.g. `N` for previews: $(AndroidSdkPath)/platforms/android-N/android.jar
1317
public string Id { get; private set; }
1418

@@ -27,26 +31,42 @@ public class AndroidVersion
2731
// Is this API level stable? Should be False for non-numeric Id values.
2832
public bool Stable { get; private set; }
2933

34+
internal HashSet<string> Ids { get; } = new ();
35+
3036
// Alternate Ids for a given API level. Allows for historical mapping, e.g. API-11 has alternate ID 'H'.
31-
internal string[]? AlternateIds { get; set; }
37+
internal string[]? AlternateIds {
38+
set => Ids.UnionWith (value);
39+
}
3240

3341
public AndroidVersion (int apiLevel, string osVersion, string? codeName = null, string? id = null, bool stable = true)
42+
: this (new Version (apiLevel, 0), osVersion, codeName, id, stable)
43+
{
44+
}
45+
46+
public AndroidVersion (Version versionCodeFull, string osVersion, string? codeName = null, string? id = null, bool stable = true)
3447
{
48+
if (versionCodeFull == null)
49+
throw new ArgumentNullException (nameof (versionCodeFull));
3550
if (osVersion == null)
3651
throw new ArgumentNullException (nameof (osVersion));
3752

38-
ApiLevel = apiLevel;
39-
Id = id ?? ApiLevel.ToString ();
53+
ApiLevel = versionCodeFull.Major;
54+
VersionCodeFull = versionCodeFull;
55+
Id = id ?? (versionCodeFull.Minor != 0 ? versionCodeFull.ToString () : ApiLevel.ToString ());
4056
CodeName = codeName;
4157
OSVersion = osVersion;
4258
TargetFrameworkVersion = Version.Parse (osVersion);
4359
FrameworkVersion = "v" + osVersion;
4460
Stable = stable;
61+
62+
Ids.Add (ApiLevel.ToString ());
63+
Ids.Add (VersionCodeFull.ToString ());
64+
Ids.Add (Id);
4565
}
4666

4767
public override string ToString ()
4868
{
49-
return $"(AndroidVersion: ApiLevel={ApiLevel} Id={Id} OSVersion={OSVersion} CodeName='{CodeName}' TargetFrameworkVersion={TargetFrameworkVersion} Stable={Stable})";
69+
return $"(AndroidVersion: ApiLevel={ApiLevel} VersionCodeFull={VersionCodeFull} Id={Id} OSVersion={OSVersion} CodeName='{CodeName}' TargetFrameworkVersion={TargetFrameworkVersion} Stable={Stable})";
5070
}
5171

5272
public static AndroidVersion Load (Stream stream)
@@ -76,8 +96,13 @@ static AndroidVersion Load (XDocument doc)
7696
var name = (string?) doc.Root?.Element ("Name") ?? throw new InvalidOperationException ("Missing Name element");
7797
var version = (string?) doc.Root?.Element ("Version") ?? throw new InvalidOperationException ("Missing Version element");
7898
var stable = (bool?) doc.Root?.Element ("Stable") ?? throw new InvalidOperationException ("Missing Stable element");
99+
var versionCodeFull = (string?) doc.Root?.Element ("VersionCodeFull");
100+
101+
var fullLevel = string.IsNullOrWhiteSpace (versionCodeFull)
102+
? new Version (level, 0)
103+
: Version.Parse (versionCodeFull);
79104

80-
return new AndroidVersion (level, version.TrimStart ('v'), name, id, stable);
105+
return new AndroidVersion (fullLevel, version.TrimStart ('v'), name, id, stable);
81106
}
82107
}
83108
}

src/Xamarin.Android.Tools.AndroidSdk/AndroidVersions.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,13 @@ static bool MatchesFrameworkVersion (AndroidVersion version, string frameworkVer
9393
{
9494
return installedVersions.FirstOrDefault (v => MatchesId (v, id))?.ApiLevel ??
9595
KnownVersions.FirstOrDefault (v => MatchesId (v, id))?.ApiLevel ??
96+
(Version.TryParse (id, out var versionCodeFull) ? (int?) versionCodeFull.Major : default (int?)) ??
9697
(int.TryParse (id, out int apiLevel) ? apiLevel : default (int?));
9798
}
9899

99100
static bool MatchesId (AndroidVersion version, string id)
100101
{
101-
return version.Id == id ||
102-
(version.AlternateIds?.Contains (id) ?? false) ||
103-
(version.ApiLevel.ToString () == id);
102+
return version.Ids.Contains (id);
104103
}
105104

106105
public string? GetIdFromApiLevel (int apiLevel)
@@ -110,12 +109,21 @@ static bool MatchesId (AndroidVersion version, string id)
110109
apiLevel.ToString ();
111110
}
112111

112+
public string? GetIdFromVersionCodeFull (Version versionCodeFull)
113+
{
114+
return installedVersions.FirstOrDefault (v => v.VersionCodeFull == versionCodeFull)?.Id ??
115+
KnownVersions.FirstOrDefault (v => v.VersionCodeFull == versionCodeFull)?.Id ??
116+
versionCodeFull.ToString ();
117+
}
118+
113119
// Sometimes, e.g. when new API levels are introduced, the "API level" is a letter, not a number,
114120
// e.g. 'API-H' for API-11, 'API-O' for API-26, etc.
115121
public string? GetIdFromApiLevel (string apiLevel)
116122
{
117123
if (int.TryParse (apiLevel, out var platform))
118124
return GetIdFromApiLevel (platform);
125+
if (Version.TryParse (apiLevel, out var versionCodeFull))
126+
return GetIdFromVersionCodeFull (versionCodeFull);
119127
return installedVersions.FirstOrDefault (v => MatchesId (v, apiLevel))?.Id ??
120128
KnownVersions.FirstOrDefault (v => MatchesId (v, apiLevel))?.Id ??
121129
apiLevel;

src/Xamarin.Android.Tools.AndroidSdk/Xamarin.Android.Tools.AndroidSdk.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<PropertyGroup>
44
<TargetFrameworks>netstandard2.0</TargetFrameworks>
55
<TargetFrameworks Condition=" '$(AndroidToolsDisableMultiTargeting)' != 'true' ">$(TargetFrameworks);$(DotNetTargetFramework)</TargetFrameworks>
6-
<LangVersion>8.0</LangVersion>
6+
<LangVersion>9.0</LangVersion>
77
<Nullable>enable</Nullable>
88
<DefineConstants>INTERNAL_NULLABLE_ATTRIBUTES</DefineConstants>
99
<SignAssembly>true</SignAssembly>

tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidVersionTests.cs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,41 @@ public void Constructor_Exceptions ()
1414
{
1515
Assert.Throws<ArgumentNullException> (() => new AndroidVersion (0, null));
1616
Assert.Throws<ArgumentException> (() => new AndroidVersion (0, "not a number"));
17+
Assert.Throws<ArgumentNullException> (() => new AndroidVersion ((Version) null, osVersion: "1.0"));
1718
}
1819

1920
[Test]
2021
public void Constructor ()
2122
{
2223
var v = new AndroidVersion (apiLevel: 1, osVersion: "2.3", codeName: "Four", id: "E", stable: false);
2324
Assert.AreEqual (1, v.ApiLevel);
25+
Assert.AreEqual (new Version (1, 0), v.VersionCodeFull);
2426
Assert.AreEqual ("E", v.Id);
2527
Assert.AreEqual ("Four", v.CodeName);
2628
Assert.AreEqual ("2.3", v.OSVersion);
2729
Assert.AreEqual (new Version (2, 3), v.TargetFrameworkVersion);
2830
Assert.AreEqual ("v2.3", v.FrameworkVersion);
2931
Assert.AreEqual (false, v.Stable);
32+
Assert.IsTrue (v.Ids.SetEquals (new [] { "1", "1.0", "E" }), $"Actual Ids: {{ {string.Join (", ", v.Ids)} }}");
33+
}
34+
35+
[Test]
36+
public void Constructor_NoId ()
37+
{
38+
var v = new AndroidVersion (apiLevel: 1, osVersion: "2.3", codeName: "Four", stable: false);
39+
Assert.AreEqual (1, v.ApiLevel);
40+
Assert.AreEqual (new Version (1, 0), v.VersionCodeFull);
41+
Assert.AreEqual ("1", v.Id);
42+
Assert.AreEqual ("Four", v.CodeName);
43+
Assert.AreEqual ("2.3", v.OSVersion);
44+
Assert.AreEqual (new Version (2, 3), v.TargetFrameworkVersion);
45+
Assert.AreEqual ("v2.3", v.FrameworkVersion);
46+
Assert.AreEqual (false, v.Stable);
47+
Assert.IsTrue (v.Ids.SetEquals (new [] { "1", "1.0" }));
48+
49+
v = new AndroidVersion (new Version (2, 3), osVersion: "2.3", codeName: "Four", stable: false);
50+
Assert.AreEqual ("2.3", v.Id);
51+
Assert.IsTrue (v.Ids.SetEquals (new [] { "2", "2.3" }), $"Actual Ids: {{ {string.Join (", ", v.Ids)} }}");
3052
}
3153

3254
[Test]
@@ -55,14 +77,39 @@ public void Load ()
5577
<Version>v7.99.0</Version>
5678
<Stable>False</Stable>
5779
</AndroidApiInfo>";
58-
var v = AndroidVersion.Load (new MemoryStream (Encoding.UTF8.GetBytes (xml)));
80+
var v = AndroidVersion.Load (new MemoryStream (Encoding.UTF8.GetBytes (xml)));
5981
Assert.AreEqual (26, v.ApiLevel);
82+
Assert.AreEqual (new Version (26, 0), v.VersionCodeFull);
83+
Assert.AreEqual ("O", v.Id);
84+
Assert.AreEqual ("Android O", v.CodeName);
85+
Assert.AreEqual ("7.99.0", v.OSVersion);
86+
Assert.AreEqual (new Version (7, 99, 0), v.TargetFrameworkVersion);
87+
Assert.AreEqual ("v7.99.0", v.FrameworkVersion);
88+
Assert.AreEqual (false, v.Stable);
89+
Assert.IsTrue (v.Ids.SetEquals (new [] { "26", "26.0", "O" }), $"Actual Ids: {{ {string.Join (", ", v.Ids)} }}");
90+
}
91+
92+
[Test]
93+
public void Load_VersionCodeFull_Replaces_Level ()
94+
{
95+
var xml = @"<AndroidApiInfo>
96+
<Id>O</Id>
97+
<Level>26</Level>
98+
<VersionCodeFull>27.1</VersionCodeFull>
99+
<Name>Android O</Name>
100+
<Version>v7.99.0</Version>
101+
<Stable>False</Stable>
102+
</AndroidApiInfo>";
103+
var v = AndroidVersion.Load (new MemoryStream (Encoding.UTF8.GetBytes (xml)));
104+
Assert.AreEqual (27, v.ApiLevel);
105+
Assert.AreEqual (new Version (27, 1), v.VersionCodeFull);
60106
Assert.AreEqual ("O", v.Id);
61107
Assert.AreEqual ("Android O", v.CodeName);
62108
Assert.AreEqual ("7.99.0", v.OSVersion);
63109
Assert.AreEqual (new Version (7, 99, 0), v.TargetFrameworkVersion);
64110
Assert.AreEqual ("v7.99.0", v.FrameworkVersion);
65111
Assert.AreEqual (false, v.Stable);
112+
Assert.IsTrue (v.Ids.SetEquals (new [] { "27", "27.1", "O" }), $"Actual Ids: {{ {string.Join (", ", v.Ids)} }}");
66113
}
67114
}
68115
}

tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidVersionsTests.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.IO;
4-
4+
using Microsoft.VisualStudio.TestPlatform.Utilities;
55
using NUnit.Framework;
66

77
namespace Xamarin.Android.Tools.Tests
@@ -92,6 +92,7 @@ public void Constructor_FrameworkDirectories ()
9292
"<AndroidApiInfo>",
9393
" <Id>Z</Id>",
9494
" <Level>127</Level>",
95+
" <VersionCodeFull>127.1</VersionCodeFull>",
9596
" <Name>Z</Name>",
9697
" <Version>v108.1.99</Version>",
9798
" <Stable>False</Stable>",
@@ -138,6 +139,9 @@ static AndroidVersions CreateTestVersions ()
138139
new AndroidVersion (apiLevel: 3, osVersion: "1.2", id: "C", stable: true),
139140
// Hides/shadows a Known Version
140141
new AndroidVersion (apiLevel: 14, osVersion: "4.0", id: "II", stable: false),
142+
// Demonstrates new "minor" release support
143+
new AndroidVersion (versionCodeFull: new Version (36, 0), osVersion: "16.0", id: "Baklava", stable: true),
144+
new AndroidVersion (versionCodeFull: new Version (36, 1), osVersion: "16.1", id: "CANARY", stable: false),
141145
});
142146
}
143147

@@ -157,6 +161,7 @@ public void GetApiLevelFromFrameworkVersion ()
157161
Assert.AreEqual (null, versions.GetApiLevelFromFrameworkVersion ("1.3"));
158162
Assert.AreEqual (14, versions.GetApiLevelFromFrameworkVersion ("v4.0"));
159163
Assert.AreEqual (14, versions.GetApiLevelFromFrameworkVersion ("4.0"));
164+
Assert.AreEqual (36, versions.GetApiLevelFromFrameworkVersion ("16.1"));
160165

161166
// via KnownVersions
162167
Assert.AreEqual (4, versions.GetApiLevelFromFrameworkVersion ("v1.6"));
@@ -177,6 +182,8 @@ public void GetApiLevelFromId ()
177182
Assert.AreEqual (3, versions.GetApiLevelFromId ("3"));
178183
Assert.AreEqual (14, versions.GetApiLevelFromId ("14"));
179184
Assert.AreEqual (14, versions.GetApiLevelFromId ("II"));
185+
Assert.AreEqual (36, versions.GetApiLevelFromId ("36"));
186+
Assert.AreEqual (36, versions.GetApiLevelFromId ("CANARY"));
180187

181188
Assert.AreEqual (null, versions.GetApiLevelFromId ("D"));
182189

@@ -202,6 +209,13 @@ public void GetIdFromApiLevel ()
202209
Assert.AreEqual ("II", versions.GetIdFromApiLevel ("14"));
203210
Assert.AreEqual ("II", versions.GetIdFromApiLevel ("II"));
204211

212+
Assert.AreEqual ("Baklava", versions.GetIdFromApiLevel (36));
213+
Assert.AreEqual ("Baklava", versions.GetIdFromApiLevel ("36"));
214+
Assert.AreEqual ("Baklava", versions.GetIdFromApiLevel ("36.0"));
215+
Assert.AreEqual ("Baklava", versions.GetIdFromApiLevel ("Baklava"));
216+
Assert.AreEqual ("CANARY", versions.GetIdFromApiLevel ("36.1"));
217+
Assert.AreEqual ("CANARY", versions.GetIdFromApiLevel ("CANARY"));
218+
205219
Assert.AreEqual ("-1", versions.GetIdFromApiLevel (-1));
206220
Assert.AreEqual ("-1", versions.GetIdFromApiLevel ("-1"));
207221
Assert.AreEqual ("D", versions.GetIdFromApiLevel ("D"));
@@ -226,6 +240,7 @@ public void GetIdFromFrameworkVersion ()
226240
Assert.AreEqual ("C", versions.GetIdFromFrameworkVersion ("1.2"));
227241
Assert.AreEqual ("II", versions.GetIdFromFrameworkVersion ("v4.0"));
228242
Assert.AreEqual ("II", versions.GetIdFromFrameworkVersion ("4.0"));
243+
Assert.AreEqual ("CANARY", versions.GetIdFromFrameworkVersion ("16.1"));
229244

230245
Assert.AreEqual (null, versions.GetIdFromFrameworkVersion ("v0.99"));
231246
Assert.AreEqual (null, versions.GetIdFromFrameworkVersion ("0.99"));
@@ -245,6 +260,7 @@ public void GetFrameworkVersionFromApiLevel ()
245260
Assert.AreEqual ("v1.1", versions.GetFrameworkVersionFromApiLevel (2));
246261
Assert.AreEqual ("v1.2", versions.GetFrameworkVersionFromApiLevel (3));
247262
Assert.AreEqual ("v4.0", versions.GetFrameworkVersionFromApiLevel (14));
263+
Assert.AreEqual ("v16.0", versions.GetFrameworkVersionFromApiLevel (36));
248264

249265
// via KnownVersions
250266
Assert.AreEqual ("v2.3", versions.GetFrameworkVersionFromApiLevel (10));
@@ -264,6 +280,10 @@ public void GetFrameworkVersionFromId ()
264280
Assert.AreEqual ("v1.2", versions.GetFrameworkVersionFromId ("C"));
265281
Assert.AreEqual ("v4.0", versions.GetFrameworkVersionFromId ("14"));
266282
Assert.AreEqual ("v4.0", versions.GetFrameworkVersionFromId ("II"));
283+
Assert.AreEqual ("v16.0", versions.GetFrameworkVersionFromId ("36"));
284+
Assert.AreEqual ("v16.0", versions.GetFrameworkVersionFromId ("Baklava"));
285+
Assert.AreEqual ("v16.1", versions.GetFrameworkVersionFromId ("36.1"));
286+
Assert.AreEqual ("v16.1", versions.GetFrameworkVersionFromId ("CANARY"));
267287

268288
// via KnownVersions
269289
Assert.AreEqual ("v3.0", versions.GetFrameworkVersionFromId ("11"));

0 commit comments

Comments
 (0)