From 0dee27d6cb21434fed1cf67aaf33dec2eebcef56 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Wed, 14 Feb 2018 05:50:23 -0500 Subject: [PATCH] [Xamarin.Android.Build.Tasks] Fragments & Shorter acw-map (#1301) Fixes: https://github.com/xamarin/xamarin-android/issues/1296 Xamarin.Android attempts to expose the Java-based Android API as a ".NET feeling" API. This takes many forms, such as prefixing interface names with `I`, mapping `get` and `set` methods to properties, mapping listener interfaces to events, and PascalCasing method names. This also affects Java package names and C# namespaces. When creating the `.apk` file, we philosophically need to go the opposite direction: PascalCased members need to be mapped to "something appropriate" within Java. For example, many Android Resource ids *must* be all lowercase, and Android doesn't support package names starting with an uppercase letter in all circumstances. At one point, we tried mapping C# PascalCased namespaces to camelCased namespaces, so e.g. `MyExampleNamespace` became the `myExampleNamesapce` Java package within Java Callable Wrappers. This turned out to be a Terrible Mistake, particularly on case-sensitive filesystems, as if casing was *inconsistent* (`MyExampleNamespace` vs `MyExamplenamespace`), files might not be packaged. By Mono for Android 1.0, we settled on just lowercasing the namespace name to produce Java package names within Android Callable Wrappers. This was not without it's own problems; in particular, the assembly name wasn't involved, so if the "same" type (namespace + type) were present in two different assemblies and we needed to generate Android Callable Wrappers, they'd "collide," the build would fail, and we'd have some unhappy customers. This was later addressed in Xamarin.Android 5.1 by changing the Java package name generation algorithm to be an MD5SUM of the assembly name and namespace name, thus allowing types to have assembly identity. (This was not without its own problems.) Then it gets slightly more complicated: Android allows type names to appear in various locations, such as in layout View XML. These don't allow "just" using the type name; the package name is required for types outside the `android.widget` Java package. Initially, we did nothing, so developers had to directly use the Android Callable Wrapper names: With the change in Xamarin.Android 5.1, *this couldn't work*; those types didn't exist anymore. To square this circle, we processed the resource files to "fixup" identifiers and replace them with the actual Android Callable Wrapper names. We'd replace any/all of: MyExampleNamespace.MyCustomCSharpView // Full name MyExampleNamespace.MyCustomCSharpView, MyAssembly // Partial assembly-qualified name MyExampleNamespace.MyCustomCSharpView, MyAssembly, Version=... // Full assembly qualified name myexamplenamespace.MyCustomCSharpView // compatibility name with the appropriate md5'd Android Callable Wrapper name. Brilliant as this was, there was a problem: [Bug #61073][61073]. If the assembly had a wildcard in the assembly version: [61073]: https://bugzilla.xamarin.com/show_bug.cgi?id=61073 [assembly: AssemblyVersion ("1.0.0.*")] then the "Full assembly qualified name" value would change on *every build*, which had numerious unintended knock-on effects. This was fixed in commit e5b1c92c, which worked largely by just killing the Full assembly qualified name version entirely. Xamarin.Android doesn't support embedding two different versions of the same assembly, so this was considered to be fine. ...except for one compatibility case: ``s can contain ~arbitrary strings, and we support replacing the entire Full assembly qualified name within them: However, in a post e5b1c92c world, the above now fails to load on the device, because it's *not* being appropriately fixed up! FATAL EXCEPTION: main Process: Mono.Samples.HelloTests, PID: 22977 java.lang.RuntimeException: Unable to start activity ComponentInfo{Mono.Samples.HelloTests/mono.samples.HelloApp}: android.view.InflateException: Binary XML file line #1: Binary XML file line #1: Error inflating class fragment ... Caused by: android.view.InflateException: Binary XML file line #1: Binary XML file line #1: Error inflating class fragment Caused by: android.view.InflateException: Binary XML file line #1: Error inflating class fragment Caused by: android.app.Fragment$InstantiationException: Unable to instantiate fragment Mono.Samples.Hello.MyFragment, Hello, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null: make sure class name exists, is public, and has an empty constructor that is public ... Caused by: java.lang.ClassNotFoundException: Didn't find class "Mono.Samples.Hello.MyFragment, Hello, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" on path: DexPathList[[zip file "/data/app/Mono.Samples.HelloTests-1/base.apk"],nativeLibraryDirectories=[/data/app/Mono.Samples.HelloTests-1/lib/arm64, /system/fake-libs64, /data/app/Mono.Samples.HelloTests-1/base.apk!/lib/arm64-v8a, /system/lib64, /vendor/lib64]] ... The problem is that what Android "sees" *should* be where `md5whatever.MyCustomCSharpFragment` *is* a valid Java type that Android is able to load successfully, but because of e5b1c92c this replacement was removed. The fix: simplify the Full assembly-qualified name case to an already supported example. If the `//fragment/@android:name` value contains a `,`, assume it's an assembly qualified name and compute the Full name from it, by stripping off the comma and everything after it, then use the Full name to lookup the correct Android Callable Wrapper type. --- .../Utilities/AndroidResource.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/AndroidResource.cs b/src/Xamarin.Android.Build.Tasks/Utilities/AndroidResource.cs index 629be141b66..e9bcb993d9b 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/AndroidResource.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/AndroidResource.cs @@ -192,6 +192,15 @@ private static bool TryFixFragment (XAttribute attr, Dictionary return true; } + else if (attr.Value?.Contains (',') ?? false) { + // attr.Value could be an assembly-qualified name that isn't in acw-map.txt; + // see e5b1c92c, https://github.com/xamarin/xamarin-android/issues/1296#issuecomment-365091948 + var n = attr.Value.Substring (0, attr.Value.IndexOf (',')); + if (acwMap.ContainsKey (n)) { + attr.Value = acwMap [n]; + return true; + } + } } return false;