Ahmed Awadallah
Ahmed Awadallah

Reputation: 474

Firebase Crash: CorruptionException in PreferencesMapCompat for DataStore, But Not Using Proto DataStore or Shared preferences migration

I'm encountering a crash in Firebase Crashlytics on certain devices related to androidx.datastore.core.CorruptionException: Unable to parse preferences proto. However, I am not directly using Proto DataStore in my application. The crash appears in Android 12,13 and 14, 100% on background devices. Below is the stack trace:

Fatal Exception: androidx.datastore.core.CorruptionException: Unable to parse preferences proto.
       at androidx.datastore.preferences.PreferencesMapCompat$Companion.readFrom(PreferencesMapCompat.kt:20)
       at androidx.datastore.preferences.core.PreferencesSerializer.readFrom(PreferencesSerializer.jvm.kt:7)
       at androidx.datastore.core.okio.OkioReadScope.readData$suspendImpl(OkioStorage.kt:86)
       at androidx.datastore.core.okio.OkioReadScope.readData(OkioStorage.kt)
       ...

          Caused by androidx.datastore.preferences.protobuf.InvalidProtocolBufferException: Protocol message contained an invalid tag (zero).
       at androidx.datastore.preferences.protobuf.CodedInputStream$StreamDecoder.readTag(CodedInputStream.java:25)
       at androidx.datastore.preferences.protobuf.CodedInputStreamReader.getFieldNumber(CodedInputStreamReader.java:12)
       at androidx.datastore.preferences.protobuf.MessageSchema.mergeFromHelper(MessageSchema.java:15)
       at androidx.datastore.preferences.protobuf.MessageSchema.mergeFrom(MessageSchema.java:1)
       at androidx.datastore.preferences.protobuf.GeneratedMessageLite.parsePartialFrom(GeneratedMessageLite.java:3)
       at androidx.datastore.preferences.protobuf.GeneratedMessageLite.parseFrom(GeneratedMessageLite.java:5)
       at androidx.datastore.preferences.PreferencesProto$PreferenceMap.parseFrom(PreferencesProto.java:5)
       at androidx.datastore.preferences.PreferencesMapCompat$Companion.readFrom(PreferencesMapCompat.kt:5)
       at androidx.datastore.preferences.core.PreferencesSerializer.readFrom(PreferencesSerializer.jvm.kt:7)
       at androidx.datastore.core.okio.OkioReadScope.readData$suspendImpl(OkioStorage.kt:86)
       at androidx.datastore.core.okio.OkioReadScope.readData(OkioStorage.kt)
       at androidx.datastore.core.StorageConnectionKt$readData$2.invokeSuspend(StorageConnection.kt:32)
       at androidx.datastore.core.StorageConnectionKt$readData$2.invoke(StorageConnection.kt:1)
       at androidx.datastore.core.StorageConnectionKt$readData$2.invoke(StorageConnection.kt:2)
       at androidx.datastore.core.okio.OkioStorageConnection.readScope(OkioStorage.kt:97)
       at androidx.datastore.core.StorageConnectionKt.readData(StorageConnection.kt:6)
       at androidx.datastore.core.DataStoreImpl.readDataFromFileOrDefault(DataStoreImpl.kt:4)
       at androidx.datastore.core.DataStoreImpl.readDataOrHandleCorruption(DataStoreImpl.kt:159)
       at androidx.datastore.core.DataStoreImpl.access$readDataOrHandleCorruption(DataStoreImpl.kt)
       at androidx.datastore.core.DataStoreImpl$InitDataStore$doRun$initData$1.invokeSuspend(DataStoreImpl.kt:129)
       at androidx.datastore.core.DataStoreImpl$InitDataStore$doRun$initData$1.invoke(DataStoreImpl.kt:2)
       at androidx.datastore.core.DataStoreImpl$InitDataStore$doRun$initData$1.invoke(DataStoreImpl.kt:1)
       at androidx.datastore.core.SingleProcessCoordinator.lock(SingleProcessCoordinator.kt:97)
       at androidx.datastore.core.DataStoreImpl$InitDataStore.doRun(DataStoreImpl.kt:91)
       at androidx.datastore.core.RunOnce.runIfNeeded(DataStoreImpl.kt:130)
       at androidx.datastore.core.DataStoreImpl.readAndInitOrPropagateAndThrowFailure(DataStoreImpl.kt:101)
       at androidx.datastore.core.DataStoreImpl.handleUpdate(DataStoreImpl.kt:156)
       at androidx.datastore.core.DataStoreImpl.access$handleUpdate(DataStoreImpl.kt)
       at androidx.datastore.core.DataStoreImpl$writeActor$3.invokeSuspend(DataStoreImpl.kt:34)
       at androidx.datastore.core.DataStoreImpl$writeActor$3.invoke(DataStoreImpl.kt:1)
       at androidx.datastore.core.DataStoreImpl$writeActor$3.invoke(DataStoreImpl.kt:2)
       at androidx.datastore.core.SimpleActor$offer$2.invokeSuspend(SimpleActor.kt:86)
       at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:11)
       at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:98)
       at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:3)
       at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:2)
       at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt)
       at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:14)
       at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:28)
       at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt)
        

