Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package app.opendocument.droid.background

import android.content.Context
import android.content.SharedPreferences

/**
* How often the user opened the app from the launcher, and how many documents they opened.
*
* A counter rather than the length of the recently opened list: that list is capped, pruned and
* deletable, so it undercounts exactly the returning users this is meant to find.
*/
object UsageCounters {

private const val KEY_APP_OPENS = "usage_app_opens"
private const val KEY_DOCUMENT_OPENS = "usage_document_opens"

fun recordAppOpen(context: Context): Int = increment(context, KEY_APP_OPENS)

fun recordDocumentOpen(context: Context): Int = increment(context, KEY_DOCUMENT_OPENS)

fun appOpens(context: Context): Int = preferences(context).getInt(KEY_APP_OPENS, 0)

fun documentOpens(context: Context): Int = preferences(context).getInt(KEY_DOCUMENT_OPENS, 0)

private fun increment(context: Context, key: String): Int {
val preferences = preferences(context)
val next = preferences.getInt(key, 0) + 1

// apply, not commit: nothing reads this back synchronously
preferences.edit().putInt(key, next).apply()

return next
}

/** The same default preference file [CatchAllSetting] uses. */
private fun preferences(context: Context): SharedPreferences =
context.getSharedPreferences(context.packageName + "_preferences", Context.MODE_PRIVATE)
}
47 changes: 47 additions & 0 deletions app/src/main/java/app/opendocument/droid/nonfree/InAppReview.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package app.opendocument.droid.nonfree

import android.app.Activity
import com.google.android.play.core.review.ReviewManagerFactory

