From c0221e385cfa1ddb9e9dca1cac98a72c3f657cba Mon Sep 17 00:00:00 2001 From: gela Date: Thu, 23 May 2024 21:27:03 +0000 Subject: [PATCH] Bug 1895560 - Create a new message controller for Microsurveys r=android-reviewers,amejiamarmol Differential Revision: https://phabricator.services.mozilla.com/D209740 --- .../components/service/nimbus/metrics.yaml | 23 ++++++ .../messaging/NimbusMessagingController.kt | 18 +++++ .../NimbusMessagingControllerInterface.kt | 13 ++++ mobile/android/fenix/app/metrics.yaml | 23 ------ .../fenix/components/appstate/AppAction.kt | 14 ++++ .../messaging/MicrosurveyMessageController.kt | 55 +++++++++++++++ .../messaging/state/MessagingMiddleware.kt | 18 +++++ .../fenix/messaging/state/MessagingReducer.kt | 2 +- .../fenix/telemetry/MicrosurveyUiData.kt | 44 ++++++++++++ .../MicrosurveyMessageControllerTest.kt | 70 +++++++++++++++++++ 10 files changed, 256 insertions(+), 24 deletions(-) create mode 100644 mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/MicrosurveyMessageController.kt create mode 100644 mobile/android/fenix/app/src/main/java/org/mozilla/fenix/telemetry/MicrosurveyUiData.kt create mode 100644 mobile/android/fenix/app/src/test/java/org/mozilla/fenix/messaging/MicrosurveyMessageControllerTest.kt diff --git a/mobile/android/android-components/components/service/nimbus/metrics.yaml b/mobile/android/android-components/components/service/nimbus/metrics.yaml index 4f02dd76f547..f398333106a4 100644 --- a/mobile/android/android-components/components/service/nimbus/metrics.yaml +++ b/mobile/android/android-components/components/service/nimbus/metrics.yaml @@ -108,3 +108,26 @@ messaging: data_sensitivity: - interaction expires: never + +micro_survey: + response: + type: event + description: User response data for a micro survey. + extra_keys: + survey_id: + description: The id of the survey. + type: string + user_selection: + description: | + The users selected option. For example, possible values are: + "Very satisfied", "satisfied", "neutral", "dissatisfied" and "Very dissatisfied". + type: string + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1891509 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1891509 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never diff --git a/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingController.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingController.kt index 391dfe0cb758..38ccc356c00a 100644 --- a/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingController.kt +++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingController.kt @@ -8,6 +8,7 @@ import android.content.Intent import android.net.Uri import androidx.annotation.VisibleForTesting import androidx.core.net.toUri +import mozilla.components.service.nimbus.GleanMetrics.MicroSurvey import mozilla.components.service.nimbus.GleanMetrics.Messaging as GleanMessaging /** @@ -46,6 +47,19 @@ open class NimbusMessagingController( messagingStorage.updateMetadata(updatedMetadata) } + /** + * Called when a microsurvey attached to a message has been completed by the user. + * + * @param message The message containing the microsurvey that was completed. + * @param answer The user's response to the microsurvey question. + */ + override suspend fun onMicrosurveyCompleted(message: Message, answer: String) { + val messageMetadata = message.metadata + sendMicrosurveyCompletedTelemetry(messageMetadata.id, answer) + val updatedMetadata = messageMetadata.copy(pressed = true) + messagingStorage.updateMetadata(updatedMetadata) + } + /** * Called once the user has clicked on a message. * @@ -112,6 +126,10 @@ open class NimbusMessagingController( ) } + private fun sendMicrosurveyCompletedTelemetry(messageId: String, answer: String?) { + MicroSurvey.response.record(MicroSurvey.ResponseExtra(surveyId = messageId, userSelection = answer)) + } + private fun convertActionIntoDeepLinkSchemeUri(action: String): Uri = if (action.startsWith("://")) { "$deepLinkScheme$action".toUri() diff --git a/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingControllerInterface.kt b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingControllerInterface.kt index 0b7e08046fb5..12e192a785f5 100644 --- a/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingControllerInterface.kt +++ b/mobile/android/android-components/components/service/nimbus/src/main/java/mozilla/components/service/nimbus/messaging/NimbusMessagingControllerInterface.kt @@ -15,6 +15,7 @@ import android.content.Intent * - [onMessageClicked] and [getIntentForMessage] to be called when the user taps on * the message, and to get the action for the message. * - [onMessageDismissed] to be called when the user dismisses the message. + * - [onMicrosurveyCompleted] to be called when a microsurvey has been completed by the user. */ interface NimbusMessagingControllerInterface { /** @@ -47,6 +48,18 @@ interface NimbusMessagingControllerInterface { */ suspend fun onMessageDismissed(message: Message) + /** + * Called when a microsurvey has been completed by the user. + * + * This function should be called when a microsurvey associated with a message + * has been completed by the user, providing the message object and the user's + * response in the form of LikertAnswers. + * + * @param message The message associated with the microsurvey. + * @param answer The user's response to the microsurvey, represented as LikertAnswers. + */ + suspend fun onMicrosurveyCompleted(message: Message, answer: String) + /** * Called once the user has clicked on a message. * diff --git a/mobile/android/fenix/app/metrics.yaml b/mobile/android/fenix/app/metrics.yaml index d7d71160e3b0..504803ee73d6 100644 --- a/mobile/android/fenix/app/metrics.yaml +++ b/mobile/android/fenix/app/metrics.yaml @@ -11571,29 +11571,6 @@ debug_drawer: - android-probes@mozilla.com expires: never -micro_survey: - response: - type: event - description: User response data for a micro survey. - extra_keys: - feature_name: - description: The name of the feature the survey is for e.g. "printing PDF". - type: string - user_selection: - description: | - The users selected option. For example, possible values are: - "Very satisfied", "satisfied", "neutral", "dissatisfied" and "Very dissatisfied". - type: string - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1891509 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1891509 - data_sensitivity: - - interaction - notification_emails: - - android-probes@mozilla.com - expires: never - navigation_bar: home_search_tapped: type: event diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt index f813ad33bd45..0b1df4db147a 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt @@ -188,6 +188,20 @@ sealed class AppAction : Action { * Indicates the given [message] was dismissed. */ data class MessageDismissed(val message: Message) : MessagingAction() + + /** + * Sealed class representing actions related to microsurveys within messaging functionality. + * Extends [MessagingAction]. + */ + sealed class MicrosurveyAction : MessagingAction() { + /** + * Indicates that the microsurvey associated with the [message] has been completed. + * + * @property message The message associated with the completed microsurvey. + * @property answer The answer provided for the microsurvey. + */ + data class Completed(val message: Message, val answer: String) : MicrosurveyAction() + } } /** diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/MicrosurveyMessageController.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/MicrosurveyMessageController.kt new file mode 100644 index 000000000000..ed04496b3d6f --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/MicrosurveyMessageController.kt @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.messaging + +import mozilla.components.service.nimbus.messaging.Message +import org.mozilla.fenix.BrowserDirection +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.components.AppStore +import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageClicked +import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDismissed +import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MicrosurveyAction.Completed + +/** + * Handles interactions with the microsurveys. + */ +class MicrosurveyMessageController( + private val appStore: AppStore, + private val homeActivity: HomeActivity, +) : MessageController { + + override fun onMessagePressed(message: Message) { + appStore.dispatch(MessageClicked(message)) + } + + override fun onMessageDismissed(message: Message) { + appStore.dispatch(MessageDismissed(message)) + } + + /** + * Handles the click event on the privacy link within a message. + * @param message The message containing the privacy link. + * @param privacyLink The URL of the privacy link. + */ + // Suppress unused parameter to work around the CI. + // message will be called by the UI when privacy noticed is clicked. + @Suppress("UNUSED_PARAMETER") + fun onPrivacyLinkClicked(message: Message, privacyLink: String) { + homeActivity.openToBrowserAndLoad( + searchTermOrURL = privacyLink, + newTab = true, + from = BrowserDirection.FromHome, + ) + } + + /** + * Dispatches an action when a survey is completed. + * @param message The message containing the completed survey. + * @param answer The answer provided in the survey. + */ + fun onSurveyCompleted(message: Message, answer: String) { + appStore.dispatch(Completed(message, answer)) + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/state/MessagingMiddleware.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/state/MessagingMiddleware.kt index 2122e1efeee1..4c8927a5a690 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/state/MessagingMiddleware.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/state/MessagingMiddleware.kt @@ -16,6 +16,7 @@ import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.ConsumeMe import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.Evaluate import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageClicked import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDismissed +import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MicrosurveyAction import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.Restore import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessageToShow import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessages @@ -58,6 +59,10 @@ class MessagingMiddleware( is MessageDismissed -> onMessageDismissed(context, action.message) + is MicrosurveyAction.Completed -> { + onMicrosurveyCompleted(context, action.message, action.answer) + } + else -> { // no-op } @@ -65,6 +70,19 @@ class MessagingMiddleware( next(action) } + private fun onMicrosurveyCompleted( + context: AppStoreMiddlewareContext, + message: Message, + answer: String, + ) { + val newMessages = removeMessage(context, message = message) + context.store.dispatch(UpdateMessages(newMessages)) + consumeMessageToShowIfNeeded(context, message) + coroutineScope.launch { + controller.onMicrosurveyCompleted(message, answer) + } + } + private fun onMessagedDisplayed( oldMessage: Message, context: AppStoreMiddlewareContext, diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/state/MessagingReducer.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/state/MessagingReducer.kt index 19c47cbc94d5..6250aa28c59e 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/state/MessagingReducer.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/messaging/state/MessagingReducer.kt @@ -15,7 +15,7 @@ import org.mozilla.fenix.messaging.MessagingState * Reducer for [MessagingState]. */ internal object MessagingReducer { - fun reduce(state: AppState, action: AppAction.MessagingAction): AppState = when (action) { + fun reduce(state: AppState, action: AppAction): AppState = when (action) { is UpdateMessageToShow -> { val messageToShow = state.messaging.messageToShow.toMutableMap() messageToShow[action.message.surface] = action.message diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/telemetry/MicrosurveyUiData.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/telemetry/MicrosurveyUiData.kt new file mode 100644 index 000000000000..fd80919ad4f4 --- /dev/null +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/telemetry/MicrosurveyUiData.kt @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.service.nimbus.messaging.microsurvey + +import androidx.annotation.DrawableRes + +/** + * Model containing the required data from a raw [MicrosurveyUiData] object in a UI state. + */ +data class MicrosurveyUiData( + val type: Type, + @DrawableRes val imageRes: Int, + val title: String, + val description: String, + val primaryButtonLabel: String, + val secondaryButtonLabel: String, +) { + /** + * Model for different types of Microsurvey ratings. + * + * @property fivePointScaleItems Identifier for the likert scale rating. + */ + enum class Type( + val fivePointScaleItems: String, + ) { + VERY_DISSATISFIED( + fivePointScaleItems = "very_dissatisfied", + ), + DISSATISFIED( + fivePointScaleItems = "dissatisfied", + ), + NEUTRAL( + fivePointScaleItems = "neutral", + ), + SATISFIED( + fivePointScaleItems = "satisfied", + ), + VERY_SATISFIED( + fivePointScaleItems = "very_satisfied", + ), + } +} diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/messaging/MicrosurveyMessageControllerTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/messaging/MicrosurveyMessageControllerTest.kt new file mode 100644 index 000000000000..c00aef3bf9d3 --- /dev/null +++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/messaging/MicrosurveyMessageControllerTest.kt @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.messaging + +import io.mockk.mockk +import io.mockk.verify +import mozilla.components.service.nimbus.messaging.Message +import mozilla.components.support.test.robolectric.testContext +import mozilla.telemetry.glean.testing.GleanTestRule +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.components.AppStore +import org.mozilla.fenix.components.appstate.AppAction +import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageClicked +import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MicrosurveyAction.Completed +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner + +@RunWith(FenixRobolectricTestRunner::class) +class MicrosurveyMessageControllerTest { + + @get:Rule + val gleanTestRule = GleanTestRule(testContext) + + private val homeActivity: HomeActivity = mockk(relaxed = true) + private val message: Message = mockk(relaxed = true) + private lateinit var microsurveyMessageController: MicrosurveyMessageController + private val appStore: AppStore = mockk(relaxed = true) + + @Before + fun setup() { + microsurveyMessageController = MicrosurveyMessageController( + appStore = appStore, + homeActivity = homeActivity, + ) + } + + @Test + fun `WHEN calling onMessagePressed THEN update the app store with the MessageClicked action`() { + microsurveyMessageController.onMessagePressed(message) + verify { appStore.dispatch(MessageClicked(message)) } + } + + @Test + fun `WHEN calling onMessageDismissed THEN update the app store with the MessageDismissed action`() { + microsurveyMessageController.onMessageDismissed(message) + + verify { appStore.dispatch(AppAction.MessagingAction.MessageDismissed(message)) } + } + + @Test + fun `WHEN calling onPrivacyLinkClicked THEN open the privacy URL in a new tab`() { + val privacyURL = "www.mozilla.com" + microsurveyMessageController.onPrivacyLinkClicked(message, privacyURL) + + verify { homeActivity.openToBrowserAndLoad(any(), newTab = true, any(), any()) } + } + + @Test + fun `WHEN calling onSurveyCompleted THEN update the app store with the SurveyCompleted action`() { + val answer = "satisfied" + microsurveyMessageController.onSurveyCompleted(message, answer) + + verify { appStore.dispatch(Completed(message, answer)) } + } +}