diff --git a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt index b0e6714ba68..34f32a62dc4 100644 --- a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt +++ b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt @@ -56,12 +56,15 @@ import mozilla.components.feature.prompts.file.FilePicker import mozilla.components.feature.prompts.share.DefaultShareDelegate import mozilla.components.feature.prompts.share.ShareDelegate import mozilla.components.lib.state.ext.flowScoped +import mozilla.components.support.base.crash.Breadcrumb +import mozilla.components.support.base.crash.CrashReporting import mozilla.components.support.base.feature.LifecycleAwareFeature import mozilla.components.support.base.feature.OnNeedToRequestPermissions import mozilla.components.support.base.feature.PermissionsFeature import mozilla.components.support.base.log.logger.Logger import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged +import java.lang.ClassCastException import java.lang.ref.WeakReference import java.security.InvalidParameterException import java.util.Date @@ -110,6 +113,7 @@ class PromptFeature private constructor( private val fragmentManager: FragmentManager, private val shareDelegate: ShareDelegate, override val loginValidationDelegate: LoginValidationDelegate? = null, + private val crashReporting: CrashReporting? = null, private val isSaveLoginEnabled: () -> Boolean = { false }, onNeedToRequestPermissions: OnNeedToRequestPermissions ) : LifecycleAwareFeature, PermissionsFeature, Prompter { @@ -131,6 +135,7 @@ class PromptFeature private constructor( fragmentManager: FragmentManager, shareDelegate: ShareDelegate = DefaultShareDelegate(), loginValidationDelegate: LoginValidationDelegate? = null, + crashReporting: CrashReporting? = null, isSaveLoginEnabled: () -> Boolean = { false }, onNeedToRequestPermissions: OnNeedToRequestPermissions ) : this( @@ -140,6 +145,7 @@ class PromptFeature private constructor( fragmentManager = fragmentManager, shareDelegate = shareDelegate, loginValidationDelegate = loginValidationDelegate, + crashReporting = crashReporting, isSaveLoginEnabled = isSaveLoginEnabled, onNeedToRequestPermissions = onNeedToRequestPermissions ) @@ -151,6 +157,7 @@ class PromptFeature private constructor( fragmentManager: FragmentManager, shareDelegate: ShareDelegate = DefaultShareDelegate(), loginValidationDelegate: LoginValidationDelegate? = null, + crashReporting: CrashReporting? = null, isSaveLoginEnabled: () -> Boolean = { false }, onNeedToRequestPermissions: OnNeedToRequestPermissions ) : this( @@ -160,6 +167,7 @@ class PromptFeature private constructor( fragmentManager = fragmentManager, shareDelegate = shareDelegate, loginValidationDelegate = loginValidationDelegate, + crashReporting = crashReporting, isSaveLoginEnabled = isSaveLoginEnabled, onNeedToRequestPermissions = onNeedToRequestPermissions ) @@ -184,6 +192,7 @@ class PromptFeature private constructor( fragmentManager = fragmentManager, shareDelegate = DefaultShareDelegate(), loginValidationDelegate = null, + crashReporting = null, onNeedToRequestPermissions = onNeedToRequestPermissions ) @@ -312,47 +321,58 @@ class PromptFeature private constructor( @Suppress("UNCHECKED_CAST", "ComplexMethod") override fun onConfirm(sessionId: String, value: Any?) { store.consumePromptFrom(sessionId, activePrompt) { - when (it) { - is TimeSelection -> it.onConfirm(value as Date) - is Color -> it.onConfirm(value as String) - is Alert -> { - val shouldNotShowMoreDialogs = value as Boolean - promptAbuserDetector.userWantsMoreDialogs(!shouldNotShowMoreDialogs) - it.onConfirm(!shouldNotShowMoreDialogs) - } - is SingleChoice -> it.onConfirm(value as Choice) - is MenuChoice -> it.onConfirm(value as Choice) - is PromptRequest.Popup -> it.onAllow() - is MultipleChoice -> it.onConfirm(value as Array) - - is Authentication -> { - val (user, password) = value as Pair - it.onConfirm(user, password) - } - - is TextPrompt -> { - val (shouldNotShowMoreDialogs, text) = value as Pair - - promptAbuserDetector.userWantsMoreDialogs(!shouldNotShowMoreDialogs) - it.onConfirm(!shouldNotShowMoreDialogs, text) - } + try { + when (it) { + is TimeSelection -> it.onConfirm(value as Date) + is Color -> it.onConfirm(value as String) + is Alert -> { + val shouldNotShowMoreDialogs = value as Boolean + promptAbuserDetector.userWantsMoreDialogs(!shouldNotShowMoreDialogs) + it.onConfirm(!shouldNotShowMoreDialogs) + } + is SingleChoice -> it.onConfirm(value as Choice) + is MenuChoice -> it.onConfirm(value as Choice) + is PromptRequest.Popup -> it.onAllow() + is MultipleChoice -> it.onConfirm(value as Array) + + is Authentication -> { + val (user, password) = value as Pair + it.onConfirm(user, password) + } - is Share -> it.onSuccess() + is TextPrompt -> { + val (shouldNotShowMoreDialogs, text) = value as Pair - is LoginPrompt -> it.onConfirm(value as Login) + promptAbuserDetector.userWantsMoreDialogs(!shouldNotShowMoreDialogs) + it.onConfirm(!shouldNotShowMoreDialogs, text) + } - is PromptRequest.Confirm -> { - val (isCheckBoxChecked, buttonType) = value as Pair - promptAbuserDetector.userWantsMoreDialogs(!isCheckBoxChecked) - when (buttonType) { - MultiButtonDialogFragment.ButtonType.POSITIVE -> - it.onConfirmPositiveButton(!isCheckBoxChecked) - MultiButtonDialogFragment.ButtonType.NEGATIVE -> - it.onConfirmNegativeButton(!isCheckBoxChecked) - MultiButtonDialogFragment.ButtonType.NEUTRAL -> - it.onConfirmNeutralButton(!isCheckBoxChecked) + is Share -> it.onSuccess() + + is LoginPrompt -> it.onConfirm(value as Login) + + is PromptRequest.Confirm -> { + val (isCheckBoxChecked, buttonType) = + value as Pair + promptAbuserDetector.userWantsMoreDialogs(!isCheckBoxChecked) + when (buttonType) { + MultiButtonDialogFragment.ButtonType.POSITIVE -> + it.onConfirmPositiveButton(!isCheckBoxChecked) + MultiButtonDialogFragment.ButtonType.NEGATIVE -> + it.onConfirmNegativeButton(!isCheckBoxChecked) + MultiButtonDialogFragment.ButtonType.NEUTRAL -> + it.onConfirmNeutralButton(!isCheckBoxChecked) + } } } + } catch (e: ClassCastException) { + crashReporting?.recordCrashBreadcrumb( + Breadcrumb("PromptFeature onConsume cast failed", + hashMapOf("class name" to " ${it.javaClass}"), "crash", + Breadcrumb.Level.DEBUG, Breadcrumb.Type.NAVIGATION) + ) + + throw e } } } diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptFeatureTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptFeatureTest.kt index 773b6318f7f..45efb6c2a23 100644 --- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptFeatureTest.kt +++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptFeatureTest.kt @@ -45,6 +45,7 @@ import mozilla.components.feature.prompts.dialog.MultiButtonDialogFragment import mozilla.components.feature.prompts.dialog.PromptDialogFragment import mozilla.components.feature.prompts.file.FilePicker.Companion.FILE_PICKER_ACTIVITY_REQUEST_CODE import mozilla.components.feature.prompts.share.ShareDelegate +import mozilla.components.support.base.crash.CrashReporting import mozilla.components.support.test.any import mozilla.components.support.test.eq import mozilla.components.support.test.ext.joinBlocking @@ -123,6 +124,7 @@ class PromptFeatureTest { fragment = mock(), store = store, customTabId = "custom-tab", + crashReporting = mock(), fragmentManager = fragmentManager) { } ) feature.start() @@ -961,6 +963,32 @@ class PromptFeatureTest { assertFalse(prompt!!.shouldDismissOnLoad()) } + @Test + fun `PromptFeature adds breadcrumb when ClassCastException is triggered`() { + val crashReporting: CrashReporting = mock() + val feature = PromptFeature( + activity = mock(), + store = store, + fragmentManager = fragmentManager, + crashReporting = crashReporting + ) { } + feature.start() + + val singleChoiceRequest = SingleChoice(arrayOf()) {} + var classCastExceptionThrown = false + store.dispatch(ContentAction.UpdatePromptRequestAction(tabId, singleChoiceRequest)).joinBlocking() + + try { + feature.onConfirm(tabId, "wrong") + } catch (e: ClassCastException) { + classCastExceptionThrown = true + } + + store.waitUntilIdle() + assert(classCastExceptionThrown) + verify(crashReporting).recordCrashBreadcrumb(any()) + } + private fun mockFragmentManager(): FragmentManager { val fragmentManager: FragmentManager = mock() val transaction: FragmentTransaction = mock()