Skip to content

Commit

Permalink
Allow play store in app review to be shown to users
Browse files Browse the repository at this point in the history
For mozilla-mobile#13507 - Extracts review prompt behavior into ReviewPromptController

For mozilla-mobile#13507 - Adds tests for ReviewPromptController

For mozilla-mobile#13507 - Performance fixes for the ReviewPromptController

For mozilla-mobile#14318 - Use old API to try to fix startup crash without GPS
  • Loading branch information
eliserichards authored and boek committed Aug 31, 2020
1 parent b4e7f0d commit 6282b41
Show file tree
Hide file tree
Showing 10 changed files with 350 additions and 0 deletions.
2 changes: 2 additions & 0 deletions app/build.gradle
Expand Up @@ -443,6 +443,8 @@ dependencies {

implementation Deps.google_ads_id // Required for the Google Advertising ID

implementation Deps.google_play_store // Required for in-app reviews

androidTestImplementation Deps.uiautomator
// Removed pending AndroidX fixes
androidTestImplementation "tools.fastlane:screengrab:2.0.0"
Expand Down
2 changes: 2 additions & 0 deletions app/lint.xml
Expand Up @@ -7,6 +7,8 @@
<!-- The Sentry SDK is compiled against parts of the Java SDK that are not available in the Android SDK.
Let's just ignore issues in the Sentry code since that is a third-party dependency anyways. -->
<ignore path="**/sentry*.jar" />
<!-- Temporary until https://github.com/Kotlin/kotlinx.coroutines/issues/2004 is resolved. -->
<ignore path="**/kotlinx-coroutines-core-*.jar"/>
</issue>
<!-- Lints that don't apply to our translation process -->
<issue id="MissingTranslation" severity="ignore" />
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/org/mozilla/fenix/FenixApplication.kt
Expand Up @@ -42,6 +42,7 @@ import mozilla.components.support.webextensions.WebExtensionSupport
import org.mozilla.fenix.StrictModeManager.enableStrictMode
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.metrics.MetricServiceType
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.resetPoliciesAfter
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.StorageStatsMetrics
Expand Down Expand Up @@ -217,13 +218,20 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
}
}

fun queueReviewPrompt() {
GlobalScope.launch(Dispatchers.IO) {
components.reviewPromptController.trackApplicationLaunch()
}
}

initQueue()

// We init these items in the visual completeness queue to avoid them initing in the critical
// startup path, before the UI finishes drawing (i.e. visual completeness).
queueInitExperiments()
queueInitStorageAndServices()
queueMetrics()
queueReviewPrompt()
}

