Skip to content

Commit f07b538

Browse files
authored
[generator] Support major.minor API levels (#1360)
Context: dotnet/android#10438 Context: dotnet/android#10438 (comment) Android 16 Quarterly Platform Release 2 (QPR2) (API-CANARY) Beta 1 has been released. What makes API_CANARY unique in the history of Android is that it is a "minor" SDK version: * [`<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); This is ***not*** "API-37". This is "API-36.1" This impacts *everything*: * `[SupportedOSPlatform]` should include the minor SDK value * `[UnsupportedOSPlatform]` should also include the minor SDK value. * `generator --api-level=API-LEVEL` should support the minor value and appropriately propagate the value. * … Add a new `Java.Interop.Tools.Generator.AndroidSdkVersion` type which holds the Android SDK version, both major (`ApiLevel)` and minor values (`MinorRevision)`. This is a value type which works like `System.Int32`/`System.Version`. Replace all instances of `int ApiLevel` (and equivalent) with `AndroidSdkVersion ApiLevel` (and equivalent). Update `[SupportedOSPlatform]`, `[UnsupportedOSPlatform]`, and `[ObsoletedOSPlatform]` attribute output to include the full SDK value. Update `NamingConverter.ParseApiLevel()` to support `MAJOR.MINOR` values within a `//@merge.SourceFile` value. *Note*: Changes to `NamingConverter.ParseApiLevel()` are largely a nothing-burger because the dotnet/android side usually has metadata: <attr api-since="36.1" path="/api//*[contains(@merge.SourceFile,'api-CANARY.xml.in')]" name="api-since">36.1</attr which explicitly adds/sets `//@api-since` based on the `//@merge.SourceFile` value. [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 a5d7370 commit f07b538

File tree

30 files changed

+434
-155
lines changed

30 files changed

+434
-155
lines changed

src/Java.Interop.Tools.Generator/Enumification/ConstantEntry.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ namespace Java.Interop.Tools.Generator.Enumification
99
public class ConstantEntry
1010
{
1111
public ConstantAction Action { get; set; }
12-
public int ApiLevel { get; set; }
12+
public AndroidSdkVersion ApiLevel { get; set; }
1313
public string? JavaSignature { get; set; }
1414
public string? Value { get; set; }
1515
public string? EnumFullType { get; set; }
1616
public string? EnumMember { get; set; }
1717
public FieldAction FieldAction { get; set; }
1818
public bool IsFlags { get; set; }
19-
public int? DeprecatedSince { get; set; }
19+
public AndroidSdkVersion? DeprecatedSince { get; set; }
2020

2121
public string EnumNamespace {
2222
get {
@@ -100,7 +100,7 @@ static ConstantEntry FromVersion1String (CsvParser parser, bool transientMode)
100100
{
101101
var entry = new ConstantEntry {
102102
Action = ConstantAction.Enumify,
103-
ApiLevel = parser.GetFieldAsInt (0),
103+
ApiLevel = parser.GetFieldAsAndroidSdkVersion (0),
104104
EnumFullType = parser.GetField (1),
105105
EnumMember = parser.GetField (2),
106106
JavaSignature = parser.GetField (3),
@@ -128,14 +128,14 @@ static ConstantEntry FromVersion2String (CsvParser parser)
128128
{
129129
var entry = new ConstantEntry {
130130
Action = FromConstantActionString (parser.GetField (0)),
131-
ApiLevel = parser.GetFieldAsInt (1),
131+
ApiLevel = parser.GetFieldAsAndroidSdkVersion (1),
132132
JavaSignature = parser.GetField (2),
133133
Value = parser.GetField (3),
134134
EnumFullType = parser.GetField (4),
135135
EnumMember = parser.GetField (5),
136136
FieldAction = FromFieldActionString (parser.GetField (6)),
137137
IsFlags = parser.GetField (7).ToLowerInvariant () == "flags",
138-
DeprecatedSince = parser.GetFieldAsNullableInt32 (8)
138+
DeprecatedSince = parser.GetFieldAsNullableAndroidSdkVersion (8)
139139
};
140140

141141
entry.NormalizeJavaSignature ();

src/Java.Interop.Tools.Generator/Enumification/MethodMapEntry.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace Java.Interop.Tools.Generator.Enumification
88
public class MethodMapEntry
99
{
1010
public MethodAction Action { get; set; }
11-
public int ApiLevel { get; set; }
11+
public AndroidSdkVersion ApiLevel { get; set; }
1212
public string? JavaPackage { get; set; }
1313
public string? JavaType { get; set; }
1414
public string? JavaName { get; set; }
@@ -66,7 +66,7 @@ static MethodMapEntry FromVersion1String (CsvParser parser)
6666
{
6767
var entry = new MethodMapEntry {
6868
Action = MethodAction.Enumify,
69-
ApiLevel = parser.GetFieldAsInt (0),
69+
ApiLevel = parser.GetFieldAsAndroidSdkVersion (0),
7070
JavaPackage = parser.GetField (1),
7171
JavaType = parser.GetField (2),
7272
JavaName = parser.GetField (3),
@@ -86,7 +86,7 @@ static MethodMapEntry FromVersion2String (CsvParser parser)
8686
{
8787
var entry = new MethodMapEntry {
8888
Action = FromMethodActionString (parser.GetField (0)),
89-
ApiLevel = parser.GetFieldAsInt (1),
89+
ApiLevel = parser.GetFieldAsAndroidSdkVersion (1),
9090
JavaPackage = parser.GetField (2),
9191
JavaType = parser.GetField (3),
9292
JavaName = parser.GetField (4),

src/Java.Interop.Tools.Generator/Extensions/XmlExtensions.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
using System.Xml.Linq;
44
using System.Xml.XPath;
55

6+
using Java.Interop.Tools.Generator;
7+
68
namespace Xamarin.Android.Tools
79
{
810
static class XmlExtensions
@@ -13,11 +15,11 @@ static class XmlExtensions
1315
public static string? XGetAttribute (this XPathNavigator nav, string name, string ns)
1416
=> nav.GetAttribute (name, ns)?.Trim ();
1517

16-
public static int? XGetAttributeAsInt (this XElement element, string name)
18+
public static AndroidSdkVersion? XGetAttributeAsAndroidSdkVersion (this XElement element, string name)
1719
{
1820
var value = element.XGetAttribute (name);
1921

20-
if (int.TryParse (value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
22+
if (AndroidSdkVersion.TryParse (value, out var result))
2123
return result;
2224

2325
return null;

src/Java.Interop.Tools.Generator/Metadata/FixupXmlDocument.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public FixupXmlDocument (XDocument fixupDocument)
2828
public void Apply (ApiXmlDocument apiDocument, string apiLevelString, int productVersion)
2929
{
3030
// Defaulting to 0 here is fine
31-
int.TryParse (apiLevelString, out var apiLevel);
31+
AndroidSdkVersion.TryParse (apiLevelString, out var apiLevel);
3232

3333
var metadataChildren = FixupDocument.XPathSelectElements ("/metadata/*");
3434

@@ -193,22 +193,22 @@ public IList<NamespaceTransform> GetNamespaceTransforms ()
193193
return list;
194194
}
195195

196-
bool ShouldSkip (XElement node, int apiLevel, int productVersion)
196+
bool ShouldSkip (XElement node, AndroidSdkVersion apiLevel, int productVersion)
197197
{
198198
if (apiLevel > 0) {
199-
var since = node.XGetAttributeAsInt ("api-since");
200-
var until = node.XGetAttributeAsInt ("api-until");
199+
var since = node.XGetAttributeAsAndroidSdkVersion ("api-since");
200+
var until = node.XGetAttributeAsAndroidSdkVersion ("api-until");
201201

202-
if (since is int since_int && since_int > apiLevel)
202+
if (since is AndroidSdkVersion since_int && since_int > apiLevel)
203203
return true;
204-
else if (until is int until_int && until_int < apiLevel)
204+
else if (until is AndroidSdkVersion until_int && until_int < apiLevel)
205205
return true;
206206
}
207207

208208
if (productVersion > 0) {
209-
var product_version = node.XGetAttributeAsInt ("product-version");
209+
var product_version = node.XGetAttributeAsAndroidSdkVersion ("product-version");
210210

211-
if (product_version is int version && version > productVersion)
211+
if (product_version is AndroidSdkVersion version && version > productVersion)
212212
return true;
213213

214214
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
2+
using System;
3+
using System.Diagnostics.CodeAnalysis;
4+
5+
namespace Java.Interop.Tools.Generator;
6+
7+
public struct AndroidSdkVersion : IComparable, IComparable<AndroidSdkVersion>, IEquatable<AndroidSdkVersion>
8+
{
9+
public int ApiLevel { get; private set; }
10+
public int MinorRelease { get; private set; }
11+
12+
public AndroidSdkVersion (int major, int minor = 0)
13+
{
14+
ApiLevel = major;
15+
MinorRelease = minor;
16+
}
17+
18+
int IComparable.CompareTo (object? value)
19+
{
20+
var other = value as AndroidSdkVersion?;
21+
if (other == null) {
22+
return 1;
23+
}
24+
return CompareTo (other.Value);
25+
}
26+
27+
public int CompareTo (AndroidSdkVersion value)
28+
{
29+
int r = ApiLevel.CompareTo (value.ApiLevel);
30+
if (r == 0) {
31+
r = MinorRelease.CompareTo (value.MinorRelease);
32+
}
33+
return r;
34+
}
35+
36+
public override int GetHashCode ()
37+
=> ApiLevel ^ MinorRelease;
38+
39+
public override bool Equals (object? value)
40+
{
41+
var other = value as AndroidSdkVersion?;
42+
if (other == null) {
43+
return false;
44+
}
45+
return Equals (other.Value);
46+
}
47+
48+
public bool Equals (AndroidSdkVersion value)
49+
{
50+
return value.ApiLevel == ApiLevel && value.MinorRelease == MinorRelease;
51+
}
52+
53+
public override string ToString ()
54+
=> MinorRelease == 0
55+
? ApiLevel.ToString ()
56+
: $"{ApiLevel}.{MinorRelease}";
57+
58+
// public static implicit operator ApiLevel (int value)
59+
// => new ApiLevel (value);
60+
61+
public static bool operator < (AndroidSdkVersion lhs, AndroidSdkVersion rhs)
62+
=> lhs.CompareTo (rhs) < 0;
63+
public static bool operator <= (AndroidSdkVersion lhs, AndroidSdkVersion rhs)
64+
=> lhs.CompareTo (rhs) <= 0;
65+
public static bool operator > (AndroidSdkVersion lhs, AndroidSdkVersion rhs)
66+
=> lhs.CompareTo (rhs) > 0;
67+
public static bool operator >= (AndroidSdkVersion lhs, AndroidSdkVersion rhs)
68+
=> lhs.CompareTo (rhs) >= 0;
69+
public static bool operator == (AndroidSdkVersion lhs, AndroidSdkVersion rhs)
70+
=> lhs.Equals (rhs);
71+
public static bool operator != (AndroidSdkVersion lhs, AndroidSdkVersion rhs)
72+
=> !lhs.Equals (rhs);
73+
74+
public static bool operator < (AndroidSdkVersion lhs, int rhs)
75+
=> lhs.ApiLevel.CompareTo (rhs) < 0;
76+
public static bool operator <= (AndroidSdkVersion lhs, int rhs)
77+
=> lhs.ApiLevel.CompareTo (rhs) <= 0;
78+
public static bool operator > (AndroidSdkVersion lhs, int rhs)
79+
=> lhs.ApiLevel.CompareTo (rhs) > 0;
80+
public static bool operator >= (AndroidSdkVersion lhs, int rhs)
81+
=> lhs.ApiLevel.CompareTo (rhs) >= 0;
82+
public static bool operator == (AndroidSdkVersion lhs, int rhs)
83+
=> lhs.ApiLevel.Equals (rhs);
84+
public static bool operator != (AndroidSdkVersion lhs, int rhs)
85+
=> !lhs.ApiLevel.Equals (rhs);
86+
87+
public static bool operator < (int lhs, AndroidSdkVersion rhs)
88+
=> lhs.CompareTo (rhs.ApiLevel) < 0;
89+
public static bool operator <= (int lhs, AndroidSdkVersion rhs)
90+
=> lhs.CompareTo (rhs.ApiLevel) <= 0;
91+
public static bool operator > (int lhs, AndroidSdkVersion rhs)
92+
=> lhs.CompareTo (rhs.ApiLevel) > 0;
93+
public static bool operator >= (int lhs, AndroidSdkVersion rhs)
94+
=> lhs.CompareTo (rhs.ApiLevel) >= 0;
95+
public static bool operator == (int lhs, AndroidSdkVersion rhs)
96+
=> lhs.Equals (rhs.ApiLevel);
97+
public static bool operator != (int lhs, AndroidSdkVersion rhs)
98+
=> !lhs.Equals (rhs.ApiLevel);
99+
100+
public static bool TryParse (string? value, out AndroidSdkVersion apiLevel)
101+
{
102+
if (value == null) {
103+
apiLevel = default;
104+
return false;
105+
}
106+
if (Version.TryParse (value, out var v)) {
107+
apiLevel = new AndroidSdkVersion (v.Major, v.Minor);
108+
return true;
109+
}
110+
if (int.TryParse (value, out var major)) {
111+
apiLevel = new AndroidSdkVersion (major);
112+
return true;
113+
}
114+
apiLevel = default;
115+
return false;
116+
}
117+
118+
public static AndroidSdkVersion Parse (string? value)
119+
{
120+
AndroidSdkVersion v;
121+
if (TryParse (value, out v)) {
122+
return v;
123+
}
124+
throw new NotSupportedException ($"Could not parse `{value}` as an ApiLevel.");
125+
}
126+
}

src/Java.Interop.Tools.Generator/Utilities/CsvParser.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,16 @@ public string GetField (int index)
1919
return fields [index].Trim ();
2020
}
2121

22-
public int GetFieldAsInt (int index)
22+
public AndroidSdkVersion GetFieldAsAndroidSdkVersion (int index)
2323
{
24-
return int.Parse (GetField (index));
24+
return AndroidSdkVersion.Parse (GetField (index));
2525
}
2626

27-
public int? GetFieldAsNullableInt32 (int index)
27+
public AndroidSdkVersion? GetFieldAsNullableAndroidSdkVersion (int index)
2828
{
2929
var value = GetField (index);
3030

31-
if (int.TryParse (value, out var val))
31+
if (AndroidSdkVersion.TryParse (value, out var val))
3232
return val;
3333

3434
return default;

src/Java.Interop.Tools.Generator/Utilities/NamingConverter.cs

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,49 @@ public static class NamingConverter
99
/// <summary>
1010
/// Converts a 'merge.SourceFile' attribute to an API level. (ex. "..\..\bin\BuildDebug\api\api-28.xml.in")
1111
/// </summary>
12-
public static int ParseApiLevel (string? value)
12+
public static AndroidSdkVersion ParseApiLevel (string? value)
13+
{
14+
var result = ExtractApiLevel (value);
15+
if (!result.HasValue ())
16+
return default;
17+
18+
return result switch {
19+
"R" => new AndroidSdkVersion (30),
20+
"S" => new AndroidSdkVersion (31),
21+
_ => AndroidSdkVersion.Parse (result)
22+
};
23+
}
24+
25+
static string? ExtractApiLevel (string? value)
1326
{
1427
if (!value.HasValue ())
15-
return 0;
28+
return null;
1629

17-
var hyphen = value.IndexOf ('-');
18-
var period = value.IndexOf ('.', hyphen);
30+
var hyphen = value.IndexOf ('-');
31+
if (hyphen < 0 || (hyphen+1) >= value.Length)
32+
return null;
1933

20-
var result = value.Substring (hyphen + 1, period - hyphen - 1);
34+
int end = hyphen + 1;
35+
if (char.IsAsciiDigit (value [end++])) {
36+
for ( ; end < value.Length; ++end) {
37+
var n = value [end + 1];
38+
if (!char.IsAsciiDigit (n) && n != '.')
39+
break;
40+
}
41+
} else {
42+
// codename; expect ALLCAPS
43+
for ( ; end < value.Length; ++end) {
44+
if (!char.IsAsciiLetterUpper (value [end]))
45+
break;
46+
}
47+
}
2148

22-
return result switch {
23-
"R" => 30,
24-
"S" => 31,
25-
_ => int.Parse (result)
26-
};
49+
return value.Substring (hyphen + 1, end - hyphen - 1);
2750
}
2851

2952
// The 'merge.SourceFile' attribute may be on the element, or only on its parent. For example,
3053
// a new 'class' added will only put the attribute on the '<class>' element and not its children <method>s.
31-
public static int ParseApiLevel (XElement element)
54+
public static AndroidSdkVersion ParseApiLevel (XElement element)
3255
{
3356
var loop = element;
3457

@@ -39,7 +62,7 @@ public static int ParseApiLevel (XElement element)
3962
loop = loop.Parent;
4063
}
4164

42-
return 0;
65+
return default;
4366
}
4467

4568
public static string ConvertNamespaceToCSharp (string v)

src/utils/XmlExtensions.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
using System.Xml.Linq;
66
using System.Xml.XPath;
77

8+
using Java.Interop.Tools.Generator;
9+
810
namespace Xamarin.Android.Tools {
911

1012
static class XmlExtensions {
@@ -21,14 +23,14 @@ public static string XGetAttribute (this XPathNavigator nav, string name, string
2123
return attr != null ? attr.Trim () : null;
2224
}
2325

24-
public static int? XGetAttributeAsIntOrNull (this XElement element, string name)
26+
public static AndroidSdkVersion? XGetAttributeAsAndroidSdkVersionOrNull (this XElement element, string name)
2527
{
2628
var attr = element.Attribute (name);
2729

2830
if (attr?.Value is null)
2931
return null;
3032

31-
if (int.TryParse (attr.Value, out var val))
33+
if (AndroidSdkVersion.TryParse (attr.Value, out var val))
3234
return val;
3335

3436
return null;

0 commit comments

Comments
 (0)