Skip to content

Commit 7a31216

Browse files
gugavarojonpryor
authored andcommitted
[generator] Emit events for addListener(Listener,Handler) pattern (#458)
When [generating events][0], given a Java `Listener` interface and an "add" method which takes the listener, `generator` will emit an event for each method on the listener interface, and all method parameters will become properties on a generated `EventArgs`. For example, consider [`android.view.TextureView`][1]: // Java public class TextureView { public interface SurfaceTextureListener { void onSurfaceTextureAvailable (SurfaceTexture surface, int width, int height); void onSurfaceTextureDestroyed (SurfaceTexture surface); void onSurfaceTextureChanged (SurfaceTexture surface, int width, int height); void onSurfaceTextureUpdated (SurfaceTexture surface); } public void setSurfaceTextureListener(SurfaceTextureListener listener); } `generator` will ["eventify" `TextureView` into][2]: // C# public partial class TextureView { public interface ISurfaceTextureListener { void OnSurfaceTextureAvailable (SurfaceTexture surface, int width, int height); void OnSurfaceTextureDestroyed (SurfaceTexture surface); void OnSurfaceTextureChanged (SurfaceTexture surface, int width, int height); void OnSurfaceTextureUpdated (SurfaceTexture surface); } public partial class SurfaceTextureAvailableEventArgs { public SurfaceTexture Surface {get;} public int Width {get;} pbulic int Height {get;} } public event EventHandler<SurfaceTextureAvailableEventArgs> SurfaceTextureAvailable; public partial class SurfaceTextureDestroyedEventArgs { public SurfaceTexture Surface {get;} } public event EventHandler<SurfaceTextureDestroyedEventArgs> SurfaceTextureDestroyed; public partial class SurfaceTextureChangedEventArgs { public SurfaceTexture Surface {get;} public int Width {get;} pbulic int Height {get;} } public event EventHandler<SurfaceTextureChangedEventArgs> SurfaceTextureChanged; public partial class SurfaceTextureUpdatedEventArgs { public SurfaceTexture Surface {get;} } public event EventHandler<SurfaceTextureUpdatedEventArgs> SurfaceTextureUpdated; } A requirement of the "event pattern" was that the "add" method must: 1. Start with `add` or `set`, e.g. `setSurfaceTextureListener()`. 2. Have a return type of `void`. 3. Have a *single* parameter which is an interface containing `Listener` as a suffix. As time has gone on, we've found a slight variation on this pattern that we believe `generator` should natively support: 4. *Or*, the "add" method should have *two* parameters, the first being an interface type with a `Listener` suffix, and the second parameter is of type `Android.OS.Handler`. When we encounter (4), we can emit a "Event_With_Handler_Helper" method which calls the "add" method, passing `null` as the `Handler` parameter. The addition of (4) allows us to generate events for the [`android.media.AudioRouting.OnRoutingChangedListener`][3] interface on the [`android.media.AudioRecord`][4] type, which provides the method: // Java class AudioRecord { public void addOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener, Handler handler); } This would allow `generator` to emit an `AudioRecord.RoutingChanged` event, a'la: public event EventHandler<Android.Media.AudioRoutingOnRoutingChangedEventArgs> RoutingChanged { add { global::Java.Interop.EventHelper.AddEventHandler<Android.Media.IAudioRoutingOnRoutingChangedListener, Android.Media.IAudioRoutingOnRoutingChangedListenerImplementor>( ref weak_implementor_AddOnRoutingChangedListener, __CreateIAudioRoutingOnRoutingChangedListenerImplementor, AddOnRoutingChangedListener_Event_With_Handler_Helper, __h => __h.Handler += value); } remove { global::Java.Interop.EventHelper.RemoveEventHandler<Android.Media.IAudioRoutingOnRoutingChangedListener, Android.Media.IAudioRoutingOnRoutingChangedListenerImplementor>( ref weak_implementor_AddOnRoutingChangedListener, Android.Media.IAudioRoutingOnRoutingChangedListenerImplementor.__IsEmpty, __v => RemoveOnRoutingChangedListener (__v), __h => __h.Handler -= value); } } void AddOnRoutingChangedListener_Event_With_Handler_Helper (Android.Media.IAudioRoutingOnRoutingChangedListener value) { AddOnRoutingChangedListener (value, null); } Additionally, update `generate.sh` script to include API-29. [0]: https://docs.microsoft.com/en-us/xamarin/android/internals/api-design#events-and-listeners [1]: https://developer.android.com/reference/android/view/TextureView [2]: https://docs.microsoft.com/en-us/dotnet/api/android.views.textureview?view=xamarin-android-sdk-9#events [3]: https://developer.android.com/reference/android/media/AudioRouting.OnRoutingChangedListener.html [4]: https://developer.android.com/reference/android/media/AudioRecord.html [5]: https://developer.android.com/reference/android/media/AudioRecord.html#addOnRoutingChangedListener(android.media.AudioRouting.OnRoutingChangedListener,%20android.os.Handler)
1 parent a30523e commit 7a31216

File tree

6 files changed

+56
-8
lines changed

6 files changed

+56
-8
lines changed

build-tools/xamarin-android-docimporter-ng/generate.sh

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
ANDROID_TOOLCHAIN=~/android-toolchain
44

55
DROID_DOC_API_LEVELS="10 15 16 17 18 19 20 21 22 23"
6-
JAVA_STUB_API_LEVELS="24 25 26 27 28 Q"
6+
JAVA_STUB_API_LEVELS="24 25 26 27 28 29"
77

88

99
for n in $JAVA_STUB_API_LEVELS
@@ -15,4 +15,3 @@ for API_LEVEL in $DROID_DOC_API_LEVELS
1515
do
1616
time mono --debug xamarin-android-docimporter-ng/bin/Debug/xamarin-android-docimporter-ng.exe -droiddoc=$ANDROID_TOOLCHAIN/docs/docs-api-$API_LEVEL/ -output-text=api-$API_LEVEL.params.txt -output-xml=api-$API_LEVEL.params.xml -verbose -framework-only
1717
done
18-

tools/generator/Java.Interop.Tools.Generator.CodeGeneration/CodeGenerator.cs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -804,15 +804,15 @@ public void WriteInterfaceInvoker (InterfaceGen @interface, string indent)
804804
writer.WriteLine ();
805805
}
806806

