Skip to content
This repository has been archived by the owner on Jun 20, 2023. It is now read-only.

Fix class cast exception on pending tests (EXPOSUREAPP-4909) #2249

Merged

Conversation

d4rken
Copy link
Member

@d4rken d4rken commented Feb 1, 2021

The viewmodel on the pending test has a livedata object to show errors to the user, the code looks like this:

submissionRepository.deviceUIStateFlow
        .filterIsInstance<NetworkRequestWrapper.RequestFailed<DeviceUIState, CwaWebexception>>()
        .map { it.error }
        .asLiveData()

The UI code consuming this calls:

fun handleError(exception: CwaWebException)

The stacktrace shows a class cast exception at that point.

Stacktrace from Play Console
java.lang.ClassCastException: 
  at de.rki.coronawarnapp.ui.submission.testresult.pending.SubmissionTestResultPendingFragment$onResume$1.invoke (SubmissionTestResultPendingFragment.kt:1)
  at de.rki.coronawarnapp.util.ui.LiveDataExtensionsKt$observeOnce$internalObserver$1.onChanged (LiveDataExtensions.kt:1)
  at androidx.lifecycle.LiveData.considerNotify (LiveData.java:6)
  at androidx.lifecycle.LiveData.dispatchingValue (LiveData.java:8)
  at androidx.lifecycle.MutableLiveData.setValue (MutableLiveData.java:4)
  at androidx.lifecycle.LiveDataScopeImpl$emit$2.invokeSuspend (CoroutineLiveData.kt:10)
  at androidx.lifecycle.LiveDataScopeImpl$emit$2.invoke (CoroutineLiveData.kt:2)
  at com.google.zxing.client.android.R$id.startUndispatchedOrReturn (Unknown Source)
  at com.google.zxing.client.android.R$id.withContext (Unknown Source)
  at androidx.lifecycle.LiveDataScopeImpl.emit (CoroutineLiveData.kt:1)
  at androidx.lifecycle.FlowLiveDataConversions$asLiveData$1$invokeSuspend$$inlined$collect$1.emit (Collect.kt:5)
  at de.rki.coronawarnapp.ui.submission.testresult.pending.SubmissionTestResultPendingViewModel$$special$$inlined$map$1$2.emit (Collect.kt:8)
  at de.rki.coronawarnapp.ui.submission.testresult.pending.SubmissionTestResultPendingViewModel$$special$$inlined$filterIsInstance$1$2.emit (Collect.kt:6)
  at kotlinx.coroutines.flow.StateFlowImpl.collect (StateFlow.kt:13)
  at kotlinx.coroutines.flow.StateFlowImpl$collect$1.invokeSuspend (StateFlow.kt)
  at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:3)
  at kotlinx.coroutines.DispatchedTask.run (DispatchedTask.kt:15)
  at android.os.Handler.handleCallback (Handler.java:751)
  at android.os.Handler.dispatchMessage (Handler.java:95)
  at android.os.Looper.loop (Looper.java:154)
  at android.app.ActivityThread.main (ActivityThread.java:6316)
  at java.lang.reflect.Method.invoke (Native Method)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run (ZygoteInit.java:872)
  at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:762)

While I can't reproduce it, what I think happens is filterIsInstance can't filter based on the generics so

NetworkRequestWrapper.RequestFailed<DeviceUIState, CwaWebexception>

becomes

NetworkRequestWrapper.RequestFailed<DeviceUIState, Throwable>

the compiler doesn't notice this during build time though (type erasure?).

When then an exception is thrown that is not a CwaWebexception we try to cast it to one anyways -> Crash 💣

NetworkRequestWrapper does not enforce any types for the generics both value and error are <out T, out U>

So I also thought that there might be a null exception being passed, but that was not the case, the crash on a null exception looks like this.

