androidandroid-r8

Android app crashes (ClassCastException) after R8 optimization


I'm trying to enable code optimizations and obfuscation with R8 in my application, but it causes it to crash.

When the app runs after being optimized, the following crash occurs:

FATAL EXCEPTION: StickyMoteurThread  
Process: com.tentaclestudio.fantasyracing.debug, PID: 11086  
java.lang.ClassCastException: java.lang.String cannot be cast to com.tentaclestudio.fantasyracing.interfacesMoteurs.mouvements.TypeMouvement  
    at com.tentaclestudio.fantasyracing.interfacesMoteurs.donneesparties.CaseSelectionnableInformations.toJSON(SourceFile:613)  
    at com.tentaclestudio.fantasyracing.interfacesMoteurs.sortiesMoteurJeu.ResultatStepJeu.toJSON(SourceFile:1583)  
    at com.tentaclestudio.fantasyracing.moteurdejeu.StickyPartieDebugInfo.<init>(SourceFile:255)  
    at com.tentaclestudio.fantasyracing.moteurdejeu.StickyPartie.addDebugInfos(SourceFile:3555)  
    at com.tentaclestudio.fantasyracing.moteurdejeu.CalculateurStepDeJeu.jouerStep(SourceFile:1606)  
    at com.tentaclestudio.fantasyracing.moteurdejeu.StickyMoteur.jouerStep(SourceFile:1814)  
    at com.tentaclestudio.fantasyracing.moteurdejeu.SynchronisateurPartieEnLigne.jouerPartieJusquaInteraction(SourceFile:850)  
    at com.tentaclestudio.fantasyracing.moteurdejeu.SynchronisateurPartieEnLigne.synchroniserPartieEnLigne(SourceFile:1020)  
    at com.tentaclestudio.fantasyracing.moteurdejeu.StickyMoteur.synchroniserPartieEnLigne(SourceFile:1465)  
    at com.tentaclestudio.fantasyracing.moteurdejeu.StickyMoteur.synchroniserDebutDePartie(SourceFile:1227)  
    at com.tentaclestudio.fantasyracing.moteurdejeu.StickyMoteurHandler.handleMessage(SourceFile:61)  
    at android.os.Handler.dispatchMessage(Handler.java:106)  
    at android.os.Looper.loopOnce(Looper.java:230)

The related code:

 JSONObject mvtAutorises = new JSONObject();
if (listeMouvementsAutorises != null) {
    for (int i = 0; i < listeMouvementsAutorises.size(); i++) {
        Pair<TypeMouvement, DirectionMouvement> infos = listeMouvementsAutorises.get(i);
        mvtAutorises.put("mvtAutorise_" + i, infos.first.name() + " - " + infos.second.toString()); // <=== crash here
    }
}

listeMouvementsAutorises is an (ObjectBox) class attribute:

@Convert(converter = ListeTypeEtDirectionMouvementConverter.class, dbType = String.class)
    List<Pair<TypeMouvement, DirectionMouvement>> listeMouvementsAutorises;

The converter:

public static class ListeTypeEtDirectionMouvementConverter implements PropertyConverter<List<Pair<TypeMouvement, DirectionMouvement>>, String> {

        @Override
        public List<Pair<TypeMouvement, DirectionMouvement>> convertToEntityProperty(String databaseValue) {
            Type typeListe = new TypeToken<List<Pair<TypeMouvement, DirectionMouvement>>>(){}.getType();
            Gson gson = new Gson();
            List<Pair<TypeMouvement, DirectionMouvement>> listeresultat = gson.fromJson(databaseValue, typeListe);
            return listeresultat;
        }

        @Override
        public String convertToDatabaseValue(List<Pair<TypeMouvement, DirectionMouvement>> entityProperty) {
            Type typeListe = new TypeToken<List<Pair<TypeMouvement, DirectionMouvement>>>(){}.getType();
            Gson gson = new Gson();
            return gson.toJson(entityProperty, typeListe);
        }
    }

I don’t understand the ClassCastException, since there’s no casting on the line where the crash occurs.

I thought the error might be due to enum obfuscation, so I added the following lines to proguard-rules.pro:

-keepclassmembers,allowoptimization enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

I also tried to add:

-keep enum com.tentaclestudio.fantasyracing.interfacesMoteurs.mouvements.TypeMouvement {
    *;
}

Unfortunately, that didn’t help.

Here's my full proguard-rules.pro file:

-printconfiguration /home/julien/Desktop/Brouillons/full-r8-config.txt

-dontwarn org.bouncycastle.jsse.BCSSLParameters
-dontwarn org.bouncycastle.jsse.BCSSLSocket
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
-dontwarn org.conscrypt.Conscrypt$Version
-dontwarn org.conscrypt.Conscrypt
-dontwarn org.conscrypt.ConscryptHostnameVerifier
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
-dontwarn org.openjsse.net.ssl.OpenJSSE

