diff --git a/app/build.gradle b/app/build.gradle index 52b0f4a..118bfd2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -125,6 +125,7 @@ dependencies { implementation 'com.commonsware.cwac:anddown:0.3.0' playImplementation 'com.android.billingclient:billing:3.0.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.browser:browser:1.2.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" diff --git a/app/src/free/java/com/wbrawner/simplemarkdown/utility/ReviewHelper.kt b/app/src/free/java/com/wbrawner/simplemarkdown/utility/ReviewHelper.kt new file mode 100644 index 0000000..32bf984 --- /dev/null +++ b/app/src/free/java/com/wbrawner/simplemarkdown/utility/ReviewHelper.kt @@ -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") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wbrawner/simplemarkdown/MarkdownApplication.kt b/app/src/main/java/com/wbrawner/simplemarkdown/MarkdownApplication.kt index e4060b1..bd4e15b 100644 --- a/app/src/main/java/com/wbrawner/simplemarkdown/MarkdownApplication.kt +++ b/app/src/main/java/com/wbrawner/simplemarkdown/MarkdownApplication.kt @@ -5,6 +5,7 @@ import android.content.Context import android.os.StrictMode import com.wbrawner.simplemarkdown.utility.AcraErrorHandler import com.wbrawner.simplemarkdown.utility.ErrorHandler +import com.wbrawner.simplemarkdown.utility.ReviewHelper class MarkdownApplication : Application() { val errorHandler: ErrorHandler by lazy { @@ -23,6 +24,7 @@ class MarkdownApplication : Application() { .build()) } super.onCreate() + ReviewHelper.init(this, errorHandler) } override fun attachBaseContext(base: Context?) { diff --git a/app/src/play/java/com/wbrawner/simplemarkdown/utility/ReviewHelper.kt b/app/src/play/java/com/wbrawner/simplemarkdown/utility/ReviewHelper.kt new file mode 100644 index 0000000..cb99524 --- /dev/null +++ b/app/src/play/java/com/wbrawner/simplemarkdown/utility/ReviewHelper.kt @@ -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 + } +} \ No newline at end of file