Search code examples
androidfirebaseencryptioncrashlyticsboot

Android Crashlytics fails on direct boot due to disk encryption


Android Crashlytics will fail to initialize on direct boot on android. The problem is that the default storage backing a context in Android is encrypted until the user enters their credentials:

https://developer.android.com/reference/android/content/Context.html#createDeviceProtectedStorageContext()

You'll see a bunch of info in the logs like

07-17 16:47:18.083 1897-1982/XXX E/SharedPreferencesImpl: Couldn't create directory for SharedPreferences file /data/user/0/com.xxx.xxx/shared_prefs/com.crashlytics.sdk.android:answers:settings.xml

Also verified by registering an initializationCallback with Fabric.Builder

Is there any way to configure crashlytics to use a shared preferences backend by createDeviceProtectedStorageContext storage?

The problem is that if the application starts on boot in this way then crashlytics won't work for the lifetime of the app. This can lead to a lot of missed crash reports.


Solution

  • Basically it fails because libraries like Crashlytics would call getBaseContext which invalidates createDeviceProtectedStorageContext. For Crashlytics, it also expects getApplicationContext to return an Application. Here's the full workaround.

    Implement the following class: (shouldn't be too hard to translate this into Java)

    @SuppressLint("MissingSuperCall", "Registered")
    @TargetApi(24)
    class DeviceStorageApp(private val app: Application) : Application() {
        init {
            attachBaseContext(app.createDeviceProtectedStorageContext())
        }
    
        /**
         * Thou shalt not get the REAL underlying application context which would no
         * longer be operating under device protected storage.
         */
        override fun getApplicationContext(): Context = this
    
        /**
         * Forwarding Application calls to make libraries like Firebase sessions work.
         */
        override fun onCreate() = app.onCreate()
        override fun onTerminate() = app.onTerminate()
        override fun onConfigurationChanged(newConfig: Configuration) =
            app.onConfigurationChanged(newConfig)
        override fun onLowMemory() = app.onLowMemory()
        override fun onTrimMemory(level: Int) = app.onTrimMemory(level)
        override fun registerComponentCallbacks(callback: ComponentCallbacks?) =
            app.registerComponentCallbacks(callback)
        override fun unregisterComponentCallbacks(callback: ComponentCallbacks?) =
            app.unregisterComponentCallbacks(callback)
        override fun registerActivityLifecycleCallbacks(callback: ActivityLifecycleCallbacks?) =
            app.registerActivityLifecycleCallbacks(callback)
        override fun unregisterActivityLifecycleCallbacks(callback: ActivityLifecycleCallbacks?) =
            app.unregisterActivityLifecycleCallbacks(callback)
        override fun registerOnProvideAssistDataListener(callback: OnProvideAssistDataListener?) =
            app.registerOnProvideAssistDataListener(callback)
        override fun unregisterOnProvideAssistDataListener(callback: OnProvideAssistDataListener?) =
            app.unregisterOnProvideAssistDataListener(callback)
    }
    

    Disable auto initialization by removing corresponding providers:

        <provider android:name="com.google.firebase.provider.FirebaseInitProvider"
                  tools:node="remove"/>
    

    Then in your real Application.onCreate, do this: (Android SDK version checks omitted)

    FirebaseApp.initializeApp(DeviceStorageApp(this))
    

    Full sample (with probably more up-to-date fixes): https://github.com/Mygod/VPNHotspot/blob/d45a239d49d67fd6bf6b758e1b91c354369c1b9f/mobile/src/main/java/be/mygod/vpnhotspot/util/DeviceStorageApp.kt

    EDIT: Added forwarding calls to make Firebase sessions work properly.