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")]