##---------------Begin: proguard configuration for Gson  ----------
-keepattributes Signature
-keepattributes *Annotation*
-dontwarn sun.misc.**
-keep class com.google.gson.examples.android.model.** { <fields>; }
-keep class * extends com.google.gson.TypeAdapter
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
-keepclassmembers,allowobfuscation class * {
  @com.google.gson.annotations.SerializedName <fields>;
}
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
##---------------End: proguard configuration for Gson  ----------

# For enumeration classes
-keepclassmembers,allowoptimization enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

-keep enum com.tentaclestudio.fantasyracing.interfacesMoteurs.mouvements.TypeMouvement {
    *;
}

# Keep native method names
-keepclasseswithmembers,includedescriptorclasses,allowshrinking class * {
    native <methods>;
}

And an excerpt from build.gradle:

buildTypes {
    release {
        minifyEnabled true
        shrinkResources false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        ndk.debugSymbolLevel "SYMBOL_TABLE"
    }
    debug {
        minifyEnabled true
        shrinkResources false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        applicationIdSuffix ".debug"
        debuggable true
        versionNameSuffix '.debug'
    }
}

Edit:

I added some test logs:

  1. In the Converter, inside the convertToEntityProperty() method:

    Log.d("DEBUG_JOL", "ListeTypeEtDirectionMouvementConverter: " + databaseValue);

The log output is:

ListeTypeEtDirectionMouvementConverter: [{"first":"DeplacementYPuisX","second":{"directionMouvementHorizontal":"VersLaGauche","directionMouvementVertical":"VersLeBas","valeurPourBDD":11}},{"first":"DeplacementXPuisY","second":{"directionMouvementHorizontal":"VersLaGauche","directionMouvementVertical":"VersLeBas","valeurPourBDD":11}}]

Everything looks fine here.

  1. Right before the line that causes the crash:

    Log.d("DEBUG_JOL", "infos: " + infos.first + ", " + infos.second); mvtAutorises.put("mvtAutorise_" + i, infos.first.name() + " - " + infos.second.toString());

Result:

infos: DeplacementYPuisX, {directionMouvementHorizontal=VersLaGauche, directionMouvementVertical=VersLeBas, valeurPourBDD=11.0}

Everything is also fine at this point.

However, if I modify the log to include .name():

Log.d("DEBUG_JOL", "infos: " + infos.first.name() + ", " + infos.second);

Then the exception occurs during the execution of that line. Same thing if a use toString() instead of name()

Edit #2:

After further testing, it seems that the issue comes from the Converter: the returned list doesn't contain TypeMouvement and DirectionMouvement instances, but rather String values (which correspond to the enum values).

Screenshot of converter returned values


Solution

  • I found the root of the problem — it was actually upstream of the toJSON(): it was the use of a Pair<TypeMouvement, DirectionMouvement> that caused the issue.

    Since Pair<> is a generic type, and likely due to obfuscation (despite the ProGuard configuration regarding the TypeMouvement and DirectionMouvement enums), GSON was unable to retrieve the correct type associated with the enum values stored in the database. As a fallback, it returned the corresponding Strings.

    Thus, and contrary to the statically declared typing in the code, convertToEntityProperty() was returning a List<Pair<String, Map<String>>>, and on the first access to an element of the Pair, the crash occurred because a String was retrieved instead of an Enum.

    In hindsight, the fact that GSON works correctly without obfuscation in this specific case seems a bit magical, since it doesn't actually know the type of content inside the Pair... I suppose that having all the symbols unobfuscated allows it to figure things out.

    I fixed the issue by creating a dedicated type:

    public static class TypeEtDirection {
        public TypeMouvement first;
        public DirectionMouvement second;
    
        public TypeEtDirection(TypeMouvement first, DirectionMouvement second) {
            this.first = first;
            this.second = second;
        }
    }
    

    I'm now using this instead of Pair<>, and that resolved the problem:

    public static class ListeTypeEtDirectionMouvementConverter implements PropertyConverter<List<TypeEtDirection>, String> {
    
            @Override
            public List<TypeEtDirection> convertToEntityProperty(String databaseValue) {
                Type typeListe = new TypeToken<List<TypeEtDirection>>(){}.getType();
                Gson gson = new Gson();
                Log.d("DEBUG_JOL", "ListeTypeEtDirectionMouvementConverter: " + databaseValue);
                List<TypeEtDirection> listeresultat = gson.fromJson(databaseValue, typeListe);
                return listeresultat;
            }
    
            @Override
            public String convertToDatabaseValue(List<TypeEtDirection> entityProperty) {
                Type typeListe = new TypeToken<List<TypeEtDirection>>(){}.getType();
                Gson gson = new Gson();
                return gson.toJson(entityProperty, typeListe);
            }
        }