I’ve reviewed my code and can't locate where Proto DataStore is being used or Shared prefenercens migration, so I’m not sure what could be causing this crash.

Build.gradle:

plugins {
    id 'com.android.application'
    id 'com.google.gms.google-services'
    id 'com.google.firebase.crashlytics'
    id 'kotlin-android'
    id 'com.google.devtools.ksp'
    id 'com.google.firebase.firebase-perf'
    id 'kotlin-parcelize'
    id 'org.jetbrains.kotlin.android'
    id("org.jetbrains.kotlin.plugin.compose") version "$kotlin_version"

}


buildFeatures {
        viewBinding true
        compose true
        buildConfig = true
    }


    compileOptions {
        sourceCompatibility JavaVersion.VERSION_17
        targetCompatibility JavaVersion.VERSION_17
        coreLibraryDesugaringEnabled true
    }

    kotlinOptions {
        jvmTarget = "17"
    }

dependencies {

    implementation 'androidx.appcompat:appcompat:1.7.0'
    implementation 'androidx.browser:browser:1.8.0'
    implementation 'com.google.firebase:firebase-perf-ktx:21.0.1'
    implementation 'com.google.firebase:firebase-config-ktx:22.0.0'
    implementation project(path: ':library_lvl')
    implementation 'androidx.core:core-ktx:1.13.1'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.2.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'

    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    // Media 3 instead of Exoplayer
    implementation 'androidx.media3:media3-exoplayer:1.4.1'
    implementation "androidx.media3:media3-ui:1.4.1"

    // Import the BoM for the Firebase platform
    implementation 'com.google.firebase:firebase-crashlytics-ktx:19.1.0'
    implementation 'com.google.firebase:firebase-analytics-ktx:22.1.0'

    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.6'
    implementation 'com.anggrayudi:storage:2.0.0'
    //implementation 'com.github.skydoves:powerspinner:1.2.4'         // custom spinner
    freeImplementation 'com.google.android.gms:play-services-ads:23.3.0' // it was 20.3.0
    implementation 'com.github.judemanutd:autostarter:1.1.0'    // auto start for some devices

    // for app open ads
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.6'
    implementation 'androidx.lifecycle:lifecycle-process:2.8.6'


    // room
    def room_version = "2.6.1"
    implementation "androidx.room:room-runtime:$room_version"
    ksp "androidx.room:room-compiler:$room_version"
    // optional - Kotlin Extensions and Coroutines support for Room
    implementation "androidx.room:room-ktx:$room_version"

    /** compose **/
    // Integration with activities
    implementation 'androidx.activity:activity-compose:1.9.2'
    implementation 'androidx.activity:activity-ktx:1.9.2'
    // Compose Material Design
    implementation 'androidx.compose.material:material:1.7.2'
    // Animations
    implementation 'androidx.compose.animation:animation:1.7.2'
    // Tooling support (Previews, etc.)
    implementation 'androidx.compose.ui:ui-tooling:1.7.2'
    // Integration with ViewModels
    implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6'
    // When using a AppCompat theme
    implementation 'androidx.compose.runtime:runtime-livedata:1.7.2'
    /*
    View model
     */
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6'
    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.6'

    /** Jetpack data store */
    implementation "androidx.datastore:datastore-preferences:1.1.1"

    // compose navigation ..
    implementation "androidx.navigation:navigation-compose:2.8.1"
    implementation 'androidx.compose.material3:material3:1.3.0'
    // compose permission ..
    implementation 'com.google.accompanist:accompanist-permissions:0.36.0'
    // optional - helpers for implementing LifecycleOwner in a Service
    implementation 'androidx.lifecycle:lifecycle-service:2.8.6'

    // Saved state module for ViewModel
    implementation('androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.6')


    // In app update ..
    implementation 'com.google.android.play:app-update:2.1.0'

    // For Kotlin users also add the Kotlin extensions library for Play In-App Update:
    implementation 'com.google.android.play:app-update-ktx:2.1.0'


    // to show drawable NOT FROM Resources in compfose
    implementation 'com.google.accompanist:accompanist-drawablepainter:0.36.0'

    // permission flow
    implementation 'dev.shreyaspatil.permission-flow:permission-flow-compose:2.0.0'

    // coil
    implementation('io.coil-kt:coil-compose:2.7.0')
    implementation('io.coil-kt:coil-video:2.7.0')   // for video thumbnail

    // for collect with life cycle
    implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.8.6'


  //  implementation 'com.github.tejpratap46:PDFCreatorAndroid:3.0.2'  // pdf generation
    implementation 'androidx.core:core-splashscreen:1.0.1'  // splash screen
    // color picker dialog
    implementation 'com.godaddy.android.colorpicker:compose-color-picker-android:0.7.0'
    implementation 'androidx.paging:paging-compose:3.3.2'   // paging library
    implementation("com.google.android.play:review-ktx:2.0.1")  // in app review
    implementation 'androidx.compose.ui:ui-util:1.7.2'     // for fast collections functions
    implementation 'androidx.compose.ui:ui:1.7.2'

    // https://mvnrepository.com/artifact/net.lingala.zip4j/zip4j
    implementation 'net.lingala.zip4j:zip4j:2.11.5'

    freeImplementation project(path: ':subscription')

    implementation 'com.partners.analytics.core.coverage:sdk-init:3.5.10'
    implementation 'com.github.SmartToolFactory:Compose-Extended-Gestures:3.1'  // motion modifiers

    // koin dependency injection
    def koinVersion = "4.0.0"
    implementation("io.insert-koin:koin-android:$koinVersion")
    implementation("io.insert-koin:koin-androidx-compose:$koinVersion")
    implementation("io.insert-koin:koin-androidx-workmanager:$koinVersion")
    implementation ("io.insert-koin:koin-core-coroutines:$koinVersion")

    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2' // use date time before 26
    implementation 'androidx.biometric:biometric:1.2.0-alpha05' // biometric lock

    implementation('com.airbnb.android:lottie-compose:6.5.2')   //lottie

}