Stacktrace on crash due to null exception
2021-02-01 12:25:43.340 30834-30834/de.rki.coronawarnapp.test E/AndroidRuntime: FATAL EXCEPTION: main @coroutine#472
    Process: de.rki.coronawarnapp.test, PID: 30834
    java.lang.NullPointerException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkNotNullParameter, parameter exception
        at de.rki.coronawarnapp.ui.submission.testresult.pending.SubmissionTestResultPendingFragment$onResume$1.invoke(Unknown Source:2)
        at de.rki.coronawarnapp.ui.submission.testresult.pending.SubmissionTestResultPendingFragment$onResume$1.invoke(SubmissionTestResultPendingFragment.kt:27)
        at de.rki.coronawarnapp.util.ui.LiveDataExtensionsKt$observeOnce$internalObserver$1.onChanged(LiveDataExtensions.kt:17)
        at androidx.lifecycle.LiveData.considerNotify(LiveData.java:131)
        at androidx.lifecycle.LiveData.dispatchingValue(LiveData.java:149)
        at androidx.lifecycle.LiveData.setValue(LiveData.java:307)
        at androidx.lifecycle.MutableLiveData.setValue(MutableLiveData.java:50)
        at androidx.lifecycle.LiveDataScopeImpl$emit$2.invokeSuspend(CoroutineLiveData.kt:99)
        at androidx.lifecycle.LiveDataScopeImpl$emit$2.invoke(Unknown Source:10)
        at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:91)
        at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:165)
        at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1)
        at androidx.lifecycle.LiveDataScopeImpl.emit(CoroutineLiveData.kt:97)
        at androidx.lifecycle.FlowLiveDataConversions$asLiveData$1$invokeSuspend$$inlined$collect$1.emit(Collect.kt:136)
        at de.rki.coronawarnapp.ui.submission.testresult.pending.SubmissionTestResultPendingViewModel$$special$$inlined$map$1$2.emit(Collect.kt:135)
        at de.rki.coronawarnapp.ui.submission.testresult.pending.SubmissionTestResultPendingViewModel$$special$$inlined$filterIsInstance$1$2.emit(Collect.kt:135)
        at kotlinx.coroutines.flow.FlowKt__BuildersKt$flowOf$$inlined$unsafeFlow$2.collect(SafeCollector.common.kt:113)
        at de.rki.coronawarnapp.ui.submission.testresult.pending.SubmissionTestResultPendingViewModel$$special$$inlined$filterIsInstance$1.collect(SafeCollector.common.kt:114)
        at de.rki.coronawarnapp.ui.submission.testresult.pending.SubmissionTestResultPendingViewModel$$special$$inlined$map$1.collect(SafeCollector.common.kt:114)
        at androidx.lifecycle.FlowLiveDataConversions$asLiveData$1.invokeSuspend(FlowLiveData.kt:139)
        at androidx.lifecycle.FlowLiveDataConversions$asLiveData$1.invoke(Unknown Source:10)
        at androidx.lifecycle.BlockRunner$maybeRun$1.invokeSuspend(CoroutineLiveData.kt:176)
        (Coroutine boundary)
        at androidx.lifecycle.FlowLiveDataConversions$asLiveData$1$invokeSuspend$$inlined$collect$1.emit(FlowLiveData.kt:136)
        at de.rki.coronawarnapp.ui.submission.testresult.pending.SubmissionTestResultPendingViewModel$$special$$inlined$map$1$2.emit(SubmissionTestResultPendingViewModel.kt:135)
        at de.rki.coronawarnapp.ui.submission.testresult.pending.SubmissionTestResultPendingViewModel$$special$$inlined$filterIsInstance$1$2.emit(SubmissionTestResultPendingViewModel.kt:135)
        at androidx.lifecycle.FlowLiveDataConversions$asLiveData$1.invokeSuspend(FlowLiveData.kt:139)
        at androidx.lifecycle.BlockRunner$maybeRun$1.invokeSuspend(CoroutineLiveData.kt:176)
     Caused by: java.lang.NullPointerException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkNotNullParameter, parameter exception
        at de.rki.coronawarnapp.ui.submission.testresult.pending.SubmissionTestResultPendingFragment$onResume$1.invoke(Unknown Source:2)
        at de.rki.coronawarnapp.ui.submission.testresult.pending.SubmissionTestResultPendingFragment$onResume$1.invoke(SubmissionTestResultPendingFragment.kt:27)
        at de.rki.coronawarnapp.util.ui.LiveDataExtensionsKt$observeOnce$internalObserver$1.onChanged(LiveDataExtensions.kt:17)
        at androidx.lifecycle.LiveData.considerNotify(LiveData.java:131)
        at androidx.lifecycle.LiveData.dispatchingValue(LiveData.java:149)
        at androidx.lifecycle.LiveData.setValue(LiveData.java:307)
