Androidアプリは、外部からIntentを受け取ることによって起動します。IntentにはExtrasという任意の引数を渡す仕組みがあり、多くのアプリで利用されています。しかし、Extrasに不適切な情報が含まれている場合、アプリが容易にクラッシュしてしまうことはあまり知られていないかもしれません。Extraを読み出している場合や、明示的にExtraを扱っていなくても、SavedStateHandleを受け取るViewModelを扱っている場合は注意が必要です。
例えば、アプリAのonCreateで以下のようにExtraを読み出しているとします。
val data = intent.getStringExtra("hoge")
このアプリAを、アプリBから次のように起動させてみます。
val intent = Intent()
intent.setClassName("com.example.myapplication", "com.example.myapplication.MainActivity")
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.putExtra("foo", Data("value"))
startActivity(intent)
ここで、アプリBは独自のParcelableクラスであるDataを"foo"というkeyでExtraに追加しています。このDataクラスはアプリAに存在しないので、"foo"を読み出そうとすると、BadParcelableException が発生し、クラッシュするというのは容易に想像できると思います。
しかし、"foo"を読み出していなくてもクラッシュが発生します。
BadParcelableException
Process: com.example.myapplication, PID: 12347
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.myapplication/com.example.myapplication.MainActivity}: android.os.BadParcelableException: ClassNotFoundException when unmarshalling: com.example.myapplication2.Data
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3707)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3864)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2253)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7870)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
Caused by: android.os.BadParcelableException: ClassNotFoundException when unmarshalling: com.example.myapplication2.Data
at android.os.Parcel.readParcelableCreator(Parcel.java:3417)
at android.os.Parcel.readParcelable(Parcel.java:3325)
at android.os.Parcel.readValue(Parcel.java:3227)
at android.os.Parcel.readArrayMapInternal(Parcel.java:3624)
at android.os.BaseBundle.initializeFromParcelLocked(BaseBundle.java:292)
at android.os.BaseBundle.unparcel(BaseBundle.java:236)
at android.os.BaseBundle.getString(BaseBundle.java:1196)
at android.content.Intent.getStringExtra(Intent.java:8520)
at com.example.myapplication.MainActivity.onCreate(MainActivity.kt:14)
at android.app.Activity.performCreate(Activity.java:8057)
at android.app.Activity.performCreate(Activity.java:8037)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1341)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3688)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3864)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2253)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7870)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
Caused by: java.lang.ClassNotFoundException: com.example.myapplication2.Data
at java.lang.Class.classForName(Native Method)
at java.lang.Class.forName(Class.java:454)
at android.os.Parcel.readParcelableCreator(Parcel.java:3391)
at android.os.Parcel.readParcelable(Parcel.java:3325)
at android.os.Parcel.readValue(Parcel.java:3227)
at android.os.Parcel.readArrayMapInternal(Parcel.java:3624)
at android.os.BaseBundle.initializeFromParcelLocked(BaseBundle.java:292)
at android.os.BaseBundle.unparcel(BaseBundle.java:236)
at android.os.BaseBundle.getString(BaseBundle.java:1196)
at android.content.Intent.getStringExtra(Intent.java:8520)
at com.example.myapplication.MainActivity.onCreate(MainActivity.kt:14)
at android.app.Activity.performCreate(Activity.java:8057)
at android.app.Activity.performCreate(Activity.java:8037)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1341)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3688)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3864)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2253)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7870)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
Caused by: java.lang.ClassNotFoundException: Didn't find class "com.example.myapplication2.Data" on path: DexPathList[[dex file "/data/data/com.example.myapplication/code_cache/.overlay/base.apk/classes3.dex", zip file "/data/app/~~o8PiQNL69H1KpJwa-WK_Yw==/com.example.myapplication-mCg8kMxexwb6VC35S3VA-A==/base.apk"],nativeLibraryDirectories=[/data/app/~~o8PiQNL69H1KpJwa-WK_Yw==/com.example.myapplication-mCg8kMxexwb6VC35S3VA-A==/lib/arm64, /system/lib64, /system_ext/lib64]]
at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:218)
at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
at java.lang.Class.classForName(Native Method)
at java.lang.Class.forName(Class.java:454)
at android.os.Parcel.readParcelableCreator(Parcel.java:3391)
at android.os.Parcel.readParcelable(Parcel.java:3325)
at android.os.Parcel.readValue(Parcel.java:3227)
at android.os.Parcel.readArrayMapInternal(Parcel.java:3624)
at android.os.BaseBundle.initializeFromParcelLocked(BaseBundle.java:292)
at android.os.BaseBundle.unparcel(BaseBundle.java:236)
at android.os.BaseBundle.getString(BaseBundle.java:1196)
at android.content.Intent.getStringExtra(Intent.java:8520)
at com.example.myapplication.MainActivity.onCreate(MainActivity.kt:14)
at android.app.Activity.performCreate(Activity.java:8057)
at android.app.Activity.performCreate(Activity.java:8037)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1341)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3688)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3864)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2253)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7870)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
同様に、アプリAにないEnumの値(Serializable)など、要するにアプリAではデシリアライズできないデータを含むIntentをアプリBがアプリAに投げると、アプリAがクラッシュします。
また、SavedStateHandleを受け取るViewModelをアプリAに定義し、それをActivityで参照する場合、一切Extraに触れていなくても同様のクラッシュが発生します。
class MainViewModel(
handle: SavedStateHandle,
): ViewModel()
val viewModel: MainViewModel by viewModels()
viewModel.doSomething()
アプリのコード上でExtraには一切触れていません。
それにもかかわらず、クラッシュします。
BadParcelableException
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.myapplication/com.example.myapplication.MainActivity}: android.os.BadParcelableException: ClassNotFoundException when unmarshalling: com.example.myapplication2.Data
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3707)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3864)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2253)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7870)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
Caused by: android.os.BadParcelableException: ClassNotFoundException when unmarshalling: com.example.myapplication2.Data
at android.os.Parcel.readParcelableCreator(Parcel.java:3417)
at android.os.Parcel.readParcelable(Parcel.java:3325)
at android.os.Parcel.readValue(Parcel.java:3227)
at android.os.Parcel.readArrayMapInternal(Parcel.java:3624)
at android.os.BaseBundle.initializeFromParcelLocked(BaseBundle.java:292)
at android.os.BaseBundle.unparcel(BaseBundle.java:236)
at android.os.BaseBundle.keySet(BaseBundle.java:569)
at androidx.lifecycle.SavedStateHandle$Companion.createHandle(SavedStateHandle.kt:371)
at androidx.lifecycle.SavedStateHandleSupport.createSavedStateHandle(SavedStateHandleSupport.kt:70)
at androidx.lifecycle.SavedStateHandleSupport.createSavedStateHandle(SavedStateHandleSupport.kt:103)
at androidx.lifecycle.SavedStateViewModelFactory.create(SavedStateViewModelFactory.kt:133)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.kt:187)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.kt:153)
at androidx.lifecycle.ViewModelLazy.getValue(ViewModelLazy.kt:53)
at androidx.lifecycle.ViewModelLazy.getValue(ViewModelLazy.kt:35)
at com.example.myapplication.MainActivity.onCreate$lambda$0(MainActivity.kt:11)
at com.example.myapplication.MainActivity.onCreate(MainActivity.kt:12)
at android.app.Activity.performCreate(Activity.java:8057)
at android.app.Activity.performCreate(Activity.java:8037)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1341)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3688)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3864)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2253)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7870)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
Caused by: java.lang.ClassNotFoundException: com.example.myapplication2.Data
at java.lang.Class.classForName(Native Method)
at java.lang.Class.forName(Class.java:454)
at android.os.Parcel.readParcelableCreator(Parcel.java:3391)
at android.os.Parcel.readParcelable(Parcel.java:3325)
at android.os.Parcel.readValue(Parcel.java:3227)
at android.os.Parcel.readArrayMapInternal(Parcel.java:3624)
at android.os.BaseBundle.initializeFromParcelLocked(BaseBundle.java:292)
at android.os.BaseBundle.unparcel(BaseBundle.java:236)
at android.os.BaseBundle.keySet(BaseBundle.java:569)
at androidx.lifecycle.SavedStateHandle$Companion.createHandle(SavedStateHandle.kt:371)
at androidx.lifecycle.SavedStateHandleSupport.createSavedStateHandle(SavedStateHandleSupport.kt:70)
at androidx.lifecycle.SavedStateHandleSupport.createSavedStateHandle(SavedStateHandleSupport.kt:103)
at androidx.lifecycle.SavedStateViewModelFactory.create(SavedStateViewModelFactory.kt:133)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.kt:187)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.kt:153)
at androidx.lifecycle.ViewModelLazy.getValue(ViewModelLazy.kt:53)
at androidx.lifecycle.ViewModelLazy.getValue(ViewModelLazy.kt:35)
at com.example.myapplication.MainActivity.onCreate$lambda$0(MainActivity.kt:11)
at com.example.myapplication.MainActivity.onCreate(MainActivity.kt:12)
at android.app.Activity.performCreate(Activity.java:8057)
at android.app.Activity.performCreate(Activity.java:8037)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1341)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3688)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3864)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2253)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7870)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
Caused by: java.lang.ClassNotFoundException: Didn't find class "com.example.myapplication2.Data" on path: DexPathList[[dex file "/data/data/com.example.myapplication/code_cache/.overlay/base.apk/classes3.dex", zip file "/data/app/~~o8PiQNL69H1KpJwa-WK_Yw==/com.example.myapplication-mCg8kMxexwb6VC35S3VA-A==/base.apk"],nativeLibraryDirectories=[/data/app/~~o8PiQNL69H1KpJwa-WK_Yw==/com.example.myapplication-mCg8kMxexwb6VC35S3VA-A==/lib/arm64, /system/lib64, /system_ext/lib64]]
at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:218)
at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
at java.lang.Class.classForName(Native Method)
at java.lang.Class.forName(Class.java:454)
at android.os.Parcel.readParcelableCreator(Parcel.java:3391)
at android.os.Parcel.readParcelable(Parcel.java:3325)
at android.os.Parcel.readValue(Parcel.java:3227)
at android.os.Parcel.readArrayMapInternal(Parcel.java:3624)
at android.os.BaseBundle.initializeFromParcelLocked(BaseBundle.java:292)
at android.os.BaseBundle.unparcel(BaseBundle.java:236)
at android.os.BaseBundle.keySet(BaseBundle.java:569)
at androidx.lifecycle.SavedStateHandle$Companion.createHandle(SavedStateHandle.kt:371)
at androidx.lifecycle.SavedStateHandleSupport.createSavedStateHandle(SavedStateHandleSupport.kt:70)
at androidx.lifecycle.SavedStateHandleSupport.createSavedStateHandle(SavedStateHandleSupport.kt:103)
at androidx.lifecycle.SavedStateViewModelFactory.create(SavedStateViewModelFactory.kt:133)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.kt:187)
at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.kt:153)
at androidx.lifecycle.ViewModelLazy.getValue(ViewModelLazy.kt:53)
at androidx.lifecycle.ViewModelLazy.getValue(ViewModelLazy.kt:35)
at com.example.myapplication.MainActivity.onCreate$lambda$0(MainActivity.kt:11)
at com.example.myapplication.MainActivity.onCreate(MainActivity.kt:12)
at android.app.Activity.performCreate(Activity.java:8057)
at android.app.Activity.performCreate(Activity.java:8037)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1341)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3688)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3864)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2253)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7870)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
このクラッシュは、どちらかというとクラッシュしてしまったアプリは被害者で、Intentを投げたアプリに問題があるといえます。
しかし、exportしている以上、任意のアプリからIntentを受け取ってしまうので、exportしているところでは対策を考えた方が良いでしょう。
なぜクラッシュするのか
異なるKeyのExtraを読み出している場合
ActivityがIntentを受け取った段階では、Extra(Bundle)の内部情報はParcelにシリアライズされたままです。
初めて何らかの読み出し処理が呼ばれるときにunparcel()
がコールされ、Parcelの情報がすべてデシリアライズされます。読み出そうとしたkeyとは無関係の値であっても、Parcel内に一つでもデシリアライズアできない値が含まれているとExceptionが発生してしまいます。
@Nullable
public String getString(@Nullable String key) {
unparcel();
final Object o = mMap.get(key);
try {
return (String) o;
} catch (ClassCastException e) {
typeWarning(key, o, "String", e);
return null;
}
}
なお、この挙動は API 33(Android 13) で変更されています。API 33以降では、デシリアライズが必要なオブジェクトはLazyValueとして格納され、直接その値を読み出すまではExceptionが発生しません。しかし、同じKeyが使用された場合や、SavedStateHandleを使用している場合はクラッシュしてしまうため、対策が必要です。
SavedStateHandle の場合
SavedStateHandleは初回作成時にデフォルト値としてIntentのExtrasを格納しています。(ActivityViewModelの場合)
以下のように、DEFAULT_ARGS_KEY を key として Intent の extras が格納された defaultViewModelCreationExtras が作られます。
@get:CallSuper
override val defaultViewModelCreationExtras: CreationExtras
/**
* {@inheritDoc}
*
* The extras of [getIntent] when this is first called will be used as
* the defaults to any [androidx.lifecycle.SavedStateHandle] passed to a view model
* created using this extra.
*/
get() {
val extras = MutableCreationExtras()
if (application != null) {
extras[APPLICATION_KEY] = application
}
extras[SAVED_STATE_REGISTRY_OWNER_KEY] = this
extras[VIEW_MODEL_STORE_OWNER_KEY] = this
val intentExtras = intent?.extras
if (intentExtras != null) {
extras[DEFAULT_ARGS_KEY] = intentExtras
}
return extras
}
そして、SavedStateViewModelFactoryがこの値をつかってSaveStateHandleを作成します。
@MainThread
public fun CreationExtras.createSavedStateHandle(): SavedStateHandle {
val savedStateRegistryOwner = this[SAVED_STATE_REGISTRY_OWNER_KEY]
?: throw IllegalArgumentException(
"CreationExtras must have a value by `SAVED_STATE_REGISTRY_OWNER_KEY`"
)
val viewModelStateRegistryOwner = this[VIEW_MODEL_STORE_OWNER_KEY]
?: throw IllegalArgumentException(
"CreationExtras must have a value by `VIEW_MODEL_STORE_OWNER_KEY`"
)
val defaultArgs = this[DEFAULT_ARGS_KEY]
val key = this[VIEW_MODEL_KEY] ?: throw IllegalArgumentException(
"CreationExtras must have a value by `VIEW_MODEL_KEY`"
)
return createSavedStateHandle(
savedStateRegistryOwner, viewModelStateRegistryOwner, key, defaultArgs
)
}
private fun createSavedStateHandle(
savedStateRegistryOwner: SavedStateRegistryOwner,
viewModelStoreOwner: ViewModelStoreOwner,
key: String,
defaultArgs: Bundle?
): SavedStateHandle {
val provider = savedStateRegistryOwner.savedStateHandlesProvider
val viewModel = viewModelStoreOwner.savedStateHandlesVM
// If we already have a reference to a previously created SavedStateHandle
// for a given key stored in our ViewModel, use that. Otherwise, create
// a new SavedStateHandle, providing it any restored state we might have saved
return viewModel.handles[key] ?: SavedStateHandle.createHandle(
provider.consumeRestoredStateForKey(key), defaultArgs
).also { viewModel.handles[key] = it }
}
最終的にはSavedStateHandleのファクトリーメソッドでextraの値を展開してstateとして格納しています。
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmStatic
@Suppress("DEPRECATION")
fun createHandle(restoredState: Bundle?, defaultState: Bundle?): SavedStateHandle {
if (restoredState == null) {
return if (defaultState == null) {
// No restored state and no default state -> empty SavedStateHandle
SavedStateHandle()
} else {
val state: MutableMap<String, Any?> = HashMap()
for (key in defaultState.keySet()) {
state[key] = defaultState[key]
}
SavedStateHandle(state)
}
}
// When restoring state, we use the restored state as the source of truth
// and ignore any default state, thus ensuring we are exactly the same
// state that was saved.
val keys: ArrayList<*>? = restoredState.getParcelableArrayList<Parcelable>(KEYS)
val values: ArrayList<*>? = restoredState.getParcelableArrayList<Parcelable>(VALUES)
check(!(keys == null || values == null || keys.size != values.size)) {
"Invalid bundle passed as restored state"
}
val state = mutableMapOf<String, Any?>()
for (i in keys.indices) {
state[keys[i] as String] = values[i]
}
return SavedStateHandle(state)
}
Extraのところで説明しましたが、API 33(Android 13)以上では unparcel()
だけではクラッシュしなくなりました。
しかし、SavedStateHandleではkeySet
ですべてのkeyを取り出し、イテレートしてすべての値を読み出し、詰め替えをしています。
API 33未満では、keySet
のコール時点でクラッシュし、API 33以上ではその後のget
でクラッシュ、とクラッシュする箇所がズレるだけで、いずれにせよクラッシュしてしまいます。
ViewModelの場合にやっかいなのが、ここで発生する例外を適切に処理するのが難しいところです。
対処方法
Chromiumのソースコードを読むと、IntentUtilというExtra参照をtry-catchで囲んだメソッドを一通り用意したユーティリティクラスを作っていて、Extraはこのメソッドを通じて扱うという対策をとっているようです。しかし、この方法はSavedStateHandleには適用できません。(SavedStateHandleの中で対処してほしいというのが正直なところですが)
Extraが読み出せないので、Extraに格納されたデータは諦めるしかありません。
そのため、読み出せない値が含まれていたら、Extraをすべて削除してしまうのが良いでしょう。ただし、IntentやBundleの公開メソッドを使ってunparcelされていないデータを削除するのは不可能なので、Extra以外の要素をコピーしたIntentで上書きするしかありません。
以下のように、extraの値を一通り読み出して、Exceptionが発生するかをチェック、発生した場合にはExtras以外をコピーしたIntentを返すメソッドを用意します。
fun Intent.sanitize(): Intent {
val extras = extras ?: return this
try {
extras.keySet()?.forEach {
@Suppress("DEPRECATION")
extras.get(it)
}
} catch (e: Throwable) {
return Intent(action, data)
.setFlags(flags)
.setComponent(component)
}
return this
}
そして、Activityのintentを検査して、ActivityのIntentを上書きします。
fun Activity.sanitizeIntent() {
intent = intent.sanitize()
}
これをActivityの先頭で実行することで、unparcelできないExtraが含まれている場合、Extraの内容を削除しておくことができます。
override fun onCreate(savedInstanceState: Bundle?) {
sanitizeIntent()
...
onNewIntentが必要なら以下でしょうか
fun Activity.setSanitizedIntent(intent: Intent) {
this.intent = intent.sanitize()
}
override fun onNewIntent(intent: Intent) {
setSanitizedIntent(intent)
当然、Extraにもとづいた動作はできませんが、読み出せないデータが含まれているようなIntentなので無視してしまっても問題にはならないでしょう。
以上です。