Skip to content

Commit 1f4c8be

Browse files
authored
[Mono.Android] API-compatible CellInfo.CellIdentity (#4391)
Add `Documentation/workflow/mono-android-api-compatibility.md`, which describes the scenarios we need to be familiar with regarding Java and C# API compatibility differences and how to deal with them, and update `Mono.Android.dll` for API-R so that e.g. `Android.Telephony.CellInfoGsm.CellIdentity` doesn't change types vs. API-29 (the previous version).
1 parent 27c59c8 commit 1f4c8be

File tree

5 files changed

+359
-26
lines changed

5 files changed

+359
-26
lines changed
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
# `Mono.Android.dll` API Compatibility
2+
3+
[Historically](#history), there was a separate `Mono.Android.dll` for each
4+
`$(TargetFrameworkVersion)` value, and these assemblies were *not* completely
5+
API compatible with each other.
6+
7+
Starting with `$(TargetFrameworkVersion)` v10.0, we will no longer provide a
8+
separate `Mono.Android.dll` per API level. Instead, all *stable* bindings of
9+
future API levels will reside in the same `Mono.Android.dll`.
10+
11+
This "single" `Mono.Android.dll` will require C# 8 features.
12+
13+
14+
# Preserving Compatibility
15+
16+
Java and C# have different ideas about what constitutes a change which is
17+
source- and binary-compatible. Consequently, the `Mono.Android.dll` binding
18+
needs to do extra work to bridge these gaps.
19+
20+
21+
## Added Required Interface Methods
22+
23+
It is an *binary* compatible change to add non-`default` methods to a Java
24+
interface. `Mono.Android.dll` will support this by using C#8 default
25+
interface methods which try to invoke the underlying Java method or
26+
throw `Java.Lang.AbstractMethodError()`.
27+
28+
For example, given Java API-*X*:
29+
30+
```java
31+
package example;
32+
33+
public interface Fooable {
34+
void foo();
35+
}
36+
```
37+
38+
which is then updated for API-*Y*:
39+
40+
```java
41+
package example;
42+
43+
public interface Fooable {
44+
void foo();
45+
void bar();
46+
}
47+
```
48+
49+
Then the API-*Y* binding will be:
50+
51+
```csharp
52+
namespace Example {
53+
public partial interface IFooable : IJavaObject, IJavaPeerable {
54+
[Register ("foo", "()V", "")]
55+
void Foo();
56+
57+
[Register ("bar", "()V", "")]
58+
void Bar()
59+
{
60+
const string __id = "bar.()V";
61+
try {
62+
var __rm = _members.InstanceMethods.InvokeVirtualVoidMethod (__id, this, null);
63+
}
64+
catch (Java.Lang.NoSuchMethodError e) {
65+
throw new Java.Lang.AbstractMethodError (__id);
66+
}
67+
}
68+
}
69+
}
70+
```
71+
72+
Question: will checking for `NoSuchMethodError` actually work with
73+
new required interface methods?
74+
75+
76+
<a name="added-abstract-methods">
77+
78+
## Added Abstract Methods
79+
80+
When a Java class adds a new `abstract` method, the binding needs to alter
81+
the method to instead be `virtual`, with an implementation which eventually
82+
throws `AbstractMethodError`:
83+
84+
```java
85+
package example;
86+
87+
public abstract class Foo {
88+
// Abstract in API-X
89+
public abstract void a();
90+
91+
// Added in API-Y
92+
public abstract void b();
93+
}
94+
```
95+
96+
will be bound as:
97+
98+
```csharp
99+
namespace Example {
100+
public abstract partial class Foo {
101+
public abstract void A();
102+
103+
public virtual void B()
104+
{
105+
const string __id = "b.()V";
106+
try {
107+
// Works on API levels which (1) declare the method, and
108+
// (2) the method is overridden in the runtime type of `this`.
109+
var __rm = _members.InstanceMethods.InvokeVirtuaVoidMethod (__id, this, null);
110+
}
111+
catch (Java.Lang.NoSuchMethodError e) {
112+
// Triggered on API levels which don't contain the method
113+
throw new Java.Lang.AbstractMethodError (__id);
114+
}
115+
}
116+
}
117+
}
118+
```
119+
120+
121+
## Added Covariant Return Types
122+
123+
When a base class adds a method which is overridden in a derived class,
124+
Java may make use of covariant return types. For example, in API-29
125+
the [`android.telephony.CellInfo`][cellinfo] and
126+
[`android.telephony.CellInfoCdma`][celllinfocdma] types:
127+
128+
[cellinfo]: https://developer.android.com/reference/android/telephony/CellInfo
129+
[celllinfocdma]: https://developer.android.com/reference/android/telephony/CellInfoCdma
130+
131+
```java
132+
package android.telephony;
133+
134+
public abstract class CellInfo {
135+
}
136+
137+
public class CellInfoCdma extends CellInfo {
138+
public CellIdentityCdma getCellIdentity() {…}
139+
}
140+
141+
public abstract class CellIdentity {…}
142+
public class CellIdentityCdma extends CellIdentity {…}
143+
```
144+
145+
is bound as:
146+
147+
```csharp
148+
namespace Android.Telephony {
149+
public abstract partial class CellInfo {
150+
}
151+
152+
public sealed partial class CellInfoCdma : CellInfo {
153+
public CellIdentityCdma CellIdentity {get;}
154+
}
155+
156+
public abstract partial class CellIdentity {}
157+
public partial class CellIdentityCdma : CellIdentity {}
158+
}
159+
```
160+
161+
In [API-R Developer Preview 1][cellinfo-r-dp1], `CellInfo` is updated:
162+
163+
[cellinfo-r-dp1]: https://developer.android.com/sdk/api_diff/r-dp1/changes/android.telephony.CellInfo
164+
165+
```java
166+
package android.telephony;
167+
168+
public class CellInfo {
169+
public abstract CellIdentity getCellIdentity();
170+
}
171+
```
172+
173+
Note that this is a new `abstract` method. As per the
174+
[Added Abstract Methods](#added-abstract-methods) section,
175+
`CellInfo.getCellIdentity()` will need to be bound as a *`virtual`* property:
176+
177+
```csharp
178+
namespace Android.Telephony {
179+
// API-R binding, take 1
180+
public abstract partial class CellInfo {
181+
public virtual CellIdentity CellIdentity {
182+
get => …;
183+
}
184+
}
185+
}
186+
```
187+
188+
However, this is incomplete, for two reasons:
189+
190+
1. `CellInfoCdma.CellIdentity` will issue a CS0114 warning, as it hides the
191+
added `CellInfo.CellIdentity` property. Note that we cannot change the
192+
type of `CellInfoCdma.CellIdentity`, as that would break the C# API.
193+
194+
2. At runtime, the expectation is that invoking the `CellInfo.CellIdentity`
195+
property on a `CellInfoCdma` instance *won't* throw `AbstractMethodError`,
196+
as it is "overridden" in the Java-side `CellInfoCdma`.
197+
198+
(2) is implicitly handled, if you squint "just right", by having the binding
199+
of `CellInfo.CellIdentity` invoke the Java-side method. If the runtime type
200+
is e.g. `CellInfoCdma`, then this will hit `CellInfoCdma.getCellIdentity()`,
201+
which will return the same instance as the `CellInfo.CellIdentity` property.
202+
203+
```csharp
204+
namespace Android.Telephony {
205+
// API-R binding, final
206+
public abstract partial class CellInfo {
207+
public virtual CellIdentity CellIdentity {
208+
get {
209+
const string __id = "getCellIdentity.()Landroid/telephony/CellIdentity;";
210+
try {
211+
var __rm = _members.InstanceMethods.InvokeVirtualObjectMethod (__id, this, null);
212+
return global::Java.Lang.Object.GetObject<Android.Telephony.CellIdentity> (__rm.Handle, JniHandleOwnership.TransferLocalRef);
213+
}
214+
catch (Java.Lang.NoSuchMethodError e) {
215+
throw new Java.Lang.AbstractMethodError (__id);
216+
}
217+
}
218+
}
219+
}
220+
221+
public sealed partial class CellInfoCdma : CellInfo {
222+
public new CellIdentityCdma CellIdentity {get;}
223+
}
224+
}
225+
```
226+
227+
228+
<a name="history">
229+
230+
# History
231+
232+
Java and C# have different ideas about what constitutes a change which is
233+
source- and binary-compatible. In Java, it is ABI compatible to add new
234+
required methods to an interface and to abstract classes. This is not
235+
*source* compatible -- the newly required methods will need to be added when
236+
recompiling code -- but it's *binary* compatible, and this is something that
237+
Android has frequently made use of.
238+
239+
Consider the [`android.database.Cursor`][cursor] interface, which changed in:
240+
241+
* [API-19][cursor-api-19], adding [`Cursor.getNotificationUri()`][cgnu].
242+
* [API-29][cursor-api-29], adding [`Cursor.getNotificationUris()`][cgnus]
243+
and [`Cursor.setNotificationUris()][csnus].
244+
245+
The `Cursor` interface is bound as [`Android.Database.ICursor][icursor], and
246+
before C# 8 it was not possible to add members to a C# interface.
247+
248+
How were these new Java members supported?
249+
250+
Such changes were supported by *breaking* API, and using a *new*
251+
`$(TargetFrameworkVersion)` to contain the updated API. `Mono.Android.dll`
252+
from v4.3 (API-18) would not contain a binding for
253+
`Cursor.getNotificationUri()`, while the binding in v9.0 (API-28) would not
254+
contain bindings for `Cursor.getNotificationUris()` and
255+
`Cursor.setNotificationUris()`.
256+
257+
*So long as* the `$(TargetFrameworkVersion)` value didn't change, source code
258+
would continue to compile without any errors. Changing the
259+
`$(TargetFrameworkVersion)` value *may* result in new compiler errors due to
260+
added members not being implemented, and we considered this acceptable.
261+
262+
(The alternative was to bind *no* new methods after the initial binding, which
263+
we didn't consider acceptable.)
264+
265+
Binary compatibility -- using a library built against an older binding with
266+
an app using a newer binding -- was preserved by using a [linker step][linker-fix]
267+
which
268+
would look for "missing" abstract methods and insert them so that they would
269+
throw `AbstractMethodError`.
270+
271+
272+
[cursor]: https://developer.android.com/reference/android/database/Cursor
273+
[cursor-api-19]: https://developer.android.com/sdk/api_diff/19/changes/android.database.Cursor
274+
[cursor-api-29]: https://developer.android.com/sdk/api_diff/29/changes/android.database.Cursor
275+
[cgnu]: https://developer.android.com/reference/android/database/Cursor.html#getNotificationUri()
276+
[cgnus]: https://developer.android.com/reference/android/database/Cursor.html#getNotificationUris()
277+
[csnus]: https://developer.android.com/reference/android/database/Cursor.html#setNotificationUris(android.content.ContentResolver,%20java.util.List%3Candroid.net.Uri%3E)
278+
[icursor]: https://docs.microsoft.com/en-us/dotnet/api/android.database.icursor?view=xamarin-android-sdk-9
279+
[linker-fix]: https://github.com/xamarin/xamarin-android/commit/f96fcf93e157472072576bcc0a8698302899e8cf
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Android.Runtime;
4+
using Java.Interop;
5+
6+
#if ANDROID_30
7+
8+
namespace Android.Telephony {
9+
10+
public partial class CellInfo {
11+
12+
static Delegate cb_getCellIdentity;
13+
#pragma warning disable 0169
14+
static Delegate GetGetCellIdentityHandler ()
15+
{
16+
if (cb_getCellIdentity == null)
17+
cb_getCellIdentity = JNINativeWrapper.CreateDelegate ((Func<IntPtr, IntPtr, IntPtr>) n_GetCellIdentity);
18+
return cb_getCellIdentity;
19+
}
20+
21+
static IntPtr n_GetCellIdentity (IntPtr jnienv, IntPtr native__this)
22+
{
23+
Android.Telephony.CellInfo __this = global::Java.Lang.Object.GetObject<Android.Telephony.CellInfo> (jnienv, native__this, JniHandleOwnership.DoNotTransfer);
24+
return JNIEnv.ToLocalJniHandle (__this.CellIdentity);
25+
}
26+
#pragma warning restore 0169
27+
28+
public unsafe virtual Android.Telephony.CellIdentity CellIdentity {
29+
// Metadata.xml XPath method reference: path="/api/package[@name='android.telephony']/class[@name='CellInfo']/method[@name='getCellIdentity' and count(parameter)=0]"
30+
[Register ("getCellIdentity", "()Landroid/telephony/CellIdentity;", "GetGetCellIdentityHandler", ApiSince = 30)]
31+
get {
32+
const string __id = "getCellIdentity.()Landroid/telephony/CellIdentity;";
33+
try {
34+
var __rm = _members.InstanceMethods.InvokeVirtualObjectMethod (__id, this, null);
35+
return global::Java.Lang.Object.GetObject<Android.Telephony.CellIdentity> (__rm.Handle, JniHandleOwnership.TransferLocalRef);
36+
}
37+
catch (Java.Lang.NoSuchMethodError) {
38+
throw new Java.Lang.AbstractMethodError (__id);
39+
}
40+
}
41+
}
42+
43+
static Delegate cb_getCellSignalStrength;
44+
#pragma warning disable 0169
45+
static Delegate GetGetCellSignalStrengthHandler ()
46+
{
47+
if (cb_getCellSignalStrength == null)
48+
cb_getCellSignalStrength = JNINativeWrapper.CreateDelegate ((Func<IntPtr, IntPtr, IntPtr>) n_GetCellSignalStrength);
49+
return cb_getCellSignalStrength;
50+
}
51+
52+
static IntPtr n_GetCellSignalStrength (IntPtr jnienv, IntPtr native__this)
53+
{
54+
Android.Telephony.CellInfo __this = global::Java.Lang.Object.GetObject<Android.Telephony.CellInfo> (jnienv, native__this, JniHandleOwnership.DoNotTransfer);
55+
return JNIEnv.ToLocalJniHandle (__this.CellSignalStrength);
56+
}
57+
#pragma warning restore 0169
58+
59+
public unsafe virtual Android.Telephony.CellSignalStrength CellSignalStrength {
60+
// Metadata.xml XPath method reference: path="/api/package[@name='android.telephony']/class[@name='CellInfo']/method[@name='getCellSignalStrength' and count(parameter)=0]"
61+
[Register ("getCellSignalStrength", "()Landroid/telephony/CellSignalStrength;", "GetGetCellSignalStrengthHandler", ApiSince = 30)]
62+
get {
63+
const string __id = "getCellSignalStrength.()Landroid/telephony/CellSignalStrength;";
64+
try {
65+
var __rm = _members.InstanceMethods.InvokeVirtualObjectMethod (__id, this, null);
66+
return global::Java.Lang.Object.GetObject<Android.Telephony.CellSignalStrength> (__rm.Handle, JniHandleOwnership.TransferLocalRef);
67+
}
68+
catch (Java.Lang.NoSuchMethodError) {
69+
throw new Java.Lang.AbstractMethodError (__id);
70+
}
71+
}
72+
}
73+
}
74+
}
75+
76+
#endif // ANDROID_30

src/Mono.Android/Mono.Android.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@
237237
<Compile Include="Android.Runtime\XAPeerMembers.cs" />
238238
<Compile Include="Android.Runtime\XmlPullParserReader.cs" />
239239
<Compile Include="Android.Runtime\XmlReaderPullParser.cs" />
240+
<Compile Include="Android.Telephony\CellInfo.cs" />
240241
<Compile Include="Android.Telephony\PhoneNumberUtils.cs" />
241242
<Compile Include="Android.Telephony\TelephonyManager.cs" />
242243
<Compile Include="Android.Telephony.Mbms\IGroupCallCallback.cs" />

src/Mono.Android/metadata

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1538,17 +1538,9 @@
15381538
<!-- Namespace android.view.inline should be renamed to Android.Views.Inline -->
15391539
<attr api-since="30" path="/api/package[@name='android.view.inline']" name="managedName">Android.Views.Inline</attr>
15401540

1541-
<!-- Google has added getCellIdentity and getCellSignalStrength as abstract method on the base class -->
1542-
<attr api-since="30" path="/api/package[@name='android.telephony']/class[@name='CellInfoCdma']/method[@name='getCellIdentity' and count(parameter)=0]" name="managedReturn">Android.Telephony.CellIdentity</attr>
1543-
<attr api-since="30" path="/api/package[@name='android.telephony']/class[@name='CellInfoCdma']/method[@name='getCellSignalStrength' and count(parameter)=0]" name="managedReturn">Android.Telephony.CellSignalStrength</attr>
1544-
<attr api-since="30" path="/api/package[@name='android.telephony']/class[@name='CellInfoGsm']/method[@name='getCellIdentity' and count(parameter)=0]" name="managedReturn">Android.Telephony.CellIdentity</attr>
1545-
<attr api-since="30" path="/api/package[@name='android.telephony']/class[@name='CellInfoGsm']/method[@name='getCellSignalStrength' and count(parameter)=0]" name="managedReturn">Android.Telephony.CellSignalStrength</attr>
1546-
<attr api-since="30" path="/api/package[@name='android.telephony']/class[@name='CellInfoLte']/method[@name='getCellIdentity' and count(parameter)=0]" name="managedReturn">Android.Telephony.CellIdentity</attr>
1547-
<attr api-since="30" path="/api/package[@name='android.telephony']/class[@name='CellInfoLte']/method[@name='getCellSignalStrength' and count(parameter)=0]" name="managedReturn">Android.Telephony.CellSignalStrength</attr>
1548-
<attr api-since="30" path="/api/package[@name='android.telephony']/class[@name='CellInfoTdscdma']/method[@name='getCellIdentity' and count(parameter)=0]" name="managedReturn">Android.Telephony.CellIdentity</attr>
1549-
<attr api-since="30" path="/api/package[@name='android.telephony']/class[@name='CellInfoTdscdma']/method[@name='getCellSignalStrength' and count(parameter)=0]" name="managedReturn">Android.Telephony.CellSignalStrength</attr>
1550-
<attr api-since="30" path="/api/package[@name='android.telephony']/class[@name='CellInfoWcdma']/method[@name='getCellIdentity' and count(parameter)=0]" name="managedReturn">Android.Telephony.CellIdentity</attr>
1551-
<attr api-since="30" path="/api/package[@name='android.telephony']/class[@name='CellInfoWcdma']/method[@name='getCellSignalStrength' and count(parameter)=0]" name="managedReturn">Android.Telephony.CellSignalStrength</attr>
1541+
<!-- Google has added getCellIdentity and getCellSignalStrength as abstract method on the base class. Bind manually for API-compatibility. -->
1542+
<remove-node api-since="30" path="/api/package[@name='android.telephony']/class[@name='CellInfo']/method[@name='getCellIdentity' and count(parameter)=0]" />
1543+
<remove-node api-since="30" path="/api/package[@name='android.telephony']/class[@name='CellInfo']/method[@name='getCellSignalStrength' and count(parameter)=0]" />
15521544

15531545
<!-- We don't support methods that require Actions/Functions with more than 16 arguments -->
15541546
<remove-node api-since="30" path="/api/package[@name='java.util']/interface[@jni-signature='Ljava/util/Map;']/method[@name='of' and count(parameter)=16]" />

0 commit comments

Comments
 (0)