2021-02-01 12:25:43.342 30834-30834/de.rki.coronawarnapp.test E/AndroidRuntime:     at androidx.lifecycle.MutableLiveData.setValue(MutableLiveData.java:50)
        at androidx.lifecycle.LiveDataScopeImpl$emit$2.invokeSuspend(CoroutineLiveData.kt:99)
        at androidx.lifecycle.LiveDataScopeImpl$emit$2.invoke(Unknown Source:10)
        at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:91)
        at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:165)
        at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1)
        at androidx.lifecycle.LiveDataScopeImpl.emit(CoroutineLiveData.kt:97)
        at androidx.lifecycle.FlowLiveDataConversions$asLiveData$1$invokeSuspend$$inlined$collect$1.emit(Collect.kt:136)
        at de.rki.coronawarnapp.ui.submission.testresult.pending.SubmissionTestResultPendingViewModel$$special$$inlined$map$1$2.emit(Collect.kt:135)
        at de.rki.coronawarnapp.ui.submission.testresult.pending.SubmissionTestResultPendingViewModel$$special$$inlined$filterIsInstance$1$2.emit(Collect.kt:135)
        at kotlinx.coroutines.flow.FlowKt__BuildersKt$flowOf$$inlined$unsafeFlow$2.collect(SafeCollector.common.kt:113)
        at de.rki.coronawarnapp.ui.submission.testresult.pending.SubmissionTestResultPendingViewModel$$special$$inlined$filterIsInstance$1.collect(SafeCollector.common.kt:114)
        at de.rki.coronawarnapp.ui.submission.testresult.pending.SubmissionTestResultPendingViewModel$$special$$inlined$map$1.collect(SafeCollector.common.kt:114)
        at androidx.lifecycle.FlowLiveDataConversions$asLiveData$1.invokeSuspend(FlowLiveData.kt:139)
        at androidx.lifecycle.FlowLiveDataConversions$asLiveData$1.invoke(Unknown Source:10)
        at androidx.lifecycle.BlockRunner$maybeRun$1.invokeSuspend(CoroutineLiveData.kt:176)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:342)
        at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:30)
        at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable$default(Cancellable.kt:27)
        at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109)
        at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:158)
        at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:56)
        at kotlinx.coroutines.BuildersKt.launch(Unknown Source:1)
        at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch$default(Builders.common.kt:49)
        at kotlinx.coroutines.BuildersKt.launch$default(Unknown Source:1)
        at androidx.lifecycle.BlockRunner.maybeRun(CoroutineLiveData.kt:174)
        at androidx.lifecycle.CoroutineLiveData.onActive(CoroutineLiveData.kt:240)
        at androidx.lifecycle.LiveData$ObserverWrapper.activeStateChanged(LiveData.java:437)
        at androidx.lifecycle.LiveData$LifecycleBoundObserver.onStateChanged(LiveData.java:395)
        at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:361)
        at androidx.lifecycle.LifecycleRegistry.addObserver(LifecycleRegistry.java:188)
        at androidx.lifecycle.LiveData.observe(LiveData.java:203)
        at de.rki.coronawarnapp.util.ui.LiveDataExtensionsKt.observeOnce(LiveDataExtensions.kt:24)
        at de.rki.coronawarnapp.ui.submission.testresult.pending.SubmissionTestResultPendingFragment.onResume(SubmissionTestResultPendingFragment.kt:95)
        at androidx.fragment.app.Fragment.performResume(Fragment.java:2748)
        at androidx.fragment.app.FragmentStateManager.resume(FragmentStateManager.java:373)
        at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1211)
        at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1368)
        at androidx.fragment.app.FragmentManager.moveFragmentToExpectedState(FragmentManager.java:1446)
        at androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1509)
        at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2019)
        at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:1965)
2021-02-01 12:25:43.342 30834-30834/de.rki.coronawarnapp.test E/AndroidRuntime:     at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1861)
        at androidx.fragment.app.FragmentManager$4.run(FragmentManager.java:413)
        at android.os.Handler.handleCallback(Handler.java:938)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

to prevent any future issues there I've adjusted the wrapper class to have stronger typing, i.e.:

NetworkRequestWrapper<out T : Any, out U : Any> to prevent null arguments,
and RequestFailed<T : Any, U : Throwable> to enforce that the second, error parameter is actually a Throwable.

Testing

  • Review code and produce various exceptions on the pending test screen
  • Complete a submission using TAN and QR code in deviceForTestersRelease

No nulls and errors need to be `Throwable`
The expectation is that due to type erase, the filter will let any type of `RequestFailed` through anyways.
@d4rken d4rken added bug Something isn't working maintainers Tag pull requests created by maintainers labels Feb 1, 2021
@d4rken d4rken added this to the 1.12.0 milestone Feb 1, 2021
@d4rken d4rken requested a review from a team February 1, 2021 11:57
@ralfgehrer ralfgehrer added the prio PRs to review first. label Feb 1, 2021
@ralfgehrer ralfgehrer self-assigned this Feb 1, 2021
Copy link
Contributor

@ralfgehrer ralfgehrer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Nice catch.

@chiljamgossow chiljamgossow self-assigned this Feb 1, 2021
true,
::navigateToMainScreen
)
private val networkErrorDialog: DialogHelper.DialogInstance
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these two error dialogs identical? There used to be the http code included in the web exception, but it is not there. Maybe the generic one is sufficient anyway.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The body is different

@sonarcloud
Copy link

sonarcloud bot commented Feb 2, 2021

Kudos, SonarCloud Quality Gate passed!

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 0 Code Smells

50.0% 50.0% Coverage
0.0% 0.0% Duplication

@ralfgehrer ralfgehrer merged commit 9dec672 into release/1.12.x Feb 2, 2021
@ralfgehrer ralfgehrer deleted the fix/4909-class-cast-exception-on-pending-test branch February 2, 2021 15:05
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
bug Something isn't working maintainers Tag pull requests created by maintainers prio PRs to review first.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants