Add in-app reviews for play flavor

Signed-off-by: William Brawner <me@wbrawner.com>
This commit is contained in:
William Brawner 2020-08-28 20:53:42 -07:00
parent 76c45bb914
commit c6728f1afa
4 changed files with 140 additions and 0 deletions

View file

@ -125,6 +125,7 @@ dependencies {
implementation 'com.commonsware.cwac:anddown:0.3.0' implementation 'com.commonsware.cwac:anddown:0.3.0'
playImplementation 'com.android.billingclient:billing:3.0.0' playImplementation 'com.android.billingclient:billing:3.0.0'
playImplementation 'com.google.firebase:firebase-core:17.5.0' playImplementation 'com.google.firebase:firebase-core:17.5.0'
playImplementation 'com.google.android.play:core-ktx:1.8.1'
implementation "androidx.core:core-ktx:1.3.1" implementation "androidx.core:core-ktx:1.3.1"
implementation 'androidx.browser:browser:1.2.0' implementation 'androidx.browser:browser:1.2.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

View file

@ -0,0 +1,15 @@
package com.wbrawner.simplemarkdown.utility
import android.app.Activity
import android.content.Context
import androidx.preference.PreferenceManager
import com.google.android.play.core.review.ReviewManagerFactory
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
object ReviewHelper {
// No review library for F-droid, so this is a no-op
fun init(application: Application, errorHandler: ErrorHandler) {
Log.w("ReviewHelper", "ReviewHelper not enabled for free builds")
}
}

View file

@ -5,6 +5,7 @@ import android.content.Context
import android.os.StrictMode import android.os.StrictMode
import com.wbrawner.simplemarkdown.utility.AcraErrorHandler import com.wbrawner.simplemarkdown.utility.AcraErrorHandler
import com.wbrawner.simplemarkdown.utility.ErrorHandler import com.wbrawner.simplemarkdown.utility.ErrorHandler
import com.wbrawner.simplemarkdown.utility.ReviewHelper
class MarkdownApplication : Application() { class MarkdownApplication : Application() {
val errorHandler: ErrorHandler by lazy { val errorHandler: ErrorHandler by lazy {
@ -23,6 +24,7 @@ class MarkdownApplication : Application() {
.build()) .build())
} }
super.onCreate() super.onCreate()
ReviewHelper.init(this, errorHandler)
} }
override fun attachBaseContext(base: Context?) { override fun attachBaseContext(base: Context?) {

View file

@ -0,0 +1,122 @@
package com.wbrawner.simplemarkdown.utility
import android.app.Activity
import android.app.Application
import android.content.SharedPreferences
import android.os.Bundle
import android.os.SystemClock
import android.util.Log
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import com.google.android.play.core.review.ReviewManager
import com.google.android.play.core.review.ReviewManagerFactory
import com.wbrawner.simplemarkdown.view.activity.MainActivity
private const val KEY_TIME_IN_APP = "timeInApp"
// Prompt user to review after they've used the app for 30 minutes
private const val TIME_TO_PROMPT = 1000L * 60 * 30
/**
* A simple attempt at a non-intrusive way of prompting long-time users of the app to leave a
* review. It works by registering itself as one of the [Application.ActivityLifecycleCallbacks] to
* keep track of the current activity, which is needed to launch the review flow, along with the
* time the activity is started to estimate how long the user has been active.
*/
object ReviewHelper : Application.ActivityLifecycleCallbacks {
private lateinit var application: Application
private lateinit var reviewManager: ReviewManager
private lateinit var sharedPreferences: SharedPreferences
private lateinit var errorHandler: ErrorHandler
private var currentActivity: Activity? = null
private var activityCount = 0
set(value) {
field = if (value < 0) {
0
} else {
value
}
}
private var activeTime = 0L
fun init(
application: Application,
errorHandler: ErrorHandler,
sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(application),
reviewManager: ReviewManager = ReviewManagerFactory.create(application)
) {
this.application = application
this.errorHandler = errorHandler
this.sharedPreferences = sharedPreferences
this.reviewManager = reviewManager
if (sharedPreferences.getLong(KEY_TIME_IN_APP, 0L) == -1L) {
// We've already prompted the user for the review so let's not be annoying about it
Log.i("ReviewHelper", "User already prompted for review, not configuring ReviewHelper")
return
}
application.registerActivityLifecycleCallbacks(this)
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
// No op
}
override fun onActivityStarted(activity: Activity) {
currentActivity = activity
if (activityCount++ == 0) {
activeTime = SystemClock.elapsedRealtime()
}
if (activity !is MainActivity || sharedPreferences.getLong(KEY_TIME_IN_APP, 0L) < TIME_TO_PROMPT) {
// Not ready to prompt just yet
Log.v("ReviewHelper", "Not ready to prompt user for review yet")
return
}
Log.v("ReviewHelper", "Prompting user for review")
reviewManager.requestReviewFlow().addOnCompleteListener { request ->
if (!request.isSuccessful) {
val exception = request.exception
?: RuntimeException("Failed to request review")
Log.e("ReviewHelper", "Failed to prompt user for review", exception)
errorHandler.reportException(exception)
return@addOnCompleteListener
}
reviewManager.launchReviewFlow(activity, request.result).addOnCompleteListener { _ ->
// According to the docs, this may or may not have actually been shown. Either
// way, it's not a critical piece of functionality for the app so I'm not
// worried about it failing silently. Link for reference:
// https://developer.android.com/guide/playcore/in-app-review/kotlin-java#launch-review-flow
Log.v("ReviewHelper", "User finished review, ending activity watch")
application.unregisterActivityLifecycleCallbacks(this)
sharedPreferences.edit {
putLong(KEY_TIME_IN_APP, -1L)
}
}
}
}
override fun onActivityResumed(activity: Activity) {
// No op
}
override fun onActivityPaused(activity: Activity) {
// No op
}
override fun onActivityStopped(activity: Activity) {
currentActivity = null
if (--activityCount == 0) {
sharedPreferences.edit {
putLong(KEY_TIME_IN_APP, SystemClock.elapsedRealtime() - activeTime)
}
}
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
// No op
}
override fun onActivityDestroyed(activity: Activity) {
// No op
}
}