private fun startMetricsIfEnabled() {
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/org/mozilla/fenix/components/Components.kt
Expand Up @@ -19,6 +19,7 @@ import mozilla.components.support.migration.state.MigrationStore
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.components.metrics.AppAllSourceStartTelemetry
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.utils.ClipboardHandler
import org.mozilla.fenix.utils.Mockable
import org.mozilla.fenix.utils.Settings
Expand Down Expand Up @@ -113,4 +114,11 @@ class Components(private val context: Context) {
val wifiConnectionMonitor by lazy { WifiConnectionMonitor(context.getSystemService()!!) }

val settings by lazy { Settings(context) }

val reviewPromptController by lazy {
ReviewPromptController(
context,
FenixReviewSettings(settings)
)
}
}
@@ -0,0 +1,101 @@
/* 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.components

import android.app.Activity
import android.content.Context
import androidx.annotation.VisibleForTesting
import com.google.android.play.core.review.ReviewManagerFactory
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.withContext
import org.mozilla.fenix.utils.Settings

/**
* Interface that describes the settings needed to track the Review Prompt.
*/
interface ReviewSettings {
var numberOfAppLaunches: Int
val isDefaultBrowser: Boolean
var lastReviewPromptTimeInMillis: Long
}

/**
* Wraps `Settings` to conform to `ReviewSettings`.
*/
class FenixReviewSettings(
val settings: Settings
) : ReviewSettings {
override var numberOfAppLaunches: Int
get() = settings.numberOfAppLaunches
set(value) { settings.numberOfAppLaunches = value }
override val isDefaultBrowser: Boolean
get() = settings.isDefaultBrowser()
override var lastReviewPromptTimeInMillis: Long
get() = settings.lastReviewPromptTimeInMillis
set(value) { settings.lastReviewPromptTimeInMillis = value }
}

/**
* Controls the Review Prompt behavior.
*/
class ReviewPromptController(
private val context: Context,
private val reviewSettings: ReviewSettings,
private val timeNowInMillis: () -> Long = { System.currentTimeMillis() },
private val tryPromptReview: suspend (Activity) -> Unit = { activity ->
val manager = ReviewManagerFactory.create(context)
val flow = manager.requestReviewFlow()

withContext(Main) {
flow.addOnCompleteListener {
if (it.isSuccessful) {
manager.launchReviewFlow(activity, it.result)
}
}
}
}
) {
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
@Volatile var reviewPromptIsReady = false

suspend fun promptReview(activity: Activity) {
if (shouldShowPrompt()) {
tryPromptReview(activity)
reviewSettings.lastReviewPromptTimeInMillis = timeNowInMillis()
}
}

fun trackApplicationLaunch() {
reviewSettings.numberOfAppLaunches = reviewSettings.numberOfAppLaunches + 1
// We only want to show the the prompt after we've finished "launching" the application.
reviewPromptIsReady = true
}

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
fun shouldShowPrompt(): Boolean {
if (!reviewPromptIsReady) {
return false
} else {
// We only want to try to show it once to avoid unnecessary disk reads
reviewPromptIsReady = false
}

if (!reviewSettings.isDefaultBrowser) { return false }

val hasOpenedFiveTimes = reviewSettings.numberOfAppLaunches >= NUMBER_OF_LAUNCHES_REQUIRED
val now = timeNowInMillis()
val apprxFourMonthsAgo = now - (APPRX_MONTH_IN_MILLIS * NUMBER_OF_MONTHS_TO_PASS)
val lastPrompt = reviewSettings.lastReviewPromptTimeInMillis
val hasNotBeenPromptedLastFourMonths = lastPrompt == 0L || lastPrompt <= apprxFourMonthsAgo

return hasOpenedFiveTimes && hasNotBeenPromptedLastFourMonths
}

companion object {
private const val APPRX_MONTH_IN_MILLIS: Long = 1000L * 60L * 60L * 24L * 30L
private const val NUMBER_OF_LAUNCHES_REQUIRED = 5
private const val NUMBER_OF_MONTHS_TO_PASS = 4
}
}
4 changes: 4 additions & 0 deletions app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt
Expand Up @@ -554,6 +554,10 @@ class HomeFragment : Fragment() {

// We only want this observer live just before we navigate away to the collection creation screen
requireComponents.core.tabCollectionStorage.unregister(collectionStorageObserver)

lifecycleScope.launch(IO) {
requireComponents.reviewPromptController.promptReview(requireActivity())
}
}

private fun dispatchModeChanges(mode: Mode) {
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/java/org/mozilla/fenix/utils/Settings.kt
Expand Up @@ -62,6 +62,7 @@ class Settings(private val appContext: Context) : PreferencesHolder {
private const val ALLOWED_INT = 2
private const val CFR_COUNT_CONDITION_FOCUS_INSTALLED = 1
private const val CFR_COUNT_CONDITION_FOCUS_NOT_INSTALLED = 3
private const val MIN_DAYS_SINCE_FEEDBACK_PROMPT = 120

private fun Action.toInt() = when (this) {
Action.BLOCKED -> BLOCKED_INT
Expand Down Expand Up @@ -103,6 +104,16 @@ class Settings(private val appContext: Context) : PreferencesHolder {
featureFlag = FeatureFlags.newSearchExperience
)

var numberOfAppLaunches by intPreference(
appContext.getPreferenceKey(R.string.pref_key_times_app_opened),
default = 0
)

var lastReviewPromptTimeInMillis by longPreference(
appContext.getPreferenceKey(R.string.pref_key_last_review_prompt_shown_time),
default = 0L
)

var waitToShowPageUntilFirstPaint by featureFlagPreference(
appContext.getPreferenceKey(R.string.pref_key_wait_first_paint),
default = false,
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values/preference_keys.xml
Expand Up @@ -57,6 +57,8 @@
<string name="pref_key_open_in_app_opened" translatable="false">pref_key_open_in_app_opened</string>
<string name="pref_key_install_pwa_opened" translatable="false">pref_key_install_pwa_opened</string>
<string name="pref_key_install_pwa_visits" translatable="false">pref_key_install_pwa_visits</string>
<string name="pref_key_times_app_opened" translatable="false">pref_key_times_app_opened</string>
<string name="pref_key_last_review_prompt_shown_time" translatable="false">pref_key_last_review_prompt_shown_time</string>

<!-- Data Choices -->
<string name="pref_key_telemetry" translatable="false">pref_key_telemetry</string>
Expand Down

0 comments on commit 6282b41

Please sign in to comment.