807-
public void WriteInterfaceListenerEvent (InterfaceGen @interface, string indent, string name, string nameSpec, string methodName, string full_delegate_name, bool needs_sender, string wrefSuffix, string add, string remove)
807+
public void WriteInterfaceListenerEvent (InterfaceGen @interface, string indent, string name, string nameSpec, string methodName, string full_delegate_name, bool needs_sender, string wrefSuffix, string add, string remove, bool hasHandlerArgument = false)
808808
{
809809
writer.WriteLine ("{0}public event {1} {2} {{", indent, opt.GetOutputName (full_delegate_name), name);
810810
writer.WriteLine ("{0}\tadd {{", indent);
811811
writer.WriteLine ("{0}\t\tglobal::Java.Interop.EventHelper.AddEventHandler<{1}, {1}Implementor>(",
812812
indent, opt.GetOutputName (@interface.FullName));
813813
writer.WriteLine ("{0}\t\t\t\tref weak_implementor_{1},", indent, wrefSuffix);
814814
writer.WriteLine ("{0}\t\t\t\t__Create{1}Implementor,", indent, @interface.Name);
815-
writer.WriteLine ("{0}\t\t\t\t{1},", indent, add);
815+
writer.WriteLine ("{0}\t\t\t\t{1},", indent, add + (hasHandlerArgument ? "_Event_With_Handler_Helper" : null));
816816
writer.WriteLine ("{0}\t\t\t\t__h => __h.{1}Handler += value);", indent, nameSpec);
817817
writer.WriteLine ("{0}\t}}", indent);
818818
writer.WriteLine ("{0}\tremove {{", indent);
@@ -825,6 +825,14 @@ public void WriteInterfaceListenerEvent (InterfaceGen @interface, string indent,
825825
writer.WriteLine ("{0}\t}}", indent);
826826
writer.WriteLine ("{0}}}", indent);
827827
writer.WriteLine ();
828+
829+
if (hasHandlerArgument) {
830+
writer.WriteLine ("{0}void {1} ({2} value)", indent, add + "_Event_With_Handler_Helper", opt.GetOutputName (@interface.FullName));
831+
writer.WriteLine ("{0}{{", indent);
832+
writer.WriteLine ("{0}\t{1} (value, null);", indent, add);
833+
writer.WriteLine ("{0}}}", indent);
834+
writer.WriteLine ();
835+
}
828836
}
829837

830838
public void WriteInterfaceListenerEventsAndProperties (InterfaceGen @interface, string indent, ClassGen target, string name, string connector_fmt, string add, string remove)
@@ -847,7 +855,7 @@ public void WriteInterfaceListenerEventsAndProperties (InterfaceGen @interface,
847855
var methods = target.Methods.Concat (target.Properties.Where (p => p.Setter != null).Select (p => p.Setter));
848856
var props = new HashSet<string> ();
849857
var refs = new HashSet<string> ();
850-
var eventMethods = methods.Where (m => m.IsListenerConnector && m.EventName != String.Empty && m.ListenerType == @interface).Distinct ();
858+
var eventMethods = methods.Where (m => m.IsListenerConnector && m.EventName != String.Empty && m.ListenerType == @interface).OrderBy (m => m.Parameters.Count).GroupBy (m => m.Name).Select (g => g.First ()).Distinct ();
851859
foreach (var method in eventMethods) {
852860
string name = method.CalculateEventName (target.ContainsName);
853861
if (String.IsNullOrEmpty (name)) {
@@ -914,8 +922,11 @@ public void WriteInterfaceListenerEventOrProperty (InterfaceGen @interface, Meth
914922
if (opt.GetSafeIdentifier (name) != name) {
915923
Report.Warning (0, Report.WarningInterfaceGen + 5, "event name for {0}.{1} is invalid. `eventName' or `argsType` can be used to assign a valid member name.", @interface.FullName, name);
916924
return;
917-
} else
918-
WriteInterfaceListenerEvent (@interface, indent, name, nameSpec, m.AdjustedName, full_delegate_name, !m.Parameters.HasSender, connector_fmt, add, remove);
925+
} else {
926+
var mt = target.Methods.Where (method => string.Compare (method.Name, connector_fmt, StringComparison.OrdinalIgnoreCase) == 0 && method.IsListenerConnector).FirstOrDefault ();
927+
var hasHandlerArgument = mt != null && mt.IsListenerConnector && mt.Parameters.Count == 2 && mt.Parameters [1].Type == "Android.OS.Handler";
928+
WriteInterfaceListenerEvent (@interface, indent, name, nameSpec, m.AdjustedName, full_delegate_name, !m.Parameters.HasSender, connector_fmt, add, remove, hasHandlerArgument);
929+
}
919930
} else {
920931
if (opt.GetSafeIdentifier (name) != name) {
921932
Report.Warning (0, Report.WarningInterfaceGen + 6, "event property name for {0}.{1} is invalid. `eventName' or `argsType` can be used to assign a valid member name.", @interface.FullName, name);

tools/generator/Java.Interop.Tools.Generator.ObjectModel/Method.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ public string AutoDetectEnumifiedOverrideReturn (AncestorDescendantCache cache)
6464

6565
public bool CanAdd {
6666
get {
67-
return Name.Length > 3 && Name.StartsWith ("Add") && Name.EndsWith ("Listener") && Parameters.Count == 1 && IsVoid &&
67+
return Name.Length > 3 && Name.StartsWith ("Add") && Name.EndsWith ("Listener") && IsVoid &&
68+
(Parameters.Count == 1 || (Parameters.Count == 2 && Parameters [1].Type == "Android.OS.Handler")) &&
6869
!(Parameters [0].IsArray);
6970
}
7071
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
public event MyFullDelegateName MyName {
2+
add {
3+
global::Java.Interop.EventHelper.AddEventHandler<java.code.IMyInterface, java.code.IMyInterfaceImplementor>(
4+
ref weak_implementor_MyWrefSuffix,
5+
__CreateIMyInterfaceImplementor,
6+
AddMyName_Event_With_Handler_Helper,
7+
__h => __h.MyNameSpecHandler += value);
8+
}
9+
remove {
10+
global::Java.Interop.EventHelper.RemoveEventHandler<java.code.IMyInterface, java.code.IMyInterfaceImplementor>(
11+
ref weak_implementor_MyWrefSuffix,
12+
java.code.IMyInterfaceImplementor.__IsEmpty,
13+
RemoveMyName,
14+
__h => __h.MyNameSpecHandler -= value);
15+
}
16+
}
17+
18+
void AddMyName_Event_With_Handler_Helper (java.code.IMyInterface value)
19+
{
20+
AddMyName (value, null);
21+
}
22+

tools/generator/Tests/Unit-Tests/CodeGeneratorTests.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,18 @@ public void WriteInterfaceListenerEvent ()
528528
Assert.AreEqual (GetExpected (nameof (WriteInterfaceListenerEvent)), writer.ToString ().NormalizeLineEndings ());
529529
}
530530

531+
[Test]
532+
public void WriteInterfaceListenerEventWithHandlerArgument ()
533+
{
534+
var iface = SupportTypeBuilder.CreateInterface ("java.code.IMyInterface", options);
535+
536+
generator.Context.ContextTypes.Push (iface);
537+
generator.WriteInterfaceListenerEvent (iface, string.Empty, "MyName", "MyNameSpec", "MyMethodName", "MyFullDelegateName", true, "MyWrefSuffix", "AddMyName", "RemoveMyName", true);
538+
generator.Context.ContextTypes.Pop ();
539+
540+
Assert.AreEqual (GetExpected (nameof (WriteInterfaceListenerEventWithHandlerArgument)), writer.ToString ().NormalizeLineEndings ());
541+
}
542+
531543
[Test]
532544
public void WriteInterfaceListenerProperty ()
533545
{

tools/generator/Tests/generator-Tests.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,9 @@
503503
<Content Include="Unit-Tests\CodeGeneratorExpectedResults\Common\WriteInterfaceListenerEvent.txt">
504504
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
505505
</Content>
506+
<Content Include="Unit-Tests\CodeGeneratorExpectedResults\Common\WriteInterfaceListenerEventWithHandlerArgument.txt">
507+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
508+
</Content>
506509
<Content Include="Unit-Tests\CodeGeneratorExpectedResults\Common\WriteInterfaceListenerProperty.txt">
507510
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
508511
</Content>

0 commit comments

Comments
 (0)