From dc82325bbffa1756368934f70f4ae73120f43312 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 17 Aug 2026 16:07:32 +0200 Subject: [PATCH 1/2] [typemap] Trim managed-created Java peers Mark generated listener implementors as managed-created and model their Java peers conditionally so unused implementors can be removed by trimming. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 568c6ce4-aef4-44cd-bd52-d5950a5da923 --- .../Android.Runtime/RegisterAttribute.cs | 9 ++++ .../Java.Interop/JniTypeSignatureAttribute.cs | 10 ++++- .../src/Java.Interop/PublicAPI.Unshipped.txt | 2 + .../SupportFiles/RegisterAttribute.cs | 1 + .../SourceWriters/RegisterAttrTests.cs | 45 +++++++++++++++++++ .../Core_Jar2Xml/Android.Views.View.cs | 2 +- ...ogle.Android.Exoplayer.Drm.IExoMediaDrm.cs | 2 +- .../Core_Jar2Xml/Android.Views.View.cs | 2 +- ...ogle.Android.Exoplayer.Drm.IExoMediaDrm.cs | 2 +- .../SourceWriters/Attributes/RegisterAttr.cs | 7 ++- .../InterfaceEventHandlerImplClass.cs | 5 ++- .../Generator/ModelBuilder.cs | 16 ++++--- .../Scanner/AssemblyIndex.cs | 11 +++++ .../Scanner/JavaPeerInfo.cs | 7 +++ .../Scanner/JavaPeerScanner.cs | 1 + .../PublicAPI/API-35/PublicAPI.Unshipped.txt | 2 + .../API-36.1/PublicAPI.Unshipped.txt | 2 + .../PublicAPI/API-36/PublicAPI.Unshipped.txt | 2 + .../PublicAPI/API-37/PublicAPI.Unshipped.txt | 2 + .../Android.Runtime/RegisterAttribute.cs | 9 ++++ .../Generator/TypeMapModelBuilderTests.cs | 36 ++++++++++++--- .../Scanner/JavaPeerScannerTests.cs | 9 ++++ .../TestFixtures/StubAttributes.cs | 2 + .../TestFixtures/TestTypes.cs | 4 +- 24 files changed, 170 insertions(+), 20 deletions(-) create mode 100644 external/Java.Interop/tests/generator-Tests/Unit-Tests/SourceWriters/RegisterAttrTests.cs diff --git a/external/Java.Interop/src/Java.Interop.NamingCustomAttributes/Android.Runtime/RegisterAttribute.cs b/external/Java.Interop/src/Java.Interop.NamingCustomAttributes/Android.Runtime/RegisterAttribute.cs index caa417936e8..315baa2cac0 100644 --- a/external/Java.Interop/src/Java.Interop.NamingCustomAttributes/Android.Runtime/RegisterAttribute.cs +++ b/external/Java.Interop/src/Java.Interop.NamingCustomAttributes/Android.Runtime/RegisterAttribute.cs @@ -60,6 +60,15 @@ public string? Signature { public bool DoNotGenerateAcw {get; set;} + /// + /// Gets or sets whether instances of this type are created by managed code before being passed to Java. + /// + /// + /// Build tooling may trim the Java peer when the managed type is unreachable. This should only be set + /// for types that are not independently instantiated or discovered from Java. + /// + public bool IsManagedCreated {get; set;} + public int ApiSince {get; set;} } } diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniTypeSignatureAttribute.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniTypeSignatureAttribute.cs index 6fc4814c543..441ae5283ec 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniTypeSignatureAttribute.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniTypeSignatureAttribute.cs @@ -33,8 +33,16 @@ public int ArrayRank { public bool GenerateJavaPeer {get; set;} + /// + /// Gets or sets whether instances of this type are created by managed code before being passed to Java. + /// + /// + /// Build tooling may trim the Java peer when the managed type is unreachable. This should only be set + /// for types that are not independently instantiated or discovered from Java. + /// + public bool IsManagedCreated {get; set;} + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] public Type? InvokerType {get; set;} } } - diff --git a/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt b/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt index 2088a07dc59..a7d443eeb90 100644 --- a/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt +++ b/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt @@ -20,6 +20,8 @@ Java.Interop.JniRuntime.JniValueManager.GetPeer(Java.Interop.JniObjectReference Java.Interop.JniRuntime.JniValueManager.EnsureNotDisposed() -> void Java.Interop.JniRuntime.ReflectionJniValueManager Java.Interop.JniRuntime.ReflectionJniValueManager.ReflectionJniValueManager() -> void +Java.Interop.JniTypeSignatureAttribute.IsManagedCreated.get -> bool +Java.Interop.JniTypeSignatureAttribute.IsManagedCreated.set -> void Java.Interop.JniTypeSignatureAttribute.InvokerType.get -> System.Type? Java.Interop.JniTypeSignatureAttribute.InvokerType.set -> void Java.Interop.IJavaPeerable.JniObjectReferenceControlBlock.get -> nint diff --git a/external/Java.Interop/tests/generator-Tests/SupportFiles/RegisterAttribute.cs b/external/Java.Interop/tests/generator-Tests/SupportFiles/RegisterAttribute.cs index 2da73db4715..45025665725 100644 --- a/external/Java.Interop/tests/generator-Tests/SupportFiles/RegisterAttribute.cs +++ b/external/Java.Interop/tests/generator-Tests/SupportFiles/RegisterAttribute.cs @@ -35,6 +35,7 @@ public string Signature { } public bool DoNotGenerateAcw {get; set;} + public bool IsManagedCreated {get; set;} } } diff --git a/external/Java.Interop/tests/generator-Tests/Unit-Tests/SourceWriters/RegisterAttrTests.cs b/external/Java.Interop/tests/generator-Tests/Unit-Tests/SourceWriters/RegisterAttrTests.cs new file mode 100644 index 00000000000..1366818afd9 --- /dev/null +++ b/external/Java.Interop/tests/generator-Tests/Unit-Tests/SourceWriters/RegisterAttrTests.cs @@ -0,0 +1,45 @@ +using System.IO; +using System.Reflection; +using generator.SourceWriters; +using NUnit.Framework; +using Xamarin.SourceWriter; + +namespace generatortests.SourceWriters +{ + [TestFixture] + public class RegisterAttrTests : SourceWritersTestBase + { + static string GetAttributeOutput (RegisterAttr attribute) + { + var writer = new StringWriter (); + attribute.WriteAttribute (new CodeWriter (writer)); + return writer.ToString (); + } + + [Test] + public void RegisterAttribute_ManagedCreated () + { + var attribute = new RegisterAttr ("my/ManagedPeer") { + IsManagedCreated = true, + UseGlobal = true, + }; + + Assert.AreEqual ( + "[global::Android.Runtime.Register (\"my/ManagedPeer\", IsManagedCreated=true)]", + GetAttributeOutput (attribute).Trim ()); + } + + [Test] + public void JniTypeSignatureAttribute_ManagedCreated () + { + var attribute = new RegisterAttr ("my/ManagedPeer", connector: "My.ManagedPeerInvoker") { + IsManagedCreated = true, + MemberType = MemberTypes.TypeInfo, + }; + + Assert.AreEqual ( + "[global::Java.Interop.JniTypeSignature (\"my/ManagedPeer\", GenerateJavaPeer=true, IsManagedCreated=true, InvokerType=typeof (My.ManagedPeerInvoker))]", + GetAttributeOutput (attribute).Trim ()); + } + } +} diff --git a/external/Java.Interop/tests/generator-Tests/expected.ji/Core_Jar2Xml/Android.Views.View.cs b/external/Java.Interop/tests/generator-Tests/expected.ji/Core_Jar2Xml/Android.Views.View.cs index 6abf1945ac2..555873f4893 100644 --- a/external/Java.Interop/tests/generator-Tests/expected.ji/Core_Jar2Xml/Android.Views.View.cs +++ b/external/Java.Interop/tests/generator-Tests/expected.ji/Core_Jar2Xml/Android.Views.View.cs @@ -15,7 +15,7 @@ public partial interface IOnClickListener : IJavaPeerable { } - [global::Android.Runtime.Register ("mono/android/view/View_OnClickListenerImplementor")] + [global::Android.Runtime.Register ("mono/android/view/View_OnClickListenerImplementor", IsManagedCreated=true)] internal sealed partial class IOnClickListenerImplementor : global::Java.Lang.Object, IOnClickListener { public IOnClickListenerImplementor () : base (global::Android.Runtime.JNIEnv.StartCreateInstance ("mono/android/view/View_OnClickListenerImplementor", "()V"), JniHandleOwnership.TransferLocalRef) { diff --git a/external/Java.Interop/tests/generator-Tests/expected.ji/GenericArguments/Com.Google.Android.Exoplayer.Drm.IExoMediaDrm.cs b/external/Java.Interop/tests/generator-Tests/expected.ji/GenericArguments/Com.Google.Android.Exoplayer.Drm.IExoMediaDrm.cs index 5dd4b571f15..692c2134128 100644 --- a/external/Java.Interop/tests/generator-Tests/expected.ji/GenericArguments/Com.Google.Android.Exoplayer.Drm.IExoMediaDrm.cs +++ b/external/Java.Interop/tests/generator-Tests/expected.ji/GenericArguments/Com.Google.Android.Exoplayer.Drm.IExoMediaDrm.cs @@ -56,7 +56,7 @@ public int P3 { } - [global::Android.Runtime.Register ("mono/com/google/android/exoplayer/drm/ExoMediaDrm_OnEventListenerImplementor")] + [global::Android.Runtime.Register ("mono/com/google/android/exoplayer/drm/ExoMediaDrm_OnEventListenerImplementor", IsManagedCreated=true)] internal sealed partial class IExoMediaDrmOnEventListenerImplementor : global::Java.Lang.Object, IExoMediaDrmOnEventListener { object sender; diff --git a/external/Java.Interop/tests/generator-Tests/expected.xaji/Core_Jar2Xml/Android.Views.View.cs b/external/Java.Interop/tests/generator-Tests/expected.xaji/Core_Jar2Xml/Android.Views.View.cs index 3a1c6b7f821..cafd2467768 100644 --- a/external/Java.Interop/tests/generator-Tests/expected.xaji/Core_Jar2Xml/Android.Views.View.cs +++ b/external/Java.Interop/tests/generator-Tests/expected.xaji/Core_Jar2Xml/Android.Views.View.cs @@ -92,7 +92,7 @@ public unsafe void OnClick (global::Android.Views.View v) } - [global::Android.Runtime.Register ("mono/android/view/View_OnClickListenerImplementor")] + [global::Android.Runtime.Register ("mono/android/view/View_OnClickListenerImplementor", IsManagedCreated=true)] internal sealed partial class IOnClickListenerImplementor : global::Java.Lang.Object, IOnClickListener { public unsafe IOnClickListenerImplementor () : base (IntPtr.Zero, JniHandleOwnership.DoNotTransfer) { diff --git a/external/Java.Interop/tests/generator-Tests/expected.xaji/GenericArguments/Com.Google.Android.Exoplayer.Drm.IExoMediaDrm.cs b/external/Java.Interop/tests/generator-Tests/expected.xaji/GenericArguments/Com.Google.Android.Exoplayer.Drm.IExoMediaDrm.cs index e5e00d5f16b..417049ddbda 100644 --- a/external/Java.Interop/tests/generator-Tests/expected.xaji/GenericArguments/Com.Google.Android.Exoplayer.Drm.IExoMediaDrm.cs +++ b/external/Java.Interop/tests/generator-Tests/expected.xaji/GenericArguments/Com.Google.Android.Exoplayer.Drm.IExoMediaDrm.cs @@ -145,7 +145,7 @@ public byte[] P4 { } - [global::Android.Runtime.Register ("mono/com/google/android/exoplayer/drm/ExoMediaDrm_OnEventListenerImplementor")] + [global::Android.Runtime.Register ("mono/com/google/android/exoplayer/drm/ExoMediaDrm_OnEventListenerImplementor", IsManagedCreated=true)] internal sealed partial class IExoMediaDrmOnEventListenerImplementor : global::Java.Lang.Object, IExoMediaDrmOnEventListener { object sender; diff --git a/external/Java.Interop/tools/generator/SourceWriters/Attributes/RegisterAttr.cs b/external/Java.Interop/tools/generator/SourceWriters/Attributes/RegisterAttr.cs index bad28fbc654..d0c9e67b7b5 100644 --- a/external/Java.Interop/tools/generator/SourceWriters/Attributes/RegisterAttr.cs +++ b/external/Java.Interop/tools/generator/SourceWriters/Attributes/RegisterAttr.cs @@ -14,6 +14,7 @@ public class RegisterAttr : AttributeWriter public string Signature { get; set; } public string Connector { get; set; } public bool DoNotGenerateAcw { get; set; } + public bool IsManagedCreated { get; set; } public string AdditionalProperties { get; set; } public bool UseGlobal { get; set; } // TODO: Temporary for matching existing unit tests public bool UseShortForm { get; set; } // TODO: Temporary for matching existing unit tests @@ -49,6 +50,9 @@ public override void WriteAttribute (CodeWriter writer) if (DoNotGenerateAcw && !AcwLast) sb.Append (", DoNotGenerateAcw=true"); + if (IsManagedCreated) + sb.Append (", IsManagedCreated=true"); + if (AdditionalProperties.HasValue ()) sb.Append (AdditionalProperties); @@ -67,7 +71,8 @@ private void WriteJavaInterop1Attribute (CodeWriter writer) var invokerType = string.IsNullOrEmpty (Connector) ? "" : $", InvokerType=typeof ({Connector.Replace ('/', '.')})"; - writer.WriteLine ($"[global::Java.Interop.JniTypeSignature (\"{Name}\", GenerateJavaPeer={(DoNotGenerateAcw ? "false" : "true")}{invokerType})]"); + var isManagedCreated = IsManagedCreated ? ", IsManagedCreated=true" : ""; + writer.WriteLine ($"[global::Java.Interop.JniTypeSignature (\"{Name}\", GenerateJavaPeer={(DoNotGenerateAcw ? "false" : "true")}{isManagedCreated}{invokerType})]"); break; case MemberTypes.Constructor: writer.WriteLine ($"[global::Java.Interop.JniConstructorSignature (\"{Signature}\")]"); diff --git a/external/Java.Interop/tools/generator/SourceWriters/InterfaceEventHandlerImplClass.cs b/external/Java.Interop/tools/generator/SourceWriters/InterfaceEventHandlerImplClass.cs index 6fee9ef06a5..8c63cb5b0a2 100644 --- a/external/Java.Interop/tools/generator/SourceWriters/InterfaceEventHandlerImplClass.cs +++ b/external/Java.Interop/tools/generator/SourceWriters/InterfaceEventHandlerImplClass.cs @@ -27,7 +27,10 @@ public InterfaceEventHandlerImplClass (InterfaceGen iface, CodeGenerationOptions type.Nullable = opt.SupportNullableReferenceTypes; Fields.Add (new FieldWriter { Name = "sender", Type = type }); } - Attributes.Add (new RegisterAttr (jni_class, additionalProperties: iface.AdditionalAttributeString ()) { UseGlobal = true }); + Attributes.Add (new RegisterAttr (jni_class, additionalProperties: iface.AdditionalAttributeString ()) { + IsManagedCreated = true, + UseGlobal = true, + }); AddConstructor (iface); AddMethods (iface, opt); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index ca25d1962d7..3bf4021b389 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -235,14 +235,20 @@ 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) { + // Managed-created peers only enter Java after managed code constructs them. Their conditional + // entry follows the managed type's reachability while preserving callbacks when the type is used. + if (peer.IsManagedCreated) { + 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/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index f16e61eb2cc..7411c24bbae 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -314,6 +314,7 @@ internal RegisterInfo ParseJniTypeSignatureAttribute (CustomAttribute ca) string jniName = ""; bool doNotGenerateAcw = false; + bool isManagedCreated = false; if (value.FixedArguments.Length > 0) { jniName = (string?)value.FixedArguments [0].Value ?? ""; @@ -322,12 +323,16 @@ internal RegisterInfo ParseJniTypeSignatureAttribute (CustomAttribute ca) if (TryGetNamedArgument (value, "GenerateJavaPeer", out var generateJavaPeer)) { doNotGenerateAcw = !generateJavaPeer; } + if (TryGetNamedArgument (value, "IsManagedCreated", out var managedCreated)) { + isManagedCreated = managedCreated; + } var isArrayType = TryGetNamedArgument (value, "ArrayRank", out var rank) && rank > 0; return new RegisterInfo { JniName = jniName.Replace ('.', '/'), DoNotGenerateAcw = doNotGenerateAcw, + IsManagedCreated = isManagedCreated, IsFromJniTypeSignature = true, IsArrayType = isArrayType, }; @@ -345,6 +350,7 @@ RegisterInfo ParseRegisterInfo (CustomAttributeValue value) string? signature = null; string? connector = null; bool doNotGenerateAcw = false; + bool isManagedCreated = false; if (value.FixedArguments.Length > 0) { jniName = (string?)value.FixedArguments [0].Value ?? ""; @@ -359,12 +365,16 @@ RegisterInfo ParseRegisterInfo (CustomAttributeValue value) if (TryGetNamedArgument (value, "DoNotGenerateAcw", out var doNotGenerateAcwValue)) { doNotGenerateAcw = doNotGenerateAcwValue; } + if (TryGetNamedArgument (value, "IsManagedCreated", out var managedCreated)) { + isManagedCreated = managedCreated; + } return new RegisterInfo { JniName = jniName, Signature = signature, Connector = connector, DoNotGenerateAcw = doNotGenerateAcw, + IsManagedCreated = isManagedCreated, }; } @@ -667,6 +677,7 @@ sealed record RegisterInfo public string? Signature { get; init; } public string? Connector { get; init; } public bool DoNotGenerateAcw { get; init; } + public bool IsManagedCreated { get; init; } public bool IsFromJniTypeSignature { get; init; } public bool IsArrayType { get; init; } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 5f591978de3..cb95eb7217b 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 instances are created by managed code before being passed to Java. + /// Such peers can be trimmed based on managed reachability because Java does not independently + /// instantiate or discover them. + /// + public bool IsManagedCreated { 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..547a40ade29 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -388,6 +388,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A IsInterface = isInterface, IsAbstract = isAbstract, DoNotGenerateAcw = doNotGenerateAcw, + IsManagedCreated = registerInfo?.IsManagedCreated ?? false, IsFromJniTypeSignature = registerInfo?.IsFromJniTypeSignature ?? false, IsUnconditional = isUnconditional, CannotRegisterInStaticConstructor = cannotRegisterInStaticConstructor, diff --git a/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt index 232ca9037b0..be599187e76 100644 --- a/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt +++ b/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ #nullable enable +Android.Runtime.RegisterAttribute.IsManagedCreated.get -> bool +Android.Runtime.RegisterAttribute.IsManagedCreated.set -> void REMOVED Xamarin.Android.Net.AndroidClientHandler REMOVED Xamarin.Android.Net.AndroidClientHandler.AndroidClientHandler() -> void REMOVED Xamarin.Android.Net.AndroidClientHandler.AssertSelf() -> void diff --git a/src/Mono.Android/PublicAPI/API-36.1/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-36.1/PublicAPI.Unshipped.txt index 232ca9037b0..be599187e76 100644 --- a/src/Mono.Android/PublicAPI/API-36.1/PublicAPI.Unshipped.txt +++ b/src/Mono.Android/PublicAPI/API-36.1/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ #nullable enable +Android.Runtime.RegisterAttribute.IsManagedCreated.get -> bool +Android.Runtime.RegisterAttribute.IsManagedCreated.set -> void REMOVED Xamarin.Android.Net.AndroidClientHandler REMOVED Xamarin.Android.Net.AndroidClientHandler.AndroidClientHandler() -> void REMOVED Xamarin.Android.Net.AndroidClientHandler.AssertSelf() -> void diff --git a/src/Mono.Android/PublicAPI/API-36/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-36/PublicAPI.Unshipped.txt index 2716042abfc..f447d7f542b 100644 --- a/src/Mono.Android/PublicAPI/API-36/PublicAPI.Unshipped.txt +++ b/src/Mono.Android/PublicAPI/API-36/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ #nullable enable +Android.Runtime.RegisterAttribute.IsManagedCreated.get -> bool +Android.Runtime.RegisterAttribute.IsManagedCreated.set -> void REMOVED Xamarin.Android.Net.AndroidClientHandler REMOVED Xamarin.Android.Net.AndroidClientHandler.AndroidClientHandler() -> void REMOVED Xamarin.Android.Net.AndroidClientHandler.AssertSelf() -> void diff --git a/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt index 7fdf466ee6b..64f04be24a7 100644 --- a/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt +++ b/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ #nullable enable +Android.Runtime.RegisterAttribute.IsManagedCreated.get -> bool +Android.Runtime.RegisterAttribute.IsManagedCreated.set -> void *REMOVED*Android.App.Admin.DevicePolicyManager.SetDeviceOwnerLockScreenInfo(Android.Content.ComponentName! admin, string? info) -> void *REMOVED*Android.App.AppFunctions.AppFunctionManager.SetAppFunctionEnabled(string! functionIdentifier, Android.App.AppFunctions.AppFunctionState newEnabledState, Java.Util.Concurrent.IExecutor! executor, Android.OS.IOutcomeReceiver! callback) -> void *REMOVED*Android.App.AppFunctions.AppFunctionState.Default = 0 -> Android.App.AppFunctions.AppFunctionState diff --git a/src/Xamarin.Android.NamingCustomAttributes/Android.Runtime/RegisterAttribute.cs b/src/Xamarin.Android.NamingCustomAttributes/Android.Runtime/RegisterAttribute.cs index 058bb0d85c7..0c45489bd1d 100644 --- a/src/Xamarin.Android.NamingCustomAttributes/Android.Runtime/RegisterAttribute.cs +++ b/src/Xamarin.Android.NamingCustomAttributes/Android.Runtime/RegisterAttribute.cs @@ -59,6 +59,15 @@ public string? Signature { public bool DoNotGenerateAcw {get; set;} + /// + /// Gets or sets whether instances of this type are created by managed code before being passed to Java. + /// + /// + /// Build tooling may trim the Java peer when the managed type is unreachable. This should only be set + /// for types that are not independently instantiated or discovered from Java. + /// + public bool IsManagedCreated {get; set;} + public int ApiSince {get; set;} } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index f66eb72f608..68653981949 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_ManagedCreatedUserAcw_IsTrimmable () + { + var peer = MakeAcwPeer ("my/app/Listener", "MyApp.Listener", "App") with { + IsManagedCreated = true, + }; + var model = BuildModel ([peer]); + + Assert.False (model.Entries [0].IsUnconditional); + Assert.Equal ("MyApp.Listener, App", model.Entries [0].TargetTypeReference); + } + + [Fact] + public void Build_ManagedCreatedUserAcw_MarkedUnconditional_IsUnconditional () + { + var peer = MakeAcwPeer ("my/app/ManifestListener", "MyApp.ManifestListener", "App") with { + IsManagedCreated = 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_UsesManagedCreatedMetadata (string javaName, bool isManagedCreated) { var peer = FindFixtureByJavaName (javaName); Assert.False (peer.DoNotGenerateAcw); Assert.False (peer.IsInterface); + Assert.Equal (isManagedCreated, peer.IsManagedCreated); 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 (!isManagedCreated, 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..4ce7b26ddda 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -130,6 +130,7 @@ public void Scan_JniTypeSignature_IsDiscovered () var peer = FindFixtureByJavaName ("net/dot/jni/test/JavaDisposedObject"); Assert.Equal ("Java.Interop.TestTypes.JavaDisposedObject", peer.ManagedTypeName); Assert.False (peer.DoNotGenerateAcw, "GenerateJavaPeer=true should map to DoNotGenerateAcw=false"); + Assert.True (peer.IsManagedCreated); } [Fact] @@ -137,6 +138,14 @@ public void Scan_JniTypeSignature_DoNotGenerateAcw () { var nonGenerated = FindFixtureByJavaName ("net/dot/jni/test/MyJavaObject"); Assert.True (nonGenerated.DoNotGenerateAcw, "NonGeneratedJavaObject has GenerateJavaPeer=false"); + Assert.False (nonGenerated.IsManagedCreated); + } + + [Fact] + public void Scan_RegisterAttribute_IsManagedCreated () + { + Assert.True (FindFixtureByJavaName ("mono/android/view/View_IOnClickListenerImplementor").IsManagedCreated); + Assert.False (FindFixtureByJavaName ("mono/android/view/View_ClickEventDispatcher").IsManagedCreated); } [Fact] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs index 050741c3e20..ab269db7a8b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs @@ -20,6 +20,7 @@ public sealed class RegisterAttribute : Attribute, Java.Interop.IJniNameProvider public string? Signature { get; set; } public string? Connector { get; set; } public bool DoNotGenerateAcw { get; set; } + public bool IsManagedCreated { get; set; } public int ApiSince { get; set; } public RegisterAttribute (string name) => Name = name; @@ -229,6 +230,7 @@ public sealed class JniTypeSignatureAttribute : Attribute { public string SimpleReference { get; } public bool GenerateJavaPeer { get; set; } = true; + public bool IsManagedCreated { get; set; } public bool IsKeyword { get; set; } public int ArrayRank { get; set; } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index ceedbc96fe6..c6c6b8f6d7b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -186,7 +186,7 @@ public interface INamedClickListener : IOnClickListener string? Label { get; } } - [Register ("mono/android/view/View_IOnClickListenerImplementor")] + [Register ("mono/android/view/View_IOnClickListenerImplementor", IsManagedCreated = true)] public class View_IOnClickListenerImplementor : Java.Lang.Object { public View_IOnClickListenerImplementor (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } @@ -1315,7 +1315,7 @@ public class JavaObject public JavaObject () { } } - [Java.Interop.JniTypeSignature ("net/dot/jni/test/JavaDisposedObject")] + [Java.Interop.JniTypeSignature ("net/dot/jni/test/JavaDisposedObject", IsManagedCreated = true)] public class JavaDisposedObject : JavaObject { public JavaDisposedObject () { } From ab6613a4ea2a1d8f343bee1411665beb365fd10d Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 17 Aug 2026 16:38:54 +0200 Subject: [PATCH 2/2] [typemap] Infer generated listener implementors Replace the explicit managed-created metadata contract with backward-compatible detection of the existing binding-generator implementor shape. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 568c6ce4-aef4-44cd-bd52-d5950a5da923 --- .../Android.Runtime/RegisterAttribute.cs | 9 ---- .../Java.Interop/JniTypeSignatureAttribute.cs | 10 +---- .../src/Java.Interop/PublicAPI.Unshipped.txt | 2 - .../SupportFiles/RegisterAttribute.cs | 1 - .../SourceWriters/RegisterAttrTests.cs | 45 ------------------- .../Core_Jar2Xml/Android.Views.View.cs | 2 +- ...ogle.Android.Exoplayer.Drm.IExoMediaDrm.cs | 2 +- .../Core_Jar2Xml/Android.Views.View.cs | 2 +- ...ogle.Android.Exoplayer.Drm.IExoMediaDrm.cs | 2 +- .../SourceWriters/Attributes/RegisterAttr.cs | 7 +-- .../InterfaceEventHandlerImplClass.cs | 5 +-- .../Generator/ModelBuilder.cs | 7 +-- .../Scanner/AssemblyIndex.cs | 11 ----- .../Scanner/JavaPeerInfo.cs | 8 ++-- .../Scanner/JavaPeerScanner.cs | 38 +++++++++++++++- .../PublicAPI/API-35/PublicAPI.Unshipped.txt | 2 - .../API-36.1/PublicAPI.Unshipped.txt | 2 - .../PublicAPI/API-36/PublicAPI.Unshipped.txt | 2 - .../PublicAPI/API-37/PublicAPI.Unshipped.txt | 2 - .../Android.Runtime/RegisterAttribute.cs | 9 ---- .../Generator/TypeMapModelBuilderTests.cs | 20 ++++----- .../Scanner/JavaPeerScannerTests.cs | 18 ++++---- .../TestFixtures/StubAttributes.cs | 2 - .../TestFixtures/TestTypes.cs | 21 +++++++-- 24 files changed, 88 insertions(+), 141 deletions(-) delete mode 100644 external/Java.Interop/tests/generator-Tests/Unit-Tests/SourceWriters/RegisterAttrTests.cs diff --git a/external/Java.Interop/src/Java.Interop.NamingCustomAttributes/Android.Runtime/RegisterAttribute.cs b/external/Java.Interop/src/Java.Interop.NamingCustomAttributes/Android.Runtime/RegisterAttribute.cs index 315baa2cac0..caa417936e8 100644 --- a/external/Java.Interop/src/Java.Interop.NamingCustomAttributes/Android.Runtime/RegisterAttribute.cs +++ b/external/Java.Interop/src/Java.Interop.NamingCustomAttributes/Android.Runtime/RegisterAttribute.cs @@ -60,15 +60,6 @@ public string? Signature { public bool DoNotGenerateAcw {get; set;} - /// - /// Gets or sets whether instances of this type are created by managed code before being passed to Java. - /// - /// - /// Build tooling may trim the Java peer when the managed type is unreachable. This should only be set - /// for types that are not independently instantiated or discovered from Java. - /// - public bool IsManagedCreated {get; set;} - public int ApiSince {get; set;} } } diff --git a/external/Java.Interop/src/Java.Interop/Java.Interop/JniTypeSignatureAttribute.cs b/external/Java.Interop/src/Java.Interop/Java.Interop/JniTypeSignatureAttribute.cs index 441ae5283ec..6fc4814c543 100644 --- a/external/Java.Interop/src/Java.Interop/Java.Interop/JniTypeSignatureAttribute.cs +++ b/external/Java.Interop/src/Java.Interop/Java.Interop/JniTypeSignatureAttribute.cs @@ -33,16 +33,8 @@ public int ArrayRank { public bool GenerateJavaPeer {get; set;} - /// - /// Gets or sets whether instances of this type are created by managed code before being passed to Java. - /// - /// - /// Build tooling may trim the Java peer when the managed type is unreachable. This should only be set - /// for types that are not independently instantiated or discovered from Java. - /// - public bool IsManagedCreated {get; set;} - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] public Type? InvokerType {get; set;} } } + diff --git a/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt b/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt index a7d443eeb90..2088a07dc59 100644 --- a/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt +++ b/external/Java.Interop/src/Java.Interop/PublicAPI.Unshipped.txt @@ -20,8 +20,6 @@ Java.Interop.JniRuntime.JniValueManager.GetPeer(Java.Interop.JniObjectReference Java.Interop.JniRuntime.JniValueManager.EnsureNotDisposed() -> void Java.Interop.JniRuntime.ReflectionJniValueManager Java.Interop.JniRuntime.ReflectionJniValueManager.ReflectionJniValueManager() -> void -Java.Interop.JniTypeSignatureAttribute.IsManagedCreated.get -> bool -Java.Interop.JniTypeSignatureAttribute.IsManagedCreated.set -> void Java.Interop.JniTypeSignatureAttribute.InvokerType.get -> System.Type? Java.Interop.JniTypeSignatureAttribute.InvokerType.set -> void Java.Interop.IJavaPeerable.JniObjectReferenceControlBlock.get -> nint diff --git a/external/Java.Interop/tests/generator-Tests/SupportFiles/RegisterAttribute.cs b/external/Java.Interop/tests/generator-Tests/SupportFiles/RegisterAttribute.cs index 45025665725..2da73db4715 100644 --- a/external/Java.Interop/tests/generator-Tests/SupportFiles/RegisterAttribute.cs +++ b/external/Java.Interop/tests/generator-Tests/SupportFiles/RegisterAttribute.cs @@ -35,7 +35,6 @@ public string Signature { } public bool DoNotGenerateAcw {get; set;} - public bool IsManagedCreated {get; set;} } } diff --git a/external/Java.Interop/tests/generator-Tests/Unit-Tests/SourceWriters/RegisterAttrTests.cs b/external/Java.Interop/tests/generator-Tests/Unit-Tests/SourceWriters/RegisterAttrTests.cs deleted file mode 100644 index 1366818afd9..00000000000 --- a/external/Java.Interop/tests/generator-Tests/Unit-Tests/SourceWriters/RegisterAttrTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.IO; -using System.Reflection; -using generator.SourceWriters; -using NUnit.Framework; -using Xamarin.SourceWriter; - -namespace generatortests.SourceWriters -{ - [TestFixture] - public class RegisterAttrTests : SourceWritersTestBase - { - static string GetAttributeOutput (RegisterAttr attribute) - { - var writer = new StringWriter (); - attribute.WriteAttribute (new CodeWriter (writer)); - return writer.ToString (); - } - - [Test] - public void RegisterAttribute_ManagedCreated () - { - var attribute = new RegisterAttr ("my/ManagedPeer") { - IsManagedCreated = true, - UseGlobal = true, - }; - - Assert.AreEqual ( - "[global::Android.Runtime.Register (\"my/ManagedPeer\", IsManagedCreated=true)]", - GetAttributeOutput (attribute).Trim ()); - } - - [Test] - public void JniTypeSignatureAttribute_ManagedCreated () - { - var attribute = new RegisterAttr ("my/ManagedPeer", connector: "My.ManagedPeerInvoker") { - IsManagedCreated = true, - MemberType = MemberTypes.TypeInfo, - }; - - Assert.AreEqual ( - "[global::Java.Interop.JniTypeSignature (\"my/ManagedPeer\", GenerateJavaPeer=true, IsManagedCreated=true, InvokerType=typeof (My.ManagedPeerInvoker))]", - GetAttributeOutput (attribute).Trim ()); - } - } -} diff --git a/external/Java.Interop/tests/generator-Tests/expected.ji/Core_Jar2Xml/Android.Views.View.cs b/external/Java.Interop/tests/generator-Tests/expected.ji/Core_Jar2Xml/Android.Views.View.cs index 555873f4893..6abf1945ac2 100644 --- a/external/Java.Interop/tests/generator-Tests/expected.ji/Core_Jar2Xml/Android.Views.View.cs +++ b/external/Java.Interop/tests/generator-Tests/expected.ji/Core_Jar2Xml/Android.Views.View.cs @@ -15,7 +15,7 @@ public partial interface IOnClickListener : IJavaPeerable { } - [global::Android.Runtime.Register ("mono/android/view/View_OnClickListenerImplementor", IsManagedCreated=true)] + [global::Android.Runtime.Register ("mono/android/view/View_OnClickListenerImplementor")] internal sealed partial class IOnClickListenerImplementor : global::Java.Lang.Object, IOnClickListener { public IOnClickListenerImplementor () : base (global::Android.Runtime.JNIEnv.StartCreateInstance ("mono/android/view/View_OnClickListenerImplementor", "()V"), JniHandleOwnership.TransferLocalRef) { diff --git a/external/Java.Interop/tests/generator-Tests/expected.ji/GenericArguments/Com.Google.Android.Exoplayer.Drm.IExoMediaDrm.cs b/external/Java.Interop/tests/generator-Tests/expected.ji/GenericArguments/Com.Google.Android.Exoplayer.Drm.IExoMediaDrm.cs index 692c2134128..5dd4b571f15 100644 --- a/external/Java.Interop/tests/generator-Tests/expected.ji/GenericArguments/Com.Google.Android.Exoplayer.Drm.IExoMediaDrm.cs +++ b/external/Java.Interop/tests/generator-Tests/expected.ji/GenericArguments/Com.Google.Android.Exoplayer.Drm.IExoMediaDrm.cs @@ -56,7 +56,7 @@ public int P3 { } - [global::Android.Runtime.Register ("mono/com/google/android/exoplayer/drm/ExoMediaDrm_OnEventListenerImplementor", IsManagedCreated=true)] + [global::Android.Runtime.Register ("mono/com/google/android/exoplayer/drm/ExoMediaDrm_OnEventListenerImplementor")] internal sealed partial class IExoMediaDrmOnEventListenerImplementor : global::Java.Lang.Object, IExoMediaDrmOnEventListener { object sender; diff --git a/external/Java.Interop/tests/generator-Tests/expected.xaji/Core_Jar2Xml/Android.Views.View.cs b/external/Java.Interop/tests/generator-Tests/expected.xaji/Core_Jar2Xml/Android.Views.View.cs index cafd2467768..3a1c6b7f821 100644 --- a/external/Java.Interop/tests/generator-Tests/expected.xaji/Core_Jar2Xml/Android.Views.View.cs +++ b/external/Java.Interop/tests/generator-Tests/expected.xaji/Core_Jar2Xml/Android.Views.View.cs @@ -92,7 +92,7 @@ public unsafe void OnClick (global::Android.Views.View v) } - [global::Android.Runtime.Register ("mono/android/view/View_OnClickListenerImplementor", IsManagedCreated=true)] + [global::Android.Runtime.Register ("mono/android/view/View_OnClickListenerImplementor")] internal sealed partial class IOnClickListenerImplementor : global::Java.Lang.Object, IOnClickListener { public unsafe IOnClickListenerImplementor () : base (IntPtr.Zero, JniHandleOwnership.DoNotTransfer) { diff --git a/external/Java.Interop/tests/generator-Tests/expected.xaji/GenericArguments/Com.Google.Android.Exoplayer.Drm.IExoMediaDrm.cs b/external/Java.Interop/tests/generator-Tests/expected.xaji/GenericArguments/Com.Google.Android.Exoplayer.Drm.IExoMediaDrm.cs index 417049ddbda..e5e00d5f16b 100644 --- a/external/Java.Interop/tests/generator-Tests/expected.xaji/GenericArguments/Com.Google.Android.Exoplayer.Drm.IExoMediaDrm.cs +++ b/external/Java.Interop/tests/generator-Tests/expected.xaji/GenericArguments/Com.Google.Android.Exoplayer.Drm.IExoMediaDrm.cs @@ -145,7 +145,7 @@ public byte[] P4 { } - [global::Android.Runtime.Register ("mono/com/google/android/exoplayer/drm/ExoMediaDrm_OnEventListenerImplementor", IsManagedCreated=true)] + [global::Android.Runtime.Register ("mono/com/google/android/exoplayer/drm/ExoMediaDrm_OnEventListenerImplementor")] internal sealed partial class IExoMediaDrmOnEventListenerImplementor : global::Java.Lang.Object, IExoMediaDrmOnEventListener { object sender; diff --git a/external/Java.Interop/tools/generator/SourceWriters/Attributes/RegisterAttr.cs b/external/Java.Interop/tools/generator/SourceWriters/Attributes/RegisterAttr.cs index d0c9e67b7b5..bad28fbc654 100644 --- a/external/Java.Interop/tools/generator/SourceWriters/Attributes/RegisterAttr.cs +++ b/external/Java.Interop/tools/generator/SourceWriters/Attributes/RegisterAttr.cs @@ -14,7 +14,6 @@ public class RegisterAttr : AttributeWriter public string Signature { get; set; } public string Connector { get; set; } public bool DoNotGenerateAcw { get; set; } - public bool IsManagedCreated { get; set; } public string AdditionalProperties { get; set; } public bool UseGlobal { get; set; } // TODO: Temporary for matching existing unit tests public bool UseShortForm { get; set; } // TODO: Temporary for matching existing unit tests @@ -50,9 +49,6 @@ public override void WriteAttribute (CodeWriter writer) if (DoNotGenerateAcw && !AcwLast) sb.Append (", DoNotGenerateAcw=true"); - if (IsManagedCreated) - sb.Append (", IsManagedCreated=true"); - if (AdditionalProperties.HasValue ()) sb.Append (AdditionalProperties); @@ -71,8 +67,7 @@ private void WriteJavaInterop1Attribute (CodeWriter writer) var invokerType = string.IsNullOrEmpty (Connector) ? "" : $", InvokerType=typeof ({Connector.Replace ('/', '.')})"; - var isManagedCreated = IsManagedCreated ? ", IsManagedCreated=true" : ""; - writer.WriteLine ($"[global::Java.Interop.JniTypeSignature (\"{Name}\", GenerateJavaPeer={(DoNotGenerateAcw ? "false" : "true")}{isManagedCreated}{invokerType})]"); + writer.WriteLine ($"[global::Java.Interop.JniTypeSignature (\"{Name}\", GenerateJavaPeer={(DoNotGenerateAcw ? "false" : "true")}{invokerType})]"); break; case MemberTypes.Constructor: writer.WriteLine ($"[global::Java.Interop.JniConstructorSignature (\"{Signature}\")]"); diff --git a/external/Java.Interop/tools/generator/SourceWriters/InterfaceEventHandlerImplClass.cs b/external/Java.Interop/tools/generator/SourceWriters/InterfaceEventHandlerImplClass.cs index 8c63cb5b0a2..6fee9ef06a5 100644 --- a/external/Java.Interop/tools/generator/SourceWriters/InterfaceEventHandlerImplClass.cs +++ b/external/Java.Interop/tools/generator/SourceWriters/InterfaceEventHandlerImplClass.cs @@ -27,10 +27,7 @@ public InterfaceEventHandlerImplClass (InterfaceGen iface, CodeGenerationOptions type.Nullable = opt.SupportNullableReferenceTypes; Fields.Add (new FieldWriter { Name = "sender", Type = type }); } - Attributes.Add (new RegisterAttr (jni_class, additionalProperties: iface.AdditionalAttributeString ()) { - IsManagedCreated = true, - UseGlobal = true, - }); + Attributes.Add (new RegisterAttr (jni_class, additionalProperties: iface.AdditionalAttributeString ()) { UseGlobal = true }); AddConstructor (iface); AddMethods (iface, opt); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 3bf4021b389..332720d7173 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -240,9 +240,10 @@ static bool IsUnconditionalEntry (JavaPeerInfo peer) return true; } - // Managed-created peers only enter Java after managed code constructs them. Their conditional - // entry follows the managed type's reachability while preserving callbacks when the type is used. - if (peer.IsManagedCreated) { + // 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; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index 7411c24bbae..f16e61eb2cc 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -314,7 +314,6 @@ internal RegisterInfo ParseJniTypeSignatureAttribute (CustomAttribute ca) string jniName = ""; bool doNotGenerateAcw = false; - bool isManagedCreated = false; if (value.FixedArguments.Length > 0) { jniName = (string?)value.FixedArguments [0].Value ?? ""; @@ -323,16 +322,12 @@ internal RegisterInfo ParseJniTypeSignatureAttribute (CustomAttribute ca) if (TryGetNamedArgument (value, "GenerateJavaPeer", out var generateJavaPeer)) { doNotGenerateAcw = !generateJavaPeer; } - if (TryGetNamedArgument (value, "IsManagedCreated", out var managedCreated)) { - isManagedCreated = managedCreated; - } var isArrayType = TryGetNamedArgument (value, "ArrayRank", out var rank) && rank > 0; return new RegisterInfo { JniName = jniName.Replace ('.', '/'), DoNotGenerateAcw = doNotGenerateAcw, - IsManagedCreated = isManagedCreated, IsFromJniTypeSignature = true, IsArrayType = isArrayType, }; @@ -350,7 +345,6 @@ RegisterInfo ParseRegisterInfo (CustomAttributeValue value) string? signature = null; string? connector = null; bool doNotGenerateAcw = false; - bool isManagedCreated = false; if (value.FixedArguments.Length > 0) { jniName = (string?)value.FixedArguments [0].Value ?? ""; @@ -365,16 +359,12 @@ RegisterInfo ParseRegisterInfo (CustomAttributeValue value) if (TryGetNamedArgument (value, "DoNotGenerateAcw", out var doNotGenerateAcwValue)) { doNotGenerateAcw = doNotGenerateAcwValue; } - if (TryGetNamedArgument (value, "IsManagedCreated", out var managedCreated)) { - isManagedCreated = managedCreated; - } return new RegisterInfo { JniName = jniName, Signature = signature, Connector = connector, DoNotGenerateAcw = doNotGenerateAcw, - IsManagedCreated = isManagedCreated, }; } @@ -677,7 +667,6 @@ sealed record RegisterInfo public string? Signature { get; init; } public string? Connector { get; init; } public bool DoNotGenerateAcw { get; init; } - public bool IsManagedCreated { get; init; } public bool IsFromJniTypeSignature { get; init; } public bool IsArrayType { get; init; } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index cb95eb7217b..82ed04db361 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -73,11 +73,11 @@ public sealed record JavaPeerInfo public bool DoNotGenerateAcw { get; init; } /// - /// True when instances are created by managed code before being passed to Java. - /// Such peers can be trimmed based on managed reachability because Java does not independently - /// instantiate or discover them. + /// 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 IsManagedCreated { get; init; } + public bool IsBindingEventListenerImplementor { get; init; } /// /// True when the type was discovered via [JniTypeSignatureAttribute] diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 547a40ade29..ac9b1255028 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -388,7 +388,8 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A IsInterface = isInterface, IsAbstract = isAbstract, DoNotGenerateAcw = doNotGenerateAcw, - IsManagedCreated = registerInfo?.IsManagedCreated ?? false, + IsBindingEventListenerImplementor = IsBindingEventListenerImplementor ( + typeDef, index, jniName, fullName, isInterface, doNotGenerateAcw, implementedInterfaces), IsFromJniTypeSignature = registerInfo?.IsFromJniTypeSignature ?? false, IsUnconditional = isUnconditional, CannotRegisterInStaticConstructor = cannotRegisterInStaticConstructor, @@ -406,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/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt index be599187e76..232ca9037b0 100644 --- a/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt +++ b/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt @@ -1,6 +1,4 @@ #nullable enable -Android.Runtime.RegisterAttribute.IsManagedCreated.get -> bool -Android.Runtime.RegisterAttribute.IsManagedCreated.set -> void REMOVED Xamarin.Android.Net.AndroidClientHandler REMOVED Xamarin.Android.Net.AndroidClientHandler.AndroidClientHandler() -> void REMOVED Xamarin.Android.Net.AndroidClientHandler.AssertSelf() -> void diff --git a/src/Mono.Android/PublicAPI/API-36.1/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-36.1/PublicAPI.Unshipped.txt index be599187e76..232ca9037b0 100644 --- a/src/Mono.Android/PublicAPI/API-36.1/PublicAPI.Unshipped.txt +++ b/src/Mono.Android/PublicAPI/API-36.1/PublicAPI.Unshipped.txt @@ -1,6 +1,4 @@ #nullable enable -Android.Runtime.RegisterAttribute.IsManagedCreated.get -> bool -Android.Runtime.RegisterAttribute.IsManagedCreated.set -> void REMOVED Xamarin.Android.Net.AndroidClientHandler REMOVED Xamarin.Android.Net.AndroidClientHandler.AndroidClientHandler() -> void REMOVED Xamarin.Android.Net.AndroidClientHandler.AssertSelf() -> void diff --git a/src/Mono.Android/PublicAPI/API-36/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-36/PublicAPI.Unshipped.txt index f447d7f542b..2716042abfc 100644 --- a/src/Mono.Android/PublicAPI/API-36/PublicAPI.Unshipped.txt +++ b/src/Mono.Android/PublicAPI/API-36/PublicAPI.Unshipped.txt @@ -1,6 +1,4 @@ #nullable enable -Android.Runtime.RegisterAttribute.IsManagedCreated.get -> bool -Android.Runtime.RegisterAttribute.IsManagedCreated.set -> void REMOVED Xamarin.Android.Net.AndroidClientHandler REMOVED Xamarin.Android.Net.AndroidClientHandler.AndroidClientHandler() -> void REMOVED Xamarin.Android.Net.AndroidClientHandler.AssertSelf() -> void diff --git a/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt index 64f04be24a7..7fdf466ee6b 100644 --- a/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt +++ b/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt @@ -1,6 +1,4 @@ #nullable enable -Android.Runtime.RegisterAttribute.IsManagedCreated.get -> bool -Android.Runtime.RegisterAttribute.IsManagedCreated.set -> void *REMOVED*Android.App.Admin.DevicePolicyManager.SetDeviceOwnerLockScreenInfo(Android.Content.ComponentName! admin, string? info) -> void *REMOVED*Android.App.AppFunctions.AppFunctionManager.SetAppFunctionEnabled(string! functionIdentifier, Android.App.AppFunctions.AppFunctionState newEnabledState, Java.Util.Concurrent.IExecutor! executor, Android.OS.IOutcomeReceiver! callback) -> void *REMOVED*Android.App.AppFunctions.AppFunctionState.Default = 0 -> Android.App.AppFunctions.AppFunctionState diff --git a/src/Xamarin.Android.NamingCustomAttributes/Android.Runtime/RegisterAttribute.cs b/src/Xamarin.Android.NamingCustomAttributes/Android.Runtime/RegisterAttribute.cs index 0c45489bd1d..058bb0d85c7 100644 --- a/src/Xamarin.Android.NamingCustomAttributes/Android.Runtime/RegisterAttribute.cs +++ b/src/Xamarin.Android.NamingCustomAttributes/Android.Runtime/RegisterAttribute.cs @@ -59,15 +59,6 @@ public string? Signature { public bool DoNotGenerateAcw {get; set;} - /// - /// Gets or sets whether instances of this type are created by managed code before being passed to Java. - /// - /// - /// Build tooling may trim the Java peer when the managed type is unreachable. This should only be set - /// for types that are not independently instantiated or discovered from Java. - /// - public bool IsManagedCreated {get; set;} - public int ApiSince {get; set;} } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 68653981949..a7104170d9f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -228,22 +228,22 @@ public void Build_UserAcwType_IsUnconditional () } [Fact] - public void Build_ManagedCreatedUserAcw_IsTrimmable () + public void Build_BindingEventListenerImplementor_IsTrimmable () { - var peer = MakeAcwPeer ("my/app/Listener", "MyApp.Listener", "App") with { - IsManagedCreated = true, + var peer = MakeAcwPeer ("mono/example/ListenerImplementor", "Example.ListenerImplementor", "Binding") with { + IsBindingEventListenerImplementor = true, }; var model = BuildModel ([peer]); Assert.False (model.Entries [0].IsUnconditional); - Assert.Equal ("MyApp.Listener, App", model.Entries [0].TargetTypeReference); + Assert.Equal ("Example.ListenerImplementor, Binding", model.Entries [0].TargetTypeReference); } [Fact] - public void Build_ManagedCreatedUserAcw_MarkedUnconditional_IsUnconditional () + public void Build_BindingEventListenerImplementor_MarkedUnconditional_IsUnconditional () { - var peer = MakeAcwPeer ("my/app/ManifestListener", "MyApp.ManifestListener", "App") with { - IsManagedCreated = true, + var peer = MakeAcwPeer ("mono/example/ListenerImplementor", "Example.ListenerImplementor", "Binding") with { + IsBindingEventListenerImplementor = true, IsUnconditional = true, }; var model = BuildModel ([peer]); @@ -781,18 +781,18 @@ public class FixtureImplementorsAndDispatchers [Theory] [InlineData ("mono/android/view/View_IOnClickListenerImplementor", true)] [InlineData ("mono/android/view/View_ClickEventDispatcher", false)] - public void Fixture_HelperType_UsesManagedCreatedMetadata (string javaName, bool isManagedCreated) + public void Fixture_HelperType_UsesGeneratedShapeDetection (string javaName, bool isBindingEventListenerImplementor) { var peer = FindFixtureByJavaName (javaName); Assert.False (peer.DoNotGenerateAcw); Assert.False (peer.IsInterface); - Assert.Equal (isManagedCreated, peer.IsManagedCreated); + Assert.Equal (isBindingEventListenerImplementor, peer.IsBindingEventListenerImplementor); var model = BuildModel (new [] { peer }, "TypeMap"); var entry = model.Entries.FirstOrDefault (); Assert.NotNull (entry); - Assert.Equal (!isManagedCreated, entry.IsUnconditional); + 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 4ce7b26ddda..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 () { @@ -130,7 +139,6 @@ public void Scan_JniTypeSignature_IsDiscovered () var peer = FindFixtureByJavaName ("net/dot/jni/test/JavaDisposedObject"); Assert.Equal ("Java.Interop.TestTypes.JavaDisposedObject", peer.ManagedTypeName); Assert.False (peer.DoNotGenerateAcw, "GenerateJavaPeer=true should map to DoNotGenerateAcw=false"); - Assert.True (peer.IsManagedCreated); } [Fact] @@ -138,14 +146,6 @@ public void Scan_JniTypeSignature_DoNotGenerateAcw () { var nonGenerated = FindFixtureByJavaName ("net/dot/jni/test/MyJavaObject"); Assert.True (nonGenerated.DoNotGenerateAcw, "NonGeneratedJavaObject has GenerateJavaPeer=false"); - Assert.False (nonGenerated.IsManagedCreated); - } - - [Fact] - public void Scan_RegisterAttribute_IsManagedCreated () - { - Assert.True (FindFixtureByJavaName ("mono/android/view/View_IOnClickListenerImplementor").IsManagedCreated); - Assert.False (FindFixtureByJavaName ("mono/android/view/View_ClickEventDispatcher").IsManagedCreated); } [Fact] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs index ab269db7a8b..050741c3e20 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs @@ -20,7 +20,6 @@ public sealed class RegisterAttribute : Attribute, Java.Interop.IJniNameProvider public string? Signature { get; set; } public string? Connector { get; set; } public bool DoNotGenerateAcw { get; set; } - public bool IsManagedCreated { get; set; } public int ApiSince { get; set; } public RegisterAttribute (string name) => Name = name; @@ -230,7 +229,6 @@ public sealed class JniTypeSignatureAttribute : Attribute { public string SimpleReference { get; } public bool GenerateJavaPeer { get; set; } = true; - public bool IsManagedCreated { get; set; } public bool IsKeyword { get; set; } public int ArrayRank { get; set; } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index c6c6b8f6d7b..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", IsManagedCreated = true)] - 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")] @@ -1315,7 +1328,7 @@ public class JavaObject public JavaObject () { } } - [Java.Interop.JniTypeSignature ("net/dot/jni/test/JavaDisposedObject", IsManagedCreated = true)] + [Java.Interop.JniTypeSignature ("net/dot/jni/test/JavaDisposedObject")] public class JavaDisposedObject : JavaObject { public JavaDisposedObject () { }