|
| 1 | +--- |
| 2 | +title: "Callbacks on Android" |
| 3 | +ms.prod: xamarin |
| 4 | +ms.assetid: F3A7A4E6-41FE-4F12-949C-96090815C5D6 |
| 5 | +author: davidortinau |
| 6 | +ms.author: daortin |
| 7 | +ms.date: 11/14/2017 |
| 8 | +--- |
| 9 | + |
| 10 | +# Callbacks on Android |
| 11 | + |
| 12 | +Calling to Java from C# is somewhat a *risky business*. That is to say there is a *pattern* for callbacks from C# to Java; however, it is more complicated than we would like. |
| 13 | + |
| 14 | +We'll cover the three options for doing callbacks that make the most sense for Java: |
| 15 | + |
| 16 | +- Abstract classes |
| 17 | +- Interfaces |
| 18 | +- Virtual methods |
| 19 | + |
| 20 | +## Abstract Classes |
| 21 | + |
| 22 | +This is the easiest route for callbacks, so I would recommend using `abstract` if you are just trying to get a callback working in the simplest form. |
| 23 | + |
| 24 | +Let's start with a C# class we would like Java to implement: |
| 25 | + |
| 26 | +```csharp |
| 27 | +[Register("mono.embeddinator.android.AbstractClass")] |
| 28 | +public abstract class AbstractClass : Java.Lang.Object |
| 29 | +{ |
| 30 | + public AbstractClass() { } |
| 31 | + |
| 32 | + public AbstractClass(IntPtr handle, JniHandleOwnership transfer) : base(handle, transfer) { } |
| 33 | + |
| 34 | + [Export("getText")] |
| 35 | + public abstract string GetText(); |
| 36 | +} |
| 37 | +``` |
| 38 | + |
| 39 | +Here are the details to make this work: |
| 40 | + |
| 41 | +- `[Register]` generates a nice package name in Java--you will get an auto-generated package name without it. |
| 42 | +- Subclassing `Java.Lang.Object` signals to .NET Embedding to run the class through Xamarin.Android's Java generator. |
| 43 | +- Empty constructor: is what you will want to use from Java code. |
| 44 | +- `(IntPtr, JniHandleOwnership)` constructor: is what Xamarin.Android will use for creating the C#-equivalent of Java objects. |
| 45 | +- `[Export]` signals Xamarin.Android to expose the method to Java. We can also change the method name, since the Java world likes to use lower case methods. |
| 46 | + |
| 47 | +Next let's make a C# method to test the scenario: |
| 48 | + |
| 49 | +```csharp |
| 50 | +[Register("mono.embeddinator.android.JavaCallbacks")] |
| 51 | +public class JavaCallbacks : Java.Lang.Object |
| 52 | +{ |
| 53 | + [Export("abstractCallback")] |
| 54 | + public static string AbstractCallback(AbstractClass callback) |
| 55 | + { |
| 56 | + return callback.GetText(); |
| 57 | + } |
| 58 | +} |
| 59 | +``` |
| 60 | + |
| 61 | +`JavaCallbacks` could be any class to test this, as long as it is a `Java.Lang.Object`. |
| 62 | + |
| 63 | +Now, run .NET Embedding on your .NET assembly to generate an AAR. See the [Getting Started guide](~/tools/dotnet-embedding/get-started/java/android.md) for details. |
| 64 | + |
| 65 | +After importing the AAR file into Android Studio, let's write a unit test: |
| 66 | + |
| 67 | +```java |
| 68 | +@Test |
| 69 | +public void abstractCallback() throws Throwable { |
| 70 | + AbstractClass callback = new AbstractClass() { |
| 71 | + @Override |
| 72 | + public String getText() { |
| 73 | + return "Java"; |
| 74 | + } |
| 75 | + }; |
| 76 | + |
| 77 | + assertEquals("Java", callback.getText()); |
| 78 | + assertEquals("Java", JavaCallbacks.abstractCallback(callback)); |
| 79 | +} |
| 80 | +``` |
| 81 | + |
| 82 | +So we: |
| 83 | + |
| 84 | +- Implemented the `AbstractClass` in Java with an anonymous type |
| 85 | +- Made sure our instance returns `"Java"` from Java |
| 86 | +- Made sure our instance returns `"Java"` from C# |
| 87 | +- Added `throws Throwable`, since C# constructors are currently marked with `throws` |
| 88 | + |
| 89 | +If we ran this unit test as-is, it would fail with an error such as: |
| 90 | + |
| 91 | +```csharp |
| 92 | +System.NotSupportedException: Unable to find Invoker for type 'Android.AbstractClass'. Was it linked away? |
| 93 | +``` |
| 94 | + |
| 95 | +What is missing here is an `Invoker` type. This is a subclass of `AbstractClass` that forwards C# calls to Java. If a Java object enters the C# world and the equivalent C# type is abstract, then Xamarin.Android automatically looks for a C# type with the suffix `Invoker` for use within C# code. |
| 96 | + |
| 97 | +Xamarin.Android uses this `Invoker` pattern for Java binding projects among other things. |
| 98 | + |
| 99 | +Here is our implementation of `AbstractClassInvoker`: |
| 100 | + |
| 101 | +```csharp |
| 102 | +class AbstractClassInvoker : AbstractClass |
| 103 | +{ |
| 104 | + IntPtr class_ref, id_gettext; |
| 105 | + |
| 106 | + public AbstractClassInvoker(IntPtr handle, JniHandleOwnership transfer) : base(handle, transfer) |
| 107 | + { |
| 108 | + IntPtr lref = JNIEnv.GetObjectClass(Handle); |
| 109 | + class_ref = JNIEnv.NewGlobalRef(lref); |
| 110 | + JNIEnv.DeleteLocalRef(lref); |
| 111 | + } |
| 112 | + |
| 113 | + protected override Type ThresholdType |
| 114 | + { |
| 115 | + get { return typeof(AbstractClassInvoker); } |
| 116 | + } |
| 117 | + |
| 118 | + protected override IntPtr ThresholdClass |
| 119 | + { |
| 120 | + get { return class_ref; } |
| 121 | + } |
| 122 | + |
| 123 | + public override string GetText() |
| 124 | + { |
| 125 | + if (id_gettext == IntPtr.Zero) |
| 126 | + id_gettext = JNIEnv.GetMethodID(class_ref, "getText", "()Ljava/lang/String;"); |
| 127 | + IntPtr lref = JNIEnv.CallObjectMethod(Handle, id_gettext); |
| 128 | + return GetObject<Java.Lang.String>(lref, JniHandleOwnership.TransferLocalRef)?.ToString(); |
| 129 | + } |
| 130 | + |
| 131 | + protected override void Dispose(bool disposing) |
| 132 | + { |
| 133 | + if (class_ref != IntPtr.Zero) |
| 134 | + JNIEnv.DeleteGlobalRef(class_ref); |
| 135 | + class_ref = IntPtr.Zero; |
| 136 | + |
| 137 | + base.Dispose(disposing); |
| 138 | + } |
| 139 | +} |
| 140 | +``` |
| 141 | + |
| 142 | +There is quite a bit going on here, we: |
| 143 | + |
| 144 | +- Added a class with the suffix `Invoker` that subclasses `AbstractClass` |
| 145 | +- Added `class_ref` to hold the JNI reference to the Java class that subclasses our C# class |
| 146 | +- Added `id_gettext` to hold the JNI reference to the Java `getText` method |
| 147 | +- Included a `(IntPtr, JniHandleOwnership)` constructor |
| 148 | +- Implemented `ThresholdType` and `ThresholdClass` as a requirement for Xamarin.Android to know details about the `Invoker` |
| 149 | +- `GetText` needed to lookup the Java `getText` method with the appropriate JNI signature and call it |
| 150 | +- `Dispose` is just needed to clear the reference to `class_ref` |
| 151 | + |
| 152 | +After adding this class and generating a new AAR, our unit test passes. As you can see this pattern for callbacks is not *ideal*, but doable. |
| 153 | + |
| 154 | +For details on Java interop, see the amazing [Xamarin.Android documentation](~/android/platform/java-integration/working-with-jni.md) on this subject. |
| 155 | + |
| 156 | +## Interfaces |
| 157 | + |
| 158 | +Interfaces are much the same as abstract classes, except for one detail: Xamarin.Android does not generate Java for them. This is because before .NET Embedding, there are not many scenarios where Java would implement a C# interface. |
| 159 | + |
| 160 | +Let's say we have the following C# interface: |
| 161 | + |
| 162 | +```csharp |
| 163 | +[Register("mono.embeddinator.android.IJavaCallback")] |
| 164 | +public interface IJavaCallback : IJavaObject |
| 165 | +{ |
| 166 | + [Export("send")] |
| 167 | + void Send(string text); |
| 168 | +} |
| 169 | +``` |
| 170 | + |
| 171 | +`IJavaObject` signals to .NET Embedding that this is a Xamarin.Android interface, but otherwise this is exactly the same as an `abstract` class. |
| 172 | + |
| 173 | +Since Xamarin.Android will not currently generate the Java code for this interface, add the following Java to your C# project: |
| 174 | + |
| 175 | +```java |
| 176 | +package mono.embeddinator.android; |
| 177 | + |
| 178 | +public interface IJavaCallback { |
| 179 | + void send(String text); |
| 180 | +} |
| 181 | +``` |
| 182 | + |
| 183 | +You can place the file anywhere, but make sure to set its build action to `AndroidJavaSource`. This will signal .NET Embedding to copy it to the proper directory to get compiled into your AAR file. |
| 184 | + |
| 185 | +Next, the `Invoker` implementation will be quite the same: |
| 186 | + |
| 187 | +```csharp |
| 188 | +class IJavaCallbackInvoker : Java.Lang.Object, IJavaCallback |
| 189 | +{ |
| 190 | + IntPtr class_ref, id_send; |
| 191 | + |
| 192 | + public IJavaCallbackInvoker(IntPtr handle, JniHandleOwnership transfer) : base(handle, transfer) |
| 193 | + { |
| 194 | + IntPtr lref = JNIEnv.GetObjectClass(Handle); |
| 195 | + class_ref = JNIEnv.NewGlobalRef(lref); |
| 196 | + JNIEnv.DeleteLocalRef(lref); |
| 197 | + } |
| 198 | + |
| 199 | + protected override Type ThresholdType |
| 200 | + { |
| 201 | + get { return typeof(IJavaCallbackInvoker); } |
| 202 | + } |
| 203 | + |
| 204 | + protected override IntPtr ThresholdClass |
| 205 | + { |
| 206 | + get { return class_ref; } |
| 207 | + } |
| 208 | + |
| 209 | + public void Send(string text) |
| 210 | + { |
| 211 | + if (id_send == IntPtr.Zero) |
| 212 | + id_send = JNIEnv.GetMethodID(class_ref, "send", "(Ljava/lang/String;)V"); |
| 213 | + JNIEnv.CallVoidMethod(Handle, id_send, new JValue(new Java.Lang.String(text))); |
| 214 | + } |
| 215 | + |
| 216 | + protected override void Dispose(bool disposing) |
| 217 | + { |
| 218 | + if (class_ref != IntPtr.Zero) |
| 219 | + JNIEnv.DeleteGlobalRef(class_ref); |
| 220 | + class_ref = IntPtr.Zero; |
| 221 | + |
| 222 | + base.Dispose(disposing); |
| 223 | + } |
| 224 | +} |
| 225 | +``` |
| 226 | + |
| 227 | +After generating an AAR file, in Android Studio we could write the following passing unit test: |
| 228 | + |
| 229 | +```java |
| 230 | +class ConcreteCallback implements IJavaCallback { |
| 231 | + public String text; |
| 232 | + @Override |
| 233 | + public void send(String text) { |
| 234 | + this.text = text; |
| 235 | + } |
| 236 | +} |
| 237 | + |
| 238 | +@Test |
| 239 | +public void interfaceCallback() { |
| 240 | + ConcreteCallback callback = new ConcreteCallback(); |
| 241 | + JavaCallbacks.interfaceCallback(callback, "Java"); |
| 242 | + assertEquals("Java", callback.text); |
| 243 | +} |
| 244 | +``` |
| 245 | + |
| 246 | +## Virtual Methods |
| 247 | + |
| 248 | +Overriding a `virtual` in Java is possible, but not a great experience. |
| 249 | + |
| 250 | +Let's assume you have the following C# class: |
| 251 | + |
| 252 | +```csharp |
| 253 | +[Register("mono.embeddinator.android.VirtualClass")] |
| 254 | +public class VirtualClass : Java.Lang.Object |
| 255 | +{ |
| 256 | + public VirtualClass() { } |
| 257 | + |
| 258 | + public VirtualClass(IntPtr handle, JniHandleOwnership transfer) : base(handle, transfer) { } |
| 259 | + |
| 260 | + [Export("getText")] |
| 261 | + public virtual string GetText() { return "C#"; } |
| 262 | +} |
| 263 | +``` |
| 264 | + |
| 265 | +If you followed the `abstract` class example above, it would work except for one detail: _Xamarin.Android won't lookup the `Invoker`_. |
| 266 | + |
| 267 | +To fix this, modify the C# class to be `abstract`: |
| 268 | + |
| 269 | +```csharp |
| 270 | +public abstract class VirtualClass : Java.Lang.Object |
| 271 | +``` |
| 272 | + |
| 273 | +This is not ideal, but it gets this scenario working. Xamarin.Android will pick up the `VirtualClassInvoker` and Java can use `@Override` on the method. |
| 274 | + |
| 275 | +## Callbacks in the Future |
| 276 | + |
| 277 | +There are a couple of things we could to do improve these scenarios: |
| 278 | + |
| 279 | +1. `throws Throwable` on C# constructors is fixed on this [PR](https://github.com/xamarin/java.interop/pull/170). |
| 280 | +1. Make the Java generator in Xamarin.Android support interfaces. |
| 281 | + - This removes the need for adding Java source file with a build action of `AndroidJavaSource`. |
| 282 | +1. Make a way for Xamarin.Android to load an `Invoker` for virtual classes. |
| 283 | + - This removes the need to mark the class in our `virtual` example `abstract`. |
| 284 | +1. Generate `Invoker` classes for .NET Embedding automatically |
| 285 | + - This is going to be complicated, but doable. Xamarin.Android is already doing something similar to this for Java binding projects. |
| 286 | + |
| 287 | +There is a lot of work to be done here, but these enhancements to .NET Embedding are possible. |
| 288 | + |
| 289 | +## Further Reading |
| 290 | + |
| 291 | +- [Getting Started on Android](~/tools/dotnet-embedding/get-started/java/android.md) |
| 292 | +- [Preliminary Android Research](~/tools/dotnet-embedding/android/index.md) |
| 293 | +- [.NET Embedding Limitations](~/tools/dotnet-embedding/limitations.md) |
| 294 | +- [Error codes and descriptions](~/tools/dotnet-embedding/errors.md) |
0 commit comments