diff --git a/README.md b/README.md index c410e18..74c9cd0 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,36 @@ dotnet test Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Tests/ dotnet test Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.IntegrationTests/ ``` +### Android DEX shrinking & obfuscation (R8) + +Release builds of the Android head run R8 over the Java/DEX side +(`AndroidLinkTool=r8` in `Xamarin.Neo4j.Android.csproj`). Play Console's *DEX code +optimization* report scores obfuscation at ~1% on a stock .NET MAUI build: .NET for +Android runs no DEX shrinker by default, and even with R8 on, the ProGuard config the +SDK generates hardcodes `-dontobfuscate` — a flag nothing later in the config list can +undo. The `_AndroidObfuscateDex` target therefore drops that file from R8's `--pg-conf` +list and substitutes `Xamarin.Neo4j.Android/proguard_xamarin.cfg` (the same file minus +that line), plus a generated file carrying the `-printmapping`/`-keepattributes` tail +the SDK would have appended. Measured on the Release APK: **49.8% of DEX classes +renamed, up from ~0%**. + +Only library-internal classes are renamed: the trimmer emits a `-keep` rule for every +Java type the managed bindings reference, aapt2 one for every class named in a layout +or the manifest, and each Android Callable Wrapper gets its own. +`Xamarin.Neo4j.Android/proguard.cfg` covers what those miss — things resolved by name +from native code. Add to it if a Release build dies with `ClassNotFoundException`, +`NoSuchFieldError` or `NoSuchMethodError` where a Debug build does not. + +`mapping.txt` is embedded in the AAB +(`BUNDLE-METADATA/com.android.tools.build.obfuscation/proguard.map`), so Play Console +retraces obfuscated Java stacks by itself. Managed (C#) stack traces are unaffected — +R8 only touches DEX. + +Local Release builds must be clean: aapt2's keep rules are registered as a `FileWrite`, +so an incremental build that skips the resource link has `IncrementalClean` delete them. +The target caches a copy outside `FileWrites` and errors out if even that is missing — +delete `Xamarin.Neo4j.Android/obj/Release` if it fires. + ## Technology Stack - .NET MAUI diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Xamarin.Neo4j.Android.csproj b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Xamarin.Neo4j.Android.csproj index 4e87c99..7092d5f 100644 --- a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Xamarin.Neo4j.Android.csproj +++ b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/Xamarin.Neo4j.Android.csproj @@ -53,4 +53,56 @@ + + + r8 + + + + + + + + + + <_AndroidObfuscationProguardConfig>$(IntermediateOutputPath)proguard\proguard_obfuscate.cfg + <_AndroidAaptProguardRules>$(IntermediateOutputPath)proguard\aapt_rules.cached.txt + + + <_AndroidObfuscationProguardLines Include="-ignorewarnings" Condition="'$(AndroidR8IgnoreWarnings)' == 'True'" /> + <_AndroidObfuscationProguardLines Include="-keepattributes SourceFile" /> + <_AndroidObfuscationProguardLines Include="-keepattributes LineNumberTable" /> + + <_AndroidObfuscationProguardLines Include="-printmapping "$([System.IO.Path]::Combine('$(MSBuildProjectDirectory)', '$(AndroidProguardMappingFile)'))"" Condition="'$(AndroidProguardMappingFile)' != ''" /> + + + + + + + + <_ProguardConfiguration Remove="$(IntermediateOutputPath)proguard\proguard_xamarin.cfg" /> + <_ProguardConfiguration Remove="$(IntermediateOutputPath)aapt_rules.txt" /> + <_ProguardConfiguration Include="$(MSBuildProjectDirectory)\proguard_xamarin.cfg" /> + <_ProguardConfiguration Include="$(_AndroidObfuscationProguardConfig)" /> + <_ProguardConfiguration Include="$(_AndroidAaptProguardRules)" /> + + + diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/proguard.cfg b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/proguard.cfg new file mode 100644 index 0000000..95bacd1 --- /dev/null +++ b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/proguard.cfg @@ -0,0 +1,22 @@ +# App-level R8 keep rules, on top of the ones .NET for Android generates +# (see proguard_xamarin.cfg and the _AndroidObfuscateDex target in the csproj). + +# The .NET runtime resolves this one by name from native code -- no DEX +# reference points at it, so R8 renames it and startup dies with +# "ClassNotFoundException: net.dot.android.ApplicationRegistration". +# The SDK's own config only covers net.dot.jni.** and net.dot.android.crypto.**, +# because upstream never obfuscates. +-keep class net.dot.android.** { *; (...); } + +# The trimmer's generated keep rules (proguard_project_references.cfg) cover the +# Java *methods* the bindings call, but not their fields -- upstream never +# obfuscates, so the gap never showed. A bound property backed by a Java field +# reads it through JNI by name, and renaming breaks that: +# NoSuchFieldError: no "Landroidx/lifecycle/Lifecycle$State;" field "DESTROYED" +# NoSuchFieldError: no "I" field "left" in class "Landroidx/core/graphics/Insets;" +# Only the API surface a binding can reach needs it; private fields still get +# renamed, and class names -- what Play's report counts -- are untouched by this. +-keepclassmembers class * { + public ; + protected ; +} diff --git a/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/proguard_xamarin.cfg b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/proguard_xamarin.cfg new file mode 100644 index 0000000..14980b6 --- /dev/null +++ b/Xamarin.Neo4j/Xamarin.Neo4j/Xamarin.Neo4j.Android/proguard_xamarin.cfg @@ -0,0 +1,45 @@ +# Verbatim copy of the ProGuard configuration .NET for Android generates at +# obj///proguard/proguard_xamarin.cfg, minus its hardcoded +# "-dontobfuscate". ProGuard has no switch that undoes that flag, so the only +# way to let R8 rename DEX classes is to drop the file that sets it from R8's +# --pg-conf list and substitute this one (see the _AndroidObfuscateDex target +# in the csproj). +# +# Taken from Microsoft.Android.Sdk 36.1.69 (.NET 10). When the Android SDK pack +# is updated, diff this against the generated file after a Release build -- +# a keep rule added upstream and missed here means a runtime JNI failure. +# The trailing -ignorewarnings/-keepattributes/-printmapping lines the SDK +# appends to that file are regenerated by the csproj target instead. + +-keep class android.support.multidex.MultiDexApplication { (); } +-keep class net.dot.jni.** { *; (); } +-keep class mono.MonoRuntimeProvider* { *; (...); } +-keep class mono.MonoPackageManager { *; (...); } +-keep class mono.MonoPackageManager_Resources { *; (...); } +-keep class mono.android.** { *; (...); } +-keep class mono.java.** { *; (...); } +-keep class mono.javax.** { *; (...); } +-keep class net.dot.jni.ManagedPeer { *; (...); } +-keep class xamarin.android.net.ServerCertificateCustomValidator_TrustManager { *; (...); } +-keep class xamarin.android.net.ServerCertificateCustomValidator_TrustManager_FakeSSLSession { *; (...); } +-keep class xamarin.android.net.ServerCertificateCustomValidator_AlwaysAcceptingHostnameVerifier { *; (...); } + +-keep class android.runtime.** { (...); } +-keep class assembly_mono_android.android.runtime.** { (...); } +# hash for android.runtime and assembly_mono_android.android.runtime. +-keep class md52ce486a14f4bcd95899665e9d932190b.** { *; (...); } +-keepclassmembers class md52ce486a14f4bcd95899665e9d932190b.** { *; (...); } + +# .NET runtime +-keep class net.dot.android.crypto.** { *; (...); } + +# Android's template misses fluent setters... +-keepclassmembers class * extends android.view.View { + *** set*(...); +} + +# also misses those inflated custom layout stuff from xml... +-keepclassmembers class * extends android.view.View { + (android.content.Context,android.util.AttributeSet); + (android.content.Context,android.util.AttributeSet,int); +}