diff --git a/amethyst/proguard-rules.pro b/amethyst/proguard-rules.pro index 5f6e347e2f..99de8a27e3 100644 --- a/amethyst/proguard-rules.pro +++ b/amethyst/proguard-rules.pro @@ -65,9 +65,25 @@ # Generic signatures, plus the inner/enclosing-class links that travel with them. # -# Signature is the load-bearing one: jacksonTypeRefOf() resolves generic types -# through it at 18 call sites, and without it List erases to List and every -# element comes back a LinkedHashMap. +# Signature carries the generic type arguments R8 would otherwise erase. +# +# It is NOT enough to make `object : TypeReference() {}` work under full mode +# (android.enableR8.fullMode=true). Measured on device: JacksonMapper's +# built its JavaTypes through jacksonTypeRefOf() and threw +# +# IllegalArgumentException: Internal error: TypeReference constructed without +# actual type information +# +# which poisons the class -- every later use is NoClassDefFoundError, so nothing +# could be signed or sent. Keeping the anonymous subclasses did not help either +# (tried -keep,allowobfuscation and a full -keep ... { *; }). The fix was to stop +# asking Jackson to read the type back off the class: JacksonMapper now builds +# those JavaTypes with TypeFactory.constructType/constructCollectionType/ +# constructParametricType, which take the Class objects directly. +# +# Anything else that still resolves a generic type reflectively is exposed the +# same way -- notably JacksonMapper.fromJsonTo, JsonMapperNip55 (NIP-55) and +# the NIP-46 bunker path, which were not reachable in this test run. # # All three are ALSO in AGP's proguard-android-optimize.txt, which keeps # AnnotationDefault, EnclosingMethod, InnerClasses, Signature and the three @@ -198,3 +214,21 @@ # generated neighbours — cheap enough not to be worth proving unnecessary # against a pre-stable (alpha) library. -keep class com.vitorpamplona.amethyst.appfunctions.** { *; } + +# ----------------------------------------------------------------------------- +# Enums used as navigation-route ARGUMENTS +# ----------------------------------------------------------------------------- +# androidx.navigation's type-safe routes resolve an enum argument by its +# fully-qualified class name (NavTypeConverter.parseEnum/parseNullableEnum call +# Class.forName on the serial name). R8 renames the class, so building the nav +# graph throws and the app cannot get past login: +# +# IllegalArgumentException: Cannot find class with name +# "...routes.DiscoverTab?". Ensure that the serialName for this argument is +# the default fully qualified name. +# +# The enum FIELDS are already pinned by the blanket `-keepclassmembers enum *` +# above; that rule deliberately lets the CLASS be renamed, which is exactly what +# breaks here. Every enum used as a route argument needs its name too. +-keep class com.vitorpamplona.amethyst.ui.navigation.routes.DiscoverTab { *; } +-keep class com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.BookmarkType { *; } diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt index 570eb1d21c..62f3ec72d6 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt @@ -101,14 +101,28 @@ class JacksonMapper { /** * Shortcuts + * + * Built from Class objects, NOT from jacksonTypeRefOf(). A TypeReference + * reads its type argument back off the anonymous subclass's generic + * superclass, and R8 in full mode does not keep that -- not with + * `-keepattributes Signature`, and not with a `-keep` on the subclasses + * either (both were tried on device). It failed here, in , with + * + * IllegalArgumentException: Internal error: TypeReference constructed + * without actual type information + * + * and a failed is permanent: every later touch of this class + * throws NoClassDefFoundError, so the release build could not hash or sign + * a single event. TypeFactory takes the Class objects directly, so there is + * nothing for R8 to erase. */ - val eventTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRefOf()) - val tagArrayTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRefOf()) - val rumorTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRefOf()) - val eventTemplateTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRefOf>()) - val eventListTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRefOf>()) - val messageTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRefOf()) - val commandTypeInstance: JavaType = mapper.typeFactory.constructType(jacksonTypeRefOf()) + val eventTypeInstance: JavaType = mapper.typeFactory.constructType(Event::class.java) + val tagArrayTypeInstance: JavaType = mapper.typeFactory.constructType(Array>::class.java) + val rumorTypeInstance: JavaType = mapper.typeFactory.constructType(Rumor::class.java) + val eventTemplateTypeInstance: JavaType = mapper.typeFactory.constructParametricType(EventTemplate::class.java, Event::class.java) + val eventListTypeInstance: JavaType = mapper.typeFactory.constructCollectionType(List::class.java, Event::class.java) + val messageTypeInstance: JavaType = mapper.typeFactory.constructType(Message::class.java) + val commandTypeInstance: JavaType = mapper.typeFactory.constructType(Command::class.java) fun fromJson(json: String): Event = mapper.readValue(json, eventTypeInstance) diff --git a/tools/r8-verify/reflection-contract.txt b/tools/r8-verify/reflection-contract.txt index bdc06ba5d5..f022954dfa 100644 --- a/tools/r8-verify/reflection-contract.txt +++ b/tools/r8-verify/reflection-contract.txt @@ -102,6 +102,19 @@ class com.vitorpamplona.amethyst.service.calendar.CalendarReminderWorker # every field name as a string literal — so there is no rule to verify. +# --- Enums used as navigation-route arguments: the CLASS name is the lookup --- +# androidx.navigation resolves an enum route argument by fully-qualified class +# name (NavTypeConverter calls Class.forName on the serial name). Renaming the +# class throws while the nav graph is being built, so the app cannot get past +# login at all -- release-only, and total. +# +# These need the CLASS pinned, which the `-keepclassmembers enum *` rule below +# deliberately does not do: it keeps constant names and lets the class be +# renamed. Add a line here whenever an enum becomes a route argument. +class com.vitorpamplona.amethyst.ui.navigation.routes.DiscoverTab +class com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarkgroups.BookmarkType + + # --- Enum constants persisted as strings ------------------------------------- # The preference stores write `enum.name` into DataStore and read it back with # valueOf(). A renamed constant resets that setting for every existing user on