diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs
index ca25d1962d7..332720d7173 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs
@@ -235,14 +235,21 @@ static bool IsUnconditionalEntry (JavaPeerInfo peer)
return true;
}
- // User-defined ACW types (not MCW bindings, not interfaces) are unconditional
- // because Android can instantiate them from Java at any time.
- if (!peer.IsFrameworkAssembly && !peer.DoNotGenerateAcw && !peer.IsInterface) {
+ // Types marked unconditional by the scanner (component attributes: Activity, Service, etc.)
+ if (peer.IsUnconditional) {
return true;
}
- // Types marked unconditional by the scanner (component attributes: Activity, Service, etc.)
- if (peer.IsUnconditional) {
+ // Binding-generated listener implementors are instantiated by the generated managed event
+ // accessors before being passed to Java. Existing binding assemblies can be recognized by
+ // their generated metadata shape, so their entries can follow managed reachability.
+ if (peer.IsBindingEventListenerImplementor) {
+ return false;
+ }
+
+ // User-defined ACW types (not MCW bindings, not interfaces) are unconditional
+ // because Android can instantiate them from Java at any time.
+ if (!peer.IsFrameworkAssembly && !peer.DoNotGenerateAcw && !peer.IsInterface) {
return true;
}
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
index 5f591978de3..82ed04db361 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
@@ -72,6 +72,13 @@ public sealed record JavaPeerInfo
///
public bool DoNotGenerateAcw { get; init; }
+ ///
+ /// True when the type matches the existing binding-generator listener implementor shape:
+ /// a generated mono/ ACW implementing a Java interface with the static
+ /// __IsEmpty(TImplementor) helper used by event removal.
+ ///
+ public bool IsBindingEventListenerImplementor { get; init; }
+
///
/// True when the type was discovered via [JniTypeSignatureAttribute]
/// rather than [RegisterAttribute]. Used to resolve cross-assembly
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
index a9b28686132..ac9b1255028 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -388,6 +388,8 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A
IsInterface = isInterface,
IsAbstract = isAbstract,
DoNotGenerateAcw = doNotGenerateAcw,
+ IsBindingEventListenerImplementor = IsBindingEventListenerImplementor (
+ typeDef, index, jniName, fullName, isInterface, doNotGenerateAcw, implementedInterfaces),
IsFromJniTypeSignature = registerInfo?.IsFromJniTypeSignature ?? false,
IsUnconditional = isUnconditional,
CannotRegisterInStaticConstructor = cannotRegisterInStaticConstructor,
@@ -405,6 +407,41 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A
}
}
+ static bool IsBindingEventListenerImplementor (
+ TypeDefinition typeDef,
+ AssemblyIndex index,
+ string jniName,
+ string managedTypeName,
+ bool isInterface,
+ bool doNotGenerateAcw,
+ IReadOnlyList implementedInterfaces)
+ {
+ if (isInterface ||
+ doNotGenerateAcw ||
+ !jniName.StartsWith ("mono/", StringComparison.Ordinal) ||
+ implementedInterfaces.Count == 0 ||
+ (typeDef.Attributes & TypeAttributes.Sealed) == 0) {
+ return false;
+ }
+
+ foreach (var methodHandle in typeDef.GetMethods ()) {
+ var methodDef = index.Reader.GetMethodDefinition (methodHandle);
+ if ((methodDef.Attributes & MethodAttributes.Static) == 0 ||
+ index.Reader.GetString (methodDef.Name) != "__IsEmpty") {
+ continue;
+ }
+
+ var signature = methodDef.DecodeSignature (TypeRefSignatureTypeProvider.Instance, genericContext: index);
+ if (signature.ReturnType.ManagedTypeName == "System.Boolean" &&
+ signature.ParameterTypes.Length == 1 &&
+ signature.ParameterTypes [0].ManagedTypeName == managedTypeName) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
bool IsResolvableJavaPeerType (
TypeDefinitionHandle typeDefHandle,
AssemblyIndex index,
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
index f66eb72f608..a7104170d9f 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs
@@ -227,6 +227,31 @@ public void Build_UserAcwType_IsUnconditional ()
Assert.Null (mainEntry.TargetTypeReference);
}
+ [Fact]
+ public void Build_BindingEventListenerImplementor_IsTrimmable ()
+ {
+ var peer = MakeAcwPeer ("mono/example/ListenerImplementor", "Example.ListenerImplementor", "Binding") with {
+ IsBindingEventListenerImplementor = true,
+ };
+ var model = BuildModel ([peer]);
+
+ Assert.False (model.Entries [0].IsUnconditional);
+ Assert.Equal ("Example.ListenerImplementor, Binding", model.Entries [0].TargetTypeReference);
+ }
+
+ [Fact]
+ public void Build_BindingEventListenerImplementor_MarkedUnconditional_IsUnconditional ()
+ {
+ var peer = MakeAcwPeer ("mono/example/ListenerImplementor", "Example.ListenerImplementor", "Binding") with {
+ IsBindingEventListenerImplementor = true,
+ IsUnconditional = true,
+ };
+ var model = BuildModel ([peer]);
+
+ Assert.True (model.Entries [0].IsUnconditional);
+ Assert.Null (model.Entries [0].TargetTypeReference);
+ }
+
[Fact]
public void Build_FrameworkAcwType_IsTrimmable ()
{
@@ -754,21 +779,20 @@ public void Fixture_AcwType_HasProxy (string javaName, string expectedProxyName)
public class FixtureImplementorsAndDispatchers
{
[Theory]
- [InlineData ("mono/android/view/View_IOnClickListenerImplementor", "Implementor")]
- [InlineData ("mono/android/view/View_ClickEventDispatcher", "EventDispatcher")]
- public void Fixture_HelperType_IsUnconditional (string javaName, string kind)
+ [InlineData ("mono/android/view/View_IOnClickListenerImplementor", true)]
+ [InlineData ("mono/android/view/View_ClickEventDispatcher", false)]
+ public void Fixture_HelperType_UsesGeneratedShapeDetection (string javaName, bool isBindingEventListenerImplementor)
{
var peer = FindFixtureByJavaName (javaName);
Assert.False (peer.DoNotGenerateAcw);
Assert.False (peer.IsInterface);
+ Assert.Equal (isBindingEventListenerImplementor, peer.IsBindingEventListenerImplementor);
var model = BuildModel (new [] { peer }, "TypeMap");
var entry = model.Entries.FirstOrDefault ();
Assert.NotNull (entry);
- // Implementor/EventDispatcher types are treated as unconditional ACW types.
- // Future optimization (see #10911) may make them trimmable.
- Assert.True (entry.IsUnconditional, $"{kind} should be unconditional");
+ Assert.Equal (!isBindingEventListenerImplementor, entry.IsUnconditional);
}
}
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
index f8c9192146b..d023af6f389 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs
@@ -92,6 +92,15 @@ public void Scan_InvokerAndInterface_ShareJavaName ()
Assert.Contains (clickListenerPeers, p => p.DoNotGenerateAcw);
}
+ [Fact]
+ public void Scan_DetectsBindingEventListenerImplementorShape ()
+ {
+ Assert.True (FindFixtureByJavaName ("mono/android/view/View_IOnClickListenerImplementor").IsBindingEventListenerImplementor);
+ Assert.False (FindFixtureByJavaName ("mono/android/view/FakeListenerPeer").IsBindingEventListenerImplementor);
+ Assert.False (FindFixtureByJavaName ("mono/android/view/View_ClickEventDispatcher").IsBindingEventListenerImplementor);
+ Assert.False (FindFixtureByJavaName ("my/app/MainActivity").IsBindingEventListenerImplementor);
+ }
+
[Fact]
public void Scan_AllTypes_HaveAssemblyName ()
{
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs
index ceedbc96fe6..f18e1113513 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs
@@ -111,6 +111,18 @@ namespace Android.Views
public class View : Java.Lang.Object
{
protected View (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { }
+
+ [Register ("mono/android/view/View_IOnClickListenerImplementor")]
+ internal sealed class IOnClickListenerImplementor : Java.Lang.Object, IOnClickListener
+ {
+ #pragma warning disable 0649
+ public EventHandler? Handler;
+ #pragma warning restore 0649
+
+ public IOnClickListenerImplementor (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { }
+ public void OnClick (View v) => Handler?.Invoke (this, EventArgs.Empty);
+ internal static bool __IsEmpty (IOnClickListenerImplementor value) => value.Handler is null;
+ }
}
[Register ("android/view/View$OnClickListener", "", "Android.Views.IOnClickListenerInvoker")]
@@ -186,10 +198,11 @@ public interface INamedClickListener : IOnClickListener
string? Label { get; }
}
- [Register ("mono/android/view/View_IOnClickListenerImplementor")]
- public class View_IOnClickListenerImplementor : Java.Lang.Object
+ [Register ("mono/android/view/FakeListenerPeer")]
+ internal sealed class FakeListenerPeer : Java.Lang.Object, IOnClickListener
{
- public View_IOnClickListenerImplementor (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { }
+ public FakeListenerPeer (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { }
+ public void OnClick (View v) { }
}
[Register ("mono/android/view/View_ClickEventDispatcher")]