tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 63d556f30e3aa8cc551b1c24aba31cec1f9f1116
parent b7e0462f57d5f90d28295ac43278f6c021d3522c
Author: Marcin KoziƄski <mkozinski@mozilla.com>
Date:   Fri, 17 Oct 2025 11:56:39 +0000

Bug 1986398 - Refactor recordReviewPromptEvent to extract determining if prompt was displayed r=android-reviewers,twhite

Differential Revision: https://phabricator.services.mozilla.com/D263363

Diffstat:
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/PlayStoreReviewPromptController.kt | 87++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/PlayStoreReviewPromptControllerTest.kt | 44+++++++++++++++++++++++++++++++++++---------
2 files changed, 99 insertions(+), 32 deletions(-)

diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/PlayStoreReviewPromptController.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/PlayStoreReviewPromptController.kt @@ -18,6 +18,10 @@ import mozilla.components.support.base.log.logger.Logger import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.GleanMetrics.ReviewPrompt import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.components.ReviewPromptAttemptResult.Displayed +import org.mozilla.fenix.components.ReviewPromptAttemptResult.Error +import org.mozilla.fenix.components.ReviewPromptAttemptResult.NotDisplayed +import org.mozilla.fenix.components.ReviewPromptAttemptResult.Unknown import org.mozilla.fenix.settings.SupportUtils import java.text.SimpleDateFormat import java.util.Date @@ -44,12 +48,12 @@ class PlayStoreReviewPromptController( val reviewInfoFlow = withContext(Dispatchers.IO) { manager.requestReviewFlow() } reviewInfoFlow.addOnCompleteListener(activity) { - val resultString = if (it.isSuccessful) { + val result = if (it.isSuccessful) { logger.info("Review flow launched.") // Launch the in-app flow. manager.launchReviewFlow(activity, it.result) - it.result.toString() + ReviewPromptAttemptResult.from(it.result.toString()) } else { // Launch the Play store flow. val reviewErrorCode = @@ -58,11 +62,11 @@ class PlayStoreReviewPromptController( onError() - "reviewErrorCode=$reviewErrorCode" + Error } recordReviewPromptEvent( - reviewInfoAsString = resultString, + promptAttemptResult = result, numberOfAppLaunches = numberOfAppLaunches(), now = Date(), ) @@ -105,35 +109,72 @@ class PlayStoreReviewPromptController( } /** + * Result of an attempt to show a Play Store In-App Review Prompt. + */ +@VisibleForTesting +enum class ReviewPromptAttemptResult { + /** + * Attempted completed without error, but the API didn't allow to display the prompt. + */ + NotDisplayed, + + /** + * Prompt has been shown. + */ + Displayed, + + /** + * There was an error, for example this is a device without Play Store. + */ + Error, + + /** + * Attempt completed without error, but we weren't able to determine if prompt has been shown or not. + */ + Unknown, + ; + + companion object { + /** + * The docs for [ReviewManager.launchReviewFlow] state 'In some circumstances the review + * flow will not be shown to the user, e.g. they have already seen it recently, so do not assume that + * calling this method will always display the review dialog.' + * However, investigation has shown that a [ReviewInfo] instance with the flag: + * - 'isNoOp=true' indicates that the prompt has NOT been displayed. + * - 'isNoOp=false' indicates that a prompt has been displayed. + * [ReviewManager.launchReviewFlow] will modify the ReviewInfo instance which can be used to determine + * which of these flags is present. + * + * The internals of ReviewInfo cannot be accessed directly or cast nicely, so let's simply use + * the object as a string. + */ + fun from(reviewInfoAsString: String): ReviewPromptAttemptResult { + return when { + reviewInfoAsString.contains("isNoOp=true") -> NotDisplayed + reviewInfoAsString.contains("isNoOp=false") -> Displayed + // ReviewInfo is susceptible to changes outside of our control hence the catch-all 'else' statement. + else -> Unknown + } + } + } +} + +/** * Records a [ReviewPrompt] with the required data. - * - * **Note:** The docs for [ReviewManager.launchReviewFlow] state 'In some circumstances the review - * flow will not be shown to the user, e.g. they have already seen it recently, so do not assume that - * calling this method will always display the review dialog.' - * However, investigation has shown that a [ReviewInfo] instance with the flag: - * - 'isNoOp=true' indicates that the prompt has NOT been displayed. - * - 'isNoOp=false' indicates that a prompt has been displayed. - * [ReviewManager.launchReviewFlow] will modify the ReviewInfo instance which can be used to determine - * which of these flags is present. */ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) fun recordReviewPromptEvent( - reviewInfoAsString: String, + promptAttemptResult: ReviewPromptAttemptResult, numberOfAppLaunches: Int, now: Date, ) { val formattedLocalDatetime = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()).format(now) - // The internals of ReviewInfo cannot be accessed directly or cast nicely, so lets simply use - // the object as a string. - // ReviewInfo is susceptible to changes outside of our control hence the catch-all 'else' statement. - val promptWasDisplayed = if (reviewInfoAsString.contains("isNoOp=true")) { - "false" - } else if (reviewInfoAsString.contains("isNoOp=false")) { - "true" - } else { - "error" + val promptWasDisplayed = when (promptAttemptResult) { + NotDisplayed -> "false" + Displayed -> "true" + Error, Unknown -> "error" } ReviewPrompt.promptAttempt.record( diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/PlayStoreReviewPromptControllerTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/PlayStoreReviewPromptControllerTest.kt @@ -27,6 +27,9 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mozilla.fenix.GleanMetrics.ReviewPrompt +import org.mozilla.fenix.components.ReviewPromptAttemptResult.Displayed +import org.mozilla.fenix.components.ReviewPromptAttemptResult.NotDisplayed +import org.mozilla.fenix.components.ReviewPromptAttemptResult.Unknown import org.mozilla.fenix.helpers.FenixGleanTestRule import org.robolectric.RobolectricTestRunner import java.text.SimpleDateFormat @@ -104,35 +107,58 @@ class PlayStoreReviewPromptControllerTest { } @Test + fun `WHEN review info contains 'isNoOp=false' THEN prompt was displayed`() { + val displayState = ReviewPromptAttemptResult.from(createReviewInfoString("isNoOp=false")) + + assertEquals(Displayed, displayState) + } + + @Test + fun `WHEN review info contains 'isNoOp=true' THEN prompt wasn't displayed`() { + val displayState = ReviewPromptAttemptResult.from(createReviewInfoString("isNoOp=true")) + + assertEquals(NotDisplayed, displayState) + } + + @Test + fun `WHEN review info doesn't contain 'isNoOp' THEN prompt display state is unknown`() { + val displayState = ReviewPromptAttemptResult.from(createReviewInfoString()) + + assertEquals(Unknown, displayState) + } + + private fun createReviewInfoString(optionalArg: String? = ""): String { + return "ReviewInfo{pendingIntent=PendingIntent{5b613b1: android.os.BinderProxy@46c8096}, $optionalArg}" + } + + @Test fun reviewPromptWasDisplayed() { - testRecordReviewPromptEventRecordsTheExpectedData("isNoOp=false", "true") + testRecordReviewPromptEventRecordsTheExpectedData(Displayed, "true") } @Test fun reviewPromptWasNotDisplayed() { - testRecordReviewPromptEventRecordsTheExpectedData("isNoOp=true", "false") + testRecordReviewPromptEventRecordsTheExpectedData(NotDisplayed, "false") } @Test fun reviewPromptDisplayStateUnknown() { - testRecordReviewPromptEventRecordsTheExpectedData(expected = "error") + testRecordReviewPromptEventRecordsTheExpectedData(Unknown, "error") } private fun testRecordReviewPromptEventRecordsTheExpectedData( - reviewInfoArg: String = "", - expected: String, + promptDisplayState: ReviewPromptAttemptResult, + promptWasDisplayed: String, ) { val numberOfAppLaunches = 1 - val reviewInfoAsString = - "ReviewInfo{pendingIntent=PendingIntent{5b613b1: android.os.BinderProxy@46c8096}, $reviewInfoArg}" val datetime = Date(TEST_TIME_NOW) val formattedNowLocalDatetime = SIMPLE_DATE_FORMAT.format(datetime) assertNull(ReviewPrompt.promptAttempt.testGetValue()) - recordReviewPromptEvent(reviewInfoAsString, numberOfAppLaunches, datetime) + recordReviewPromptEvent(promptDisplayState, numberOfAppLaunches, datetime) val reviewPromptData = ReviewPrompt.promptAttempt.testGetValue()!!.last().extra!! - assertEquals(expected, reviewPromptData["prompt_was_displayed"]) + assertEquals(promptWasDisplayed, reviewPromptData["prompt_was_displayed"]) assertEquals(numberOfAppLaunches, reviewPromptData["number_of_app_launches"]!!.toInt()) assertEquals(formattedNowLocalDatetime, reviewPromptData["local_datetime"]) }