/**
* The play in-app review sheet.
*
* Play decides from an undocumented per-user quota whether the sheet appears at all, and the
* completion listener fires the same way either way. The analytics events below are the only
* visibility there is.
*/
object InAppReview {

/**
* Opens before the first ask - a fresh install has nothing to say yet, and asking costs stars.
*/
const val MINIMUM_OPENS: Int = 3

fun requestIfEarned(activity: Activity, analyticsManager: AnalyticsManager, opens: Int) {
if (opens < MINIMUM_OPENS) {
return
}

request(activity, analyticsManager)
}

fun request(activity: Activity, analyticsManager: AnalyticsManager) {
analyticsManager.report("in_app_review_eligible")

val manager = ReviewManagerFactory.create(activity)
manager.requestReviewFlow().addOnCompleteListener { reviewInfoTask ->
if (!reviewInfoTask.isSuccessful) {
// usually an install that did not come from play, so there is no store to ask
analyticsManager.report("in_app_review_error")

return@addOnCompleteListener
}

analyticsManager.report("in_app_review_start")

manager.launchReviewFlow(activity, reviewInfoTask.result).addOnCompleteListener {
analyticsManager.report("in_app_review_done")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,16 @@ import app.opendocument.droid.background.FileLoader
import app.opendocument.droid.background.LoaderService
import app.opendocument.droid.background.LoaderServiceQueue
import app.opendocument.droid.background.StreamUtil
import app.opendocument.droid.background.UsageCounters
import app.opendocument.droid.nonfree.AnalyticsConstants
import app.opendocument.droid.nonfree.AnalyticsManager
import app.opendocument.droid.nonfree.CrashManager
import app.opendocument.droid.nonfree.InAppReview
import app.opendocument.droid.ui.OpenFileIdling
import app.opendocument.droid.ui.SnackbarHelper
import app.opendocument.droid.ui.widget.PageView
import app.opendocument.droid.ui.widget.ProgressDialogFragment
import com.google.android.material.tabs.TabLayout
import com.google.android.play.core.review.ReviewManagerFactory
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
Expand All @@ -61,6 +62,9 @@ class DocumentFragment : Fragment(), LoaderService.LoaderListener, MenuProvider
private var resultOnStart: FileLoader.Result? = null
private var errorOnStart: Throwable? = null

/** Set by [loadUri], consumed by the load it belongs to. See [loadUri]. */
private var freshOpenPending = false

private lateinit var tabLayout: TabLayout

private lateinit var serviceQueue: LoaderServiceQueue
Expand Down Expand Up @@ -303,9 +307,19 @@ class DocumentFragment : Fragment(), LoaderService.LoaderListener, MenuProvider
serviceQueue.addToQueue { service -> service.loadWithType(loaderType, options) }
}

fun loadUri(uri: Uri, persistentUri: Boolean, editable: Boolean = false) {
/**
* [freshOpen] is false for a load the user did not ask for, which then never asks for a review.
*/
fun loadUri(
uri: Uri,
persistentUri: Boolean,
editable: Boolean = false,
freshOpen: Boolean = true,
) {
initializePageView()

freshOpenPending = freshOpen

state.lastRequestedUri = uri

val options = FileLoader.Options()
Expand All @@ -320,6 +334,9 @@ class DocumentFragment : Fragment(), LoaderService.LoaderListener, MenuProvider
val lastResult = checkNotNull(state.lastResult) { "nothing was loaded yet" }
lastResult.options.translatable = translatable

// entering or leaving edit mode is not a new document, and the user is working
freshOpenPending = false

loadWithType(lastResult.loaderType, lastResult.options)
}

Expand Down Expand Up @@ -411,25 +428,6 @@ class DocumentFragment : Fragment(), LoaderService.LoaderListener, MenuProvider
pageView?.toggleDarkMode(fileType?.startsWith("application/pdf") != true)
}

private fun requestInAppRating(activity: Activity) {
analyticsManager.report("in_app_review_eligible")

val manager = ReviewManagerFactory.create(activity)
manager.requestReviewFlow().addOnCompleteListener { reviewInfoTask ->
if (!reviewInfoTask.isSuccessful) {
analyticsManager.report("in_app_review_error")

return@addOnCompleteListener
}

analyticsManager.report("in_app_review_start")

manager.launchReviewFlow(activity, reviewInfoTask.result).addOnCompleteListener {
analyticsManager.report("in_app_review_done")
}
}
}

private fun isActivityReadyForResult(result: FileLoader.Result): Boolean {
val lastRequestedUri = state.lastRequestedUri
if (lastRequestedUri != null && lastRequestedUri != result.options.originalUri) {
Expand Down Expand Up @@ -489,15 +487,17 @@ class DocumentFragment : Fragment(), LoaderService.LoaderListener, MenuProvider

state.endLoadIdling()

// asked for in both flavors, and deliberately not behind a flag. lite used to
// consult a "show_in_app_rating" remote config key, which has returned false since
// firebase remote config was gutted in v4.2 - the call site was never touched, so
// nothing looked broken while the flavor carrying almost every user silently
// stopped asking. play decides whether the sheet actually appears (undocumented
// per-user quota) and reports nothing back either way, so there is nothing here
// worth gating: DISABLE_TRACKING means crash and analytics reporting, which this
// is not.
requestInAppRating(activity)
// only a fresh open earns the ask - reloadUri and the webview reach here mid-task.
// a save reloads through loadUri and so still counts, which is wanted
if (freshOpenPending) {
freshOpenPending = false

InAppReview.requestIfEarned(
activity,
analyticsManager,
UsageCounters.recordDocumentOpen(activity),
)
}
}

override fun onError(result: FileLoader.Result, error: Throwable) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,13 @@ import app.opendocument.droid.background.LoaderService
import app.opendocument.droid.background.LoaderServiceQueue
import app.opendocument.droid.background.PersistedUriPermissions
import app.opendocument.droid.background.PrintingManager
import app.opendocument.droid.background.UsageCounters
import app.opendocument.droid.nonfree.AdManager
import app.opendocument.droid.nonfree.AnalyticsConstants
import app.opendocument.droid.nonfree.AnalyticsManager
import app.opendocument.droid.nonfree.BillingManager
import app.opendocument.droid.nonfree.CrashManager
import app.opendocument.droid.nonfree.InAppReview
import app.opendocument.droid.ui.EditActionModeCallback
import app.opendocument.droid.ui.FindActionModeCallback
import app.opendocument.droid.ui.OpenFileIdling
Expand Down Expand Up @@ -109,6 +111,10 @@ class MainActivity : AppCompatActivity(), MenuProvider {
// landing screen instead of closing the app
private var documentOpenedExternally = false

// set before we start an activity of our own, so coming back from it is not counted as
// the user opening the app
private var leftForOwnActivity = false

lateinit var loaderServiceQueue: LoaderServiceQueue
private set

Expand Down Expand Up @@ -270,6 +276,20 @@ class MainActivity : AppCompatActivity(), MenuProvider {

crashManager.log("onStart")

// here rather than in onCreate: tapping the launcher while the task is still alive
// resumes this activity instead of creating it, and those opens count too
if (documentFragment == null && loadOnStart == null) {
if (leftForOwnActivity) {
leftForOwnActivity = false
} else {
InAppReview.requestIfEarned(
this,
analyticsManager,
UsageCounters.recordAppOpen(this),
)
}
}

val loadOnStart = this.loadOnStart ?: return

// loadOnStart either came from an external intent or from a restored
Expand Down Expand Up @@ -326,6 +346,7 @@ class MainActivity : AppCompatActivity(), MenuProvider {

intent.type = documentFragment.lastFileType

leftForOwnActivity = true
createDocumentLauncher.launch(intent)
} catch (e: ActivityNotFoundException) {
// happens on a variety devices, e.g. Samsung Galaxy Tab4 7.0 with Android 4.4.2
Expand Down Expand Up @@ -690,6 +711,7 @@ class MainActivity : AppCompatActivity(), MenuProvider {
try {
OpenFileIdling.increment()

leftForOwnActivity = true
openDocumentLauncher.launch(intent)
} catch (e: Exception) {
OpenFileIdling.decrement()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,12 @@ constructor(context: Context, attributeSet: AttributeSet?) :
}

post {
documentFragment.loadUri(AndroidFileCache.getCacheFileUri(context, tmpFile), false)
// the user is mid-read, not opening something
documentFragment.loadUri(
AndroidFileCache.getCacheFileUri(context, tmpFile),
false,
freshOpen = false,
)
}
} catch (e: IOException) {
crashManager.log(e)
Expand Down
Loading