Last active
May 23, 2022 08:13
-
-
Save pyricau/5f864374c1b1a028a1a8dfe25b6761df to your computer and use it in GitHub Desktop.
Fix for https://issuetracker.google.com/issues/147246567 => see https://twitter.com/Piwai/status/1374129312153038849
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.os.Build.VERSION.SDK_INT | |
import android.os.Bundle | |
import android.os.Parcel | |
import android.util.Base64 | |
/** | |
* This class implements a fix for https://issuetracker.google.com/issues/147246567 | |
* Investigation: https://twitter.com/Piwai/status/1374129312153038849 | |
* | |
* Usage: | |
* | |
* - Call [fixOutState] at the end of [android.app.Activity.onSaveInstanceState] (i.e. AFTER | |
* the super call and any extra state saving you might be doing. | |
* - Call [restoreEncodedState] at the start of [android.app.Activity.onCreate] (i.e. BEFORE the | |
* super call and any other code), if savedInstanceState is not null. | |
* | |
* Note: the crash fixed by this code only happens on Android 10 so these methods are no-op on any | |
* other Android version. | |
* | |
* On Android 10, under specific conditions ActivityThread will receive a command to destroy an | |
* activity before it has been created, and therefore attempts to log that unexpected command. | |
* Unfortunately, as part of the logging details, a hashcode is included which itself is based on | |
* the saved state bundle side, which requires unparceling the bundle. As a result, the bundle gets | |
* unparceled eagerly but with a classloader that can't handle any custom parcelable defined in the | |
* APK. As a result, the app crashes before our activity is even created. | |
* | |
* Since we can't easily remove all custom parcelables, the fix here consists in replacing the | |
* saved state parcelable content with a string that contains the parcel bytes encoded as Base64. | |
* | |
* However, as that encoding isn't cheap, we only want to do it if the bundle is actually written to | |
* a parcel. To know when that's happening, we create a custom CharSequence and add it to the parcel. | |
* The Android Framework doesn't actually serialize CharSequence implementations, but it calls | |
* toString() on them at serialization time and saves the string content. So we override toString() | |
* and serialize + encode a copy of the saved bundle at that time. | |
*/ | |
class BadParcelableFix private constructor( | |
private val copiedState: Bundle | |
) : CharSequence { | |
override fun toString(): String { | |
// toString() is called when the CharSequence is serialized. | |
// Time to do the serialization work. | |
return copiedState.toBase64EncodedString() | |
} | |
companion object { | |
private val BUNDLE_KEY = BadParcelableFix::class.java.name | |
@JvmStatic | |
fun fixOutState(outState: Bundle) { | |
if (SDK_INT != 29) { | |
return | |
} | |
val copiedState = Bundle() | |
// This is fairly efficient (2 system array copies). | |
copiedState.putAll(outState) | |
outState.clear() | |
val fakeCharSequence = BadParcelableFix(copiedState) | |
// Note: no serialization happening here. | |
outState.putCharSequence(BUNDLE_KEY, fakeCharSequence) | |
} | |
@JvmStatic | |
fun restoreEncodedState(savedInstanceState: Bundle) { | |
if (SDK_INT != 29 || !savedInstanceState.containsKey(BUNDLE_KEY)) { | |
return | |
} | |
val stateCharSequence = savedInstanceState.getCharSequence(BUNDLE_KEY) | |
val decodedBundle = if (stateCharSequence is BadParcelableFix) { | |
stateCharSequence.copiedState | |
} else { | |
val encodedStateString = stateCharSequence.toString() | |
encodedStateString.fromBase64EncodedString() | |
} | |
savedInstanceState.remove(BUNDLE_KEY) | |
savedInstanceState.putAll(decodedBundle) | |
} | |
private fun Bundle.toBase64EncodedString(): String { | |
return Base64.encodeToString(toByteArray(), 0) | |
} | |
private fun Bundle.toByteArray(): ByteArray { | |
val parcel = Parcel.obtain() | |
parcel.writeBundle(this) | |
val bytes = parcel.marshall() | |
parcel.recycle() | |
return bytes | |
} | |
private fun String.fromBase64EncodedString(): Bundle { | |
return Base64.decode(this, 0).toBundle() | |
} | |
private fun ByteArray.toBundle(): Bundle { | |
val parcel = Parcel.obtain() | |
parcel.unmarshall(this, 0, size) | |
parcel.setDataPosition(0) | |
val bundle = parcel.readBundle(BadParcelableFix.Companion::class.java.classLoader)!! | |
parcel.recycle() | |
return bundle | |
} | |
} | |
// Methods implemented below aren't used. | |
override val length: Int | |
get() = 0 | |
override fun get(index: Int) = 0.toChar() | |
override fun subSequence( | |
startIndex: Int, | |
endIndex: Int | |
) = this | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.os.Bundle | |
import android.os.Parcel | |
import android.os.Parcelable | |
import android.os.Parcelable.Creator | |
import com.google.common.truth.Truth.assertThat | |
import org.junit.Test | |
import org.junit.runner.RunWith | |
import org.robolectric.RobolectricTestRunner | |
@RunWith(RobolectricTestRunner::class) | |
class BadParcelableFixTest { | |
@Test fun `fixOutState() removes existing entries`() { | |
val sourceBundle = Bundle() | |
sourceBundle.putIntArray("key", intArrayOf(1, 2, 3)) | |
BadParcelableFix.fixOutState(sourceBundle) | |
assertThat(sourceBundle.containsKey("key")).isFalse() | |
} | |
@Test fun `restoreEncodedState() on source bundle returns same value instance`() { | |
val sourceBundle = Bundle() | |
val sourceValue = intArrayOf(1, 2, 3) | |
sourceBundle.putIntArray("key", sourceValue) | |
BadParcelableFix.fixOutState(sourceBundle) | |
BadParcelableFix.restoreEncodedState(sourceBundle) | |
val restoredValue = sourceBundle.getIntArray("key") | |
assertThat(restoredValue).isSameInstanceAs(sourceValue) | |
} | |
@Test fun `fixOutState() removes existing entries even post unmarshalling`() { | |
val sourceBundle = Bundle() | |
sourceBundle.putIntArray("key", intArrayOf(1, 2, 3)) | |
BadParcelableFix.fixOutState(sourceBundle) | |
val unmarshalledBundle = sourceBundle.marshall().unmarshall() | |
assertThat(unmarshalledBundle.containsKey("key")).isFalse() | |
} | |
@Test fun `restoreEncodedState() on unmarshalled bundle returns new equal value instance`() { | |
val sourceBundle = Bundle() | |
val sourceValue = intArrayOf(1, 2, 3) | |
sourceBundle.putIntArray("key", sourceValue) | |
BadParcelableFix.fixOutState(sourceBundle) | |
val unmarshalledBundle = sourceBundle.marshall().unmarshall() | |
BadParcelableFix.restoreEncodedState(unmarshalledBundle) | |
val restoredValue = unmarshalledBundle.getIntArray("key") | |
assertThat(restoredValue).isNotSameInstanceAs(sourceValue) | |
assertThat(restoredValue!!.toList()).containsExactly(1, 2, 3) | |
} | |
@Test fun `restoreEncodedState() on unmarshalled bundle unparcels custom parcelable`() { | |
val sourceBundle = Bundle() | |
sourceBundle.putParcelable("key", CustomParcelable(state = "state")) | |
BadParcelableFix.fixOutState(sourceBundle) | |
val unmarshalledBundle = sourceBundle.marshall().unmarshall() | |
BadParcelableFix.restoreEncodedState(unmarshalledBundle) | |
val restoredValue = unmarshalledBundle.getParcelable<CustomParcelable>("key") | |
assertThat(restoredValue!!.state).isEqualTo("state") | |
} | |
private class CustomParcelable(val state: String) : Parcelable { | |
override fun describeContents(): Int = 0 | |
override fun writeToParcel( | |
dest: Parcel, | |
flags: Int | |
) = with(dest) { | |
writeString(state) | |
} | |
companion object CREATOR : Creator<CustomParcelable> { | |
override fun createFromParcel(parcel: Parcel): CustomParcelable { | |
return CustomParcelable(parcel.readString()!!) | |
} | |
override fun newArray(size: Int): Array<CustomParcelable?> = arrayOfNulls(size) | |
} | |
} | |
private fun Bundle.marshall(): ByteArray { | |
val parcel = Parcel.obtain() | |
parcel.writeBundle(this) | |
val bytes = parcel.marshall() | |
parcel.recycle() | |
return bytes | |
} | |
private fun ByteArray.unmarshall(): Bundle { | |
val parcel = Parcel.obtain() | |
parcel.unmarshall(this, 0, size) | |
parcel.setDataPosition(0) | |
val bundle = parcel.readBundle(BadParcelableFixTest::class.java.classLoader)!! | |
parcel.recycle() | |
return bundle | |
} | |
} |
Thanks @pyricau this is our biggest firebase crash on samsungs devices / Android 10 since a while, and now fixed.
Some devices with API level 28 also get this crash, would be better to add a check for that as well.
Edit: Seems to be coming from androidx.fragment.app.FragmentManager.attachController
.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Update: added unit tests :)