From 1297bf0ebf40e7574fa0684f680a88bb1fd3b562 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 10 Jul 2024 16:46:11 -0500 Subject: [PATCH] [Microsoft.Android.Sdk.ILLink] preserve types with `IJniNameProviderAttribute` Fixes: https://github.com/dotnet/android/issues/8940 Context: https://github.com/TobiasBuchholz/Plugin.Firebase/issues/144 Using the NuGet package: Includes a service: namespace Plugin.Firebase.CloudMessaging.Platforms.Android; [Service(Exported = true)] [IntentFilter(new[] { "com.google.firebase.MESSAGING_EVENT" })] public class MyFirebaseMessagingService : FirebaseMessagingService Unfortunately, using `TrimMode=full` completely trims away the above service, which is required for push notifications to work. I could reproduce this problem in a test using the above NuGet package. To fix this, we can modify `MarkJavaObjects` to preserve types with attributes that implement `Java.Interop.IJniNameProviderAttribute`, and the new test now passes. --- .../MarkJavaObjects.cs | 37 +++++++++++++++++-- .../Tasks/LinkerTests.cs | 34 +++++++++++++++++ 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Android.Sdk.ILLink/MarkJavaObjects.cs b/src/Microsoft.Android.Sdk.ILLink/MarkJavaObjects.cs index 290f45a70e6..416027f4f19 100644 --- a/src/Microsoft.Android.Sdk.ILLink/MarkJavaObjects.cs +++ b/src/Microsoft.Android.Sdk.ILLink/MarkJavaObjects.cs @@ -4,9 +4,9 @@ using Mono.Cecil; using Mono.Linker; using Mono.Linker.Steps; -using Mono.Tuner; using Java.Interop.Tools.Cecil; using Xamarin.Android.Tasks; +using Profile = Microsoft.Android.Sdk.ILLink.Profile; namespace MonoDroid.Tuner { @@ -27,7 +27,14 @@ public override void Initialize (LinkContext context, MarkContext markContext) bool IsActiveFor (AssemblyDefinition assembly) { - return assembly.MainModule.HasTypeReference ("System.Net.Http.HttpMessageHandler") || assembly.MainModule.HasTypeReference ("Android.Util.IAttributeSet"); + if (Profile.IsSdkAssembly (assembly)) + return false; + if (Profile.IsProductAssembly (assembly)) + return false; + + return assembly.MainModule.HasTypeReference ("System.Net.Http.HttpMessageHandler") || + assembly.MainModule.HasTypeReference ("Java.Lang.Object") || + assembly.MainModule.HasTypeReference ("Android.Util.IAttributeSet"); } public void ProcessAssembly (AssemblyDefinition assembly, string androidHttpClientHandlerType, Dictionary> customViewMap) @@ -47,16 +54,40 @@ public void ProcessAssembly (AssemblyDefinition assembly, string androidHttpClie } } - // Custom views in Android .xml files + // Continue if not an IJavaObject if (!type.ImplementsIJavaObject (cache)) continue; + + // Custom views in Android .xml files if (customViewMap.ContainsKey (type.FullName)) { Annotations.Mark (type); PreserveJavaObjectImplementation (type); + continue; + } + + // Types with Java.Interop.IJniNameProviderAttribute attributes + if (ShouldPreserveBasedOnAttributes (type)) { + Annotations.Mark (type); + PreserveJavaObjectImplementation (type); + continue; } } } + bool ShouldPreserveBasedOnAttributes (TypeDefinition type) + { + if (!type.HasCustomAttributes) + return false; + + foreach (var attr in type.CustomAttributes) { + if (attr.AttributeType.Implements ("Java.Interop.IJniNameProviderAttribute", cache)) { + return true; + } + } + + return false; + } + public void ProcessType (TypeDefinition type) { // If this isn't a JLO or IJavaObject implementer, diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LinkerTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LinkerTests.cs index d91ed72976f..2be89006b6a 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LinkerTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LinkerTests.cs @@ -598,6 +598,40 @@ public void PreserveIX509TrustManagerSubclasses ([Values(true, false)] bool hasS } } + [Test] + public void PreserveServices () + { + var proj = new XamarinAndroidApplicationProject { + IsRelease = true, + TrimModeRelease = TrimMode.Full, + PackageReferences = { + new Package { Id = "Plugin.Firebase.CloudMessaging", Version = "3.0.0" }, + } + }; + proj.MainActivity = proj.DefaultMainActivity + .Replace ("//${FIELDS}", + """ + protected override void OnNewIntent(Android.Content.Intent? intent) + { + base.OnNewIntent(intent); + Plugin.Firebase.CloudMessaging.FirebaseCloudMessagingImplementation.OnNewIntent(intent); + } + """) + .Replace ("//${AFTER_ONCREATE}", "Plugin.Firebase.Core.Platforms.Android.CrossFirebase.Initialize (this);"); + + using var b = CreateApkBuilder (); + Assert.IsTrue (b.Build (proj), "Build should have succeeded."); + + var assemblyPath = BuildTest.GetLinkedPath (b, isRelease: true, "Plugin.Firebase.CloudMessaging.dll"); + FileAssert.Exists (assemblyPath); + using var assembly = AssemblyDefinition.ReadAssembly (assemblyPath); + var types = new [] { "Plugin.Firebase.CloudMessaging.Platforms.Android.MyFirebaseMessagingService" }; + foreach (var typeName in types) { + var td = assembly.MainModule.GetType (typeName); + Assert.IsNotNull (td, $"{typeName} should not have been linked out!"); + } + } + [Test] public void DoNotErrorOnPerArchJavaTypeDuplicates ([Values(true, false)] bool enableMarshalMethods) {