Skip to content

Commit bd95226

Browse files
[trimming] preserve custom views and $(AndroidHttpClientHandlerType) (#8954)
Fixes: #8797 Here are two cases `TrimMode=full` can break applications: * `$(AndroidHttpClientHandlerType)` set to a custom type * Custom views (Android `.xml`) that are not referenced in C# code In the `MarkJavaObjects` trimmer step we can preserve both of these cases by: * Passing in `$(AndroidHttpClientHandlerType)`, preserve the public, parameterless constructor of the type * Pass in `$(_CustomViewMapFile)`, preserve `IJavaObject` types if they are found in the map file Filed an issue to address the usage of `Foo` in tests in the future: * #9008
1 parent a954a33 commit bd95226

File tree

9 files changed

+135
-27
lines changed

9 files changed

+135
-27
lines changed

src/Microsoft.Android.Sdk.ILLink/MarkJavaObjects.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Mono.Linker;
66
using Mono.Linker.Steps;
77
using Mono.Tuner;
8+
using Java.Interop.Tools.Cecil;
89
using Xamarin.Android.Tasks;
910

1011
namespace MonoDroid.Tuner {
@@ -16,9 +17,46 @@ public class MarkJavaObjects : BaseMarkHandler
1617
public override void Initialize (LinkContext context, MarkContext markContext)
1718
{
1819
base.Initialize (context, markContext);
20+
context.TryGetCustomData ("AndroidHttpClientHandlerType", out string androidHttpClientHandlerType);
21+
context.TryGetCustomData ("AndroidCustomViewMapFile", out string androidCustomViewMapFile);
22+
var customViewMap = MonoAndroidHelper.LoadCustomViewMapFile (androidCustomViewMapFile);
23+
24+
markContext.RegisterMarkAssemblyAction (assembly => ProcessAssembly (assembly, androidHttpClientHandlerType, customViewMap));
1925
markContext.RegisterMarkTypeAction (type => ProcessType (type));
2026
}
2127

28+
bool IsActiveFor (AssemblyDefinition assembly)
29+
{
30+
return assembly.MainModule.HasTypeReference ("System.Net.Http.HttpMessageHandler") || assembly.MainModule.HasTypeReference ("Android.Util.IAttributeSet");
31+
}
32+
33+
public void ProcessAssembly (AssemblyDefinition assembly, string androidHttpClientHandlerType, Dictionary<string, HashSet<string>> customViewMap)
34+
{
35+
if (!IsActiveFor (assembly))
36+
return;
37+
38+
foreach (var type in assembly.MainModule.Types) {
39+
// Custom HttpMessageHandler
40+
if (!string.IsNullOrEmpty (androidHttpClientHandlerType) &&
41+
androidHttpClientHandlerType.StartsWith (type.Name, StringComparison.Ordinal)) {
42+
var assemblyQualifiedName = type.GetPartialAssemblyQualifiedName (Context);
43+
if (assemblyQualifiedName == androidHttpClientHandlerType) {
44+
Annotations.Mark (type);
45+
PreservePublicParameterlessConstructors (type);
46+
continue;
47+
}
48+
}
49+
50+
// Custom views in Android .xml files
51+
if (!type.ImplementsIJavaObject (cache))
52+
continue;
53+
if (customViewMap.ContainsKey (type.FullName)) {
54+
Annotations.Mark (type);
55+
PreserveJavaObjectImplementation (type);
56+
}
57+
}
58+
}
59+
2260
public void ProcessType (TypeDefinition type)
2361
{
2462
// If this isn't a JLO or IJavaObject implementer,
@@ -48,6 +86,21 @@ void PreserveJavaObjectImplementation (TypeDefinition type)
4886
PreserveInterfaces (type);
4987
}
5088

89+
void PreservePublicParameterlessConstructors (TypeDefinition type)
90+
{
91+
if (!type.HasMethods)
92+
return;
93+
94+
foreach (var constructor in type.Methods)
95+
{
96+
if (!constructor.IsConstructor || constructor.IsStatic || !constructor.IsPublic || constructor.HasParameters)
97+
continue;
98+
99+
PreserveMethod (type, constructor);
100+
break; // We can stop when found
101+
}
102+
}
103+
51104
void PreserveAttributeSetConstructor (TypeDefinition type)
52105
{
53106
if (!type.HasMethods)

src/Mono.Android/Android.Runtime/AndroidEnvironment.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -336,9 +336,7 @@ static IWebProxy GetDefaultProxy ()
336336
[DynamicDependency (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor, typeof (Xamarin.Android.Net.AndroidMessageHandler))]
337337
static object GetHttpMessageHandler ()
338338
{
339-
// FIXME: https://github.com/xamarin/xamarin-android/issues/8797
340-
// Note that this is a problem for custom $(AndroidHttpClientHandlerType) or $XA_HTTP_CLIENT_HANDLER_TYPE
341-
[UnconditionalSuppressMessage ("Trimming", "IL2057", Justification = "DynamicDependency should preserve AndroidMessageHandler.")]
339+
[UnconditionalSuppressMessage ("Trimming", "IL2057", Justification = "Preserved by the MarkJavaObjects trimmer step.")]
342340
[return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
343341
static Type TypeGetType (string typeName) =>
344342
Type.GetType (typeName, throwOnError: false);

src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ _ResolveAssemblies MSBuild target.
9696
;_OuterIntermediateAssembly=@(IntermediateAssembly)
9797
;_OuterOutputPath=$(OutputPath)
9898
;_OuterIntermediateOutputPath=$(IntermediateOutputPath)
99+
;_OuterCustomViewMapFile=$(_CustomViewMapFile)
99100
</_AdditionalProperties>
100101
<_AndroidBuildRuntimeIdentifiersInParallel Condition=" '$(_AndroidBuildRuntimeIdentifiersInParallel)' == '' ">true</_AndroidBuildRuntimeIdentifiersInParallel>
101102
</PropertyGroup>

src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.ILLink.targets

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ This file contains the .NET 5-specific targets to customize ILLink
3434
Used for the <ILLink CustomData="@(_TrimmerCustomData)" /> value:
3535
https://github.com/dotnet/sdk/blob/a5393731b5b7b225692fff121f747fbbc9e8b140/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.ILLink.targets#L147
3636
-->
37+
<_TrimmerCustomData Include="AndroidHttpClientHandlerType" Value="$(AndroidHttpClientHandlerType)" />
38+
<_TrimmerCustomData Include="AndroidCustomViewMapFile" Value="$(_OuterCustomViewMapFile)" />
3739
<_TrimmerCustomData Include="XATargetFrameworkDirectories" Value="$(_XATargetFrameworkDirectories)" />
3840
<_TrimmerCustomData
3941
Condition=" '$(_ProguardProjectConfiguration)' != '' "

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LinkerTests.cs

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,14 +156,53 @@ static AssemblyDefinition CreateFauxMonoAndroidAssembly ()
156156
return assm;
157157
}
158158

159-
private void PreserveCustomHttpClientHandler (string handlerType, string handlerAssembly, string testProjectName, string assemblyPath)
159+
private void PreserveCustomHttpClientHandler (
160+
string handlerType,
161+
string handlerAssembly,
162+
string testProjectName,
163+
string assemblyPath,
164+
TrimMode trimMode)
160165
{
161-
var proj = new XamarinAndroidApplicationProject () { IsRelease = true };
166+
testProjectName += trimMode.ToString ();
167+
168+
var class_library = new XamarinAndroidLibraryProject {
169+
IsRelease = true,
170+
ProjectName = "MyClassLibrary",
171+
Sources = {
172+
new BuildItem.Source ("MyCustomHandler.cs") {
173+
TextContent = () => """
174+
class MyCustomHandler : System.Net.Http.HttpMessageHandler
175+
{
176+
protected override Task <HttpResponseMessage> SendAsync (HttpRequestMessage request, CancellationToken cancellationToken) =>
177+
throw new NotImplementedException ();
178+
}
179+
"""
180+
},
181+
new BuildItem.Source ("Bar.cs") {
182+
TextContent = () => "public class Bar { }",
183+
}
184+
}
185+
};
186+
using (var libBuilder = CreateDllBuilder ($"{testProjectName}/{class_library.ProjectName}")) {
187+
Assert.IsTrue (libBuilder.Build (class_library), $"Build for {class_library.ProjectName} should have succeeded.");
188+
}
189+
190+
var proj = new XamarinAndroidApplicationProject {
191+
ProjectName = "MyApp",
192+
IsRelease = true,
193+
TrimModeRelease = trimMode,
194+
Sources = {
195+
new BuildItem.Source ("Foo.cs") {
196+
TextContent = () => "public class Foo : Bar { }",
197+
}
198+
}
199+
};
200+
proj.AddReference (class_library);
162201
proj.AddReferences ("System.Net.Http");
163202
string handlerTypeFullName = string.IsNullOrEmpty(handlerAssembly) ? handlerType : handlerType + ", " + handlerAssembly;
164203
proj.SetProperty (proj.ActiveConfigurationProperties, "AndroidHttpClientHandlerType", handlerTypeFullName);
165204
proj.MainActivity = proj.DefaultMainActivity.Replace ("base.OnCreate (bundle);", "base.OnCreate (bundle);\nvar client = new System.Net.Http.HttpClient ();");
166-
using (var b = CreateApkBuilder (testProjectName)) {
205+
using (var b = CreateApkBuilder ($"{testProjectName}/{proj.ProjectName}")) {
167206
Assert.IsTrue (b.Build (proj), "Build should have succeeded.");
168207

169208
using (var assembly = AssemblyDefinition.ReadAssembly (Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath, assemblyPath))) {
@@ -173,12 +212,14 @@ private void PreserveCustomHttpClientHandler (string handlerType, string handler
173212
}
174213

175214
[Test]
176-
public void PreserveCustomHttpClientHandlers ()
215+
public void PreserveCustomHttpClientHandlers ([Values (TrimMode.Partial, TrimMode.Full)] TrimMode trimMode)
177216
{
178217
PreserveCustomHttpClientHandler ("Xamarin.Android.Net.AndroidMessageHandler", "",
179-
"temp/PreserveAndroidMessageHandler", "android-arm64/linked/Mono.Android.dll");
218+
"temp/PreserveAndroidMessageHandler", "android-arm64/linked/Mono.Android.dll", trimMode);
180219
PreserveCustomHttpClientHandler ("System.Net.Http.SocketsHttpHandler", "System.Net.Http",
181-
"temp/PreserveSocketsHttpHandler", "android-arm64/linked/System.Net.Http.dll");
220+
"temp/PreserveSocketsHttpHandler", "android-arm64/linked/System.Net.Http.dll", trimMode);
221+
PreserveCustomHttpClientHandler ("MyCustomHandler", "MyClassLibrary",
222+
"temp/MyCustomHandler", "android-arm64/linked/MyClassLibrary.dll", trimMode);
182223
}
183224

184225
[Test]

src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.Linker.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,24 @@ public static bool ExistsInFrameworkPath (string assembly)
6060
.Select (p => Path.GetDirectoryName (p.TrimEnd (Path.DirectorySeparatorChar)))
6161
.Any (p => assembly.StartsWith (p, StringComparison.OrdinalIgnoreCase));
6262
}
63+
64+
static readonly char [] CustomViewMapSeparator = [';'];
65+
66+
public static Dictionary<string, HashSet<string>> LoadCustomViewMapFile (string mapFile)
67+
{
68+
var map = new Dictionary<string, HashSet<string>> ();
69+
if (!File.Exists (mapFile))
70+
return map;
71+
foreach (var s in File.ReadLines (mapFile)) {
72+
var items = s.Split (CustomViewMapSeparator, count: 2);
73+
var key = items [0];
74+
var value = items [1];
75+
HashSet<string> set;
76+
if (!map.TryGetValue (key, out set))
77+
map.Add (key, set = new HashSet<string> ());
78+
set.Add (value);
79+
}
80+
return map;
81+
}
6382
}
6483
}

src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -408,19 +408,7 @@ public static Dictionary<string, HashSet<string>> LoadCustomViewMapFile (IBuildE
408408
var cachedMap = engine?.GetRegisteredTaskObjectAssemblyLocal<Dictionary<string, HashSet<string>>> (mapFile, RegisteredTaskObjectLifetime.Build);
409409
if (cachedMap != null)
410410
return cachedMap;
411-
var map = new Dictionary<string, HashSet<string>> ();
412-
if (!File.Exists (mapFile))
413-
return map;
414-
foreach (var s in File.ReadLines (mapFile)) {
415-
var items = s.Split (new char [] { ';' }, count: 2);
416-
var key = items [0];
417-
var value = items [1];
418-
HashSet<string> set;
419-
if (!map.TryGetValue (key, out set))
420-
map.Add (key, set = new HashSet<string> ());
421-
set.Add (value);
422-
}
423-
return map;
411+
return LoadCustomViewMapFile (mapFile);
424412
}
425413

426414
public static bool SaveCustomViewMapFile (IBuildEngine4 engine, string mapFile, Dictionary<string, HashSet<string>> map)

tests/Mono.Android-Tests/Android.Widget/CustomWidgetTests.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using System.Diagnostics.CodeAnalysis;
2-
using Android.App;
1+
using Android.App;
32
using Android.Content;
43
using Android.Util;
54
using Android.Views;
@@ -12,9 +11,14 @@ namespace Xamarin.Android.RuntimeTests
1211
[TestFixture]
1312
public class CustomWidgetTests
1413
{
14+
public CustomWidgetTests()
15+
{
16+
// FIXME: https://github.com/xamarin/xamarin-android/issues/9008
17+
new Foo ();
18+
}
19+
1520
// https://bugzilla.xamarin.com/show_bug.cgi?id=23880
1621
[Test]
17-
[DynamicDependency (DynamicallyAccessedMemberTypes.All, typeof (CustomTextView))]
1822
public void UpperCaseCustomWidget_ShouldNotThrowInflateException ()
1923
{
2024
Assert.DoesNotThrow (() => {
@@ -24,7 +28,6 @@ public void UpperCaseCustomWidget_ShouldNotThrowInflateException ()
2428
}
2529

2630
[Test]
27-
[DynamicDependency (DynamicallyAccessedMemberTypes.All, typeof (CustomTextView))]
2831
public void LowerCaseCustomWidget_ShouldNotThrowInflateException ()
2932
{
3033
Assert.DoesNotThrow (() => {
@@ -34,7 +37,6 @@ public void LowerCaseCustomWidget_ShouldNotThrowInflateException ()
3437
}
3538

3639
[Test]
37-
[DynamicDependency (DynamicallyAccessedMemberTypes.All, typeof (CustomTextView))]
3840
public void UpperAndLowerCaseCustomWidget_FromLibrary_ShouldNotThrowInflateException ()
3941
{
4042
Assert.DoesNotThrow (() => {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
namespace Mono.Android_Test.Library;
2+
3+
// FIXME: https://github.com/xamarin/xamarin-android/issues/9008
4+
public class Foo { }

0 commit comments

Comments
 (0)