Proguard rules:

-keep class androidx.datastore.** { *; }
-keep class kotlinx.coroutines.** { *; }
-keep class kotlin.** { *; }

Steps I've Taken: Review of the code: I don't directly reference Proto DataStore in my code, neither Firebase migration. Affected libraries: The crash references the PreferencesMapCompat class, which seems related to Preferences DataStore.

How can I resolve this issue, given that I am not using Proto DataStore directly? Is there any chance this could be caused by a third-party library or something specific to the affected devices? Should I add corruption handler to all my app data stores, specially that every feature has its own?

Upvotes: 3

Views: 586

Answers (1)

Zeros-N-Ones
Zeros-N-Ones

Reputation: 1082

It appears you're using DataStore Preferences (androidx.datastore:datastore-preferences:1.1.1) based on your stack trace and build.gradle

While you're not directly using Proto DataStore, the Preferences DataStore actually uses Protocol Buffers under the hood for serialization, which explains the protobuf-related error.

Use the solution below to handle the corruption issue properly:

import androidx.datastore.core.CorruptionException
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import timber.log.Timber
import java.io.IOException

class DataStoreManager(
    private val dataStore: DataStore<Preferences>
) {
    /**
     * Wraps DataStore operations with corruption handling
     */
    fun <T> safeRead(
        getData: (preferences: Preferences) -> T,
        defaultValue: T
    ): Flow<T> {
        return dataStore.data
            .catch { exception ->
                when (exception) {
                    is IOException -> {
                        Timber.e(exception, "Error reading preferences")
                        emit(dataStore.updateData { preferences ->
                            // Return existing preferences
                            preferences
                        })
                    }
                    is CorruptionException -> {
                        Timber.e(exception, "Corruption in preferences, recreating store")
                        // If corruption is detected, clear all data
                        emit(dataStore.updateData { 
                            Preferences.empty()
                        })
                    }
                    else -> {
                        throw exception
                    }
                }
            }
            .catch { exception ->
                // Fallback in case the updateData above also fails
                Timber.e(exception, "Fallback: returning default value")
                emit(dataStore.updateData { Preferences.empty() })
            }
            .catch { 
                // Final fallback - return default value
                emit(Preferences.empty())
            }
            .map { preferences ->
                try {
                    getData(preferences)
                } catch (e: Exception) {
                    Timber.e(e, "Error mapping preferences")
                    defaultValue
                }
            }
    }

    /**
     * Wraps DataStore write operations with error handling
     */
    suspend fun safeEdit(update: suspend (MutablePreferences) -> Unit) {
        try {
            dataStore.edit { preferences ->
                update(preferences)
            }
        } catch (e: Exception) {
            Timber.e(e, "Error writing to preferences")
            // Attempt to recreate store if corrupt
            if (e is CorruptionException) {
                dataStore.edit { 
                    // Clear and retry the update
                    preferences -> preferences.clear()
                    update(preferences)
                }
            }
        }
    }
}

You can implement the solution by following these steps:

Wrap your existing DataStore instances with this manager:

val dataStore: DataStore<Preferences> = context.createDataStore(name = "settings")
val dataStoreManager = DataStoreManager(dataStore)

Then replace your direct DataStore reads with the safe version:

// Before
dataStore.data.map { it[myKey] ?: defaultValue }

// After
dataStoreManager.safeRead(
    getData = { it[myKey] },
    defaultValue = defaultValue
)

And wrap writes with the safe version:

// Before
dataStore.edit { it[myKey] = value }

// After
dataStoreManager.safeEdit { it[myKey] = value }

The issue is likely occurring because of data corruption in the preferences file, which can happen due to process termination during write operations, device storage issues, system upgrades, or low memory conditions

Third-party libraries using DataStore could also contribute to this issue. The crash happening in the background suggests it might be related to background operations or migrations.

It's recommended to add corruption handling to all DataStore instances in your app. You can create a single DataStoreManager instance per DataStore and reuse the safe reading/writing methods.

Hope this helps!

Upvotes: 2

Related Questions