From 2aee91a04b50e8cd18085f8efdf89e572ced504a Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Sun, 13 Mar 2022 13:11:35 -0500 Subject: [PATCH] Use In-App Review API --- app/build.gradle.kts | 2 + .../java/org/tasks/play/PlayServices.kt | 8 ++++ .../java/org/tasks/gtasks/PlayServices.kt | 12 ----- .../java/org/tasks/injection/FlavorModule.kt | 29 ++++++----- .../java/org/tasks/play/PlayServices.kt | 48 +++++++++++++++++++ .../todoroo/astrid/activity/MainActivity.kt | 6 ++- .../java/org/tasks/dialogs/WhatsNewDialog.kt | 24 +--------- .../java/org/tasks/preferences/Preferences.kt | 4 ++ .../java/org/tasks/ui/MainActivityEvent.kt | 1 + .../java/org/tasks/ui/TaskEditViewModel.kt | 2 + app/src/main/res/values/keys.xml | 2 +- deps_googleplay.txt | 7 +++ 12 files changed, 96 insertions(+), 49 deletions(-) create mode 100644 app/src/generic/java/org/tasks/play/PlayServices.kt delete mode 100644 app/src/googleplay/java/org/tasks/gtasks/PlayServices.kt create mode 100644 app/src/googleplay/java/org/tasks/play/PlayServices.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 37bb3fde0..33526b5a2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -237,6 +237,8 @@ dependencies { googleplayImplementation("com.google.android.gms:play-services-location:19.0.1") googleplayImplementation("com.google.android.gms:play-services-maps:18.0.2") googleplayImplementation("com.android.billingclient:billing-ktx:3.0.3") + googleplayImplementation("com.google.android.play:core:1.10.3") + googleplayImplementation("com.google.android.play:core-ktx:1.8.1") androidTestImplementation("com.google.dagger:hilt-android-testing:${Versions.hilt}") kaptAndroidTest("com.google.dagger:hilt-compiler:${Versions.hilt}") diff --git a/app/src/generic/java/org/tasks/play/PlayServices.kt b/app/src/generic/java/org/tasks/play/PlayServices.kt new file mode 100644 index 000000000..27b7d3b18 --- /dev/null +++ b/app/src/generic/java/org/tasks/play/PlayServices.kt @@ -0,0 +1,8 @@ +package org.tasks.play + +import android.app.Activity +import javax.inject.Inject + +class PlayServices @Inject constructor() { + fun requestReview(@Suppress("UNUSED_PARAMETER") activity: Activity) {} +} \ No newline at end of file diff --git a/app/src/googleplay/java/org/tasks/gtasks/PlayServices.kt b/app/src/googleplay/java/org/tasks/gtasks/PlayServices.kt deleted file mode 100644 index 8ed500012..000000000 --- a/app/src/googleplay/java/org/tasks/gtasks/PlayServices.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.tasks.gtasks - -import android.content.Context -import com.google.android.gms.common.ConnectionResult -import com.google.android.gms.common.GoogleApiAvailability.getInstance -import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject - -object PlayServices { - fun isAvailable(context: Context) = - getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS -} \ No newline at end of file diff --git a/app/src/googleplay/java/org/tasks/injection/FlavorModule.kt b/app/src/googleplay/java/org/tasks/injection/FlavorModule.kt index 207d2f9a9..61c4f4ce0 100644 --- a/app/src/googleplay/java/org/tasks/injection/FlavorModule.kt +++ b/app/src/googleplay/java/org/tasks/injection/FlavorModule.kt @@ -1,31 +1,36 @@ package org.tasks.injection -import android.content.Context import dagger.Lazy import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import org.tasks.gtasks.PlayServices -import org.tasks.location.* +import org.tasks.location.Geocoder +import org.tasks.location.GeocoderMapbox +import org.tasks.location.GoogleMapFragment +import org.tasks.location.LocationService +import org.tasks.location.LocationServiceAndroid +import org.tasks.location.LocationServiceGooglePlay +import org.tasks.location.MapFragment +import org.tasks.location.OsmMapFragment +import org.tasks.play.PlayServices @Module @InstallIn(SingletonComponent::class) class FlavorModule { @Provides fun getLocationService( - @ApplicationContext context: Context, - google: Lazy, - android: Lazy - ): LocationService = if (PlayServices.isAvailable(context)) google.get() else android.get() + google: Lazy, + android: Lazy, + playServices: PlayServices, + ): LocationService = if (playServices.isAvailable()) google.get() else android.get() @Provides fun getMapFragment( - @ApplicationContext context: Context, - osm: Lazy, - google: Lazy, - ): MapFragment = if (PlayServices.isAvailable(context)) google.get() else osm.get() + osm: Lazy, + google: Lazy, + playServices: PlayServices, + ): MapFragment = if (playServices.isAvailable()) google.get() else osm.get() @Provides fun getGeocoder(mapbox: GeocoderMapbox): Geocoder = mapbox diff --git a/app/src/googleplay/java/org/tasks/play/PlayServices.kt b/app/src/googleplay/java/org/tasks/play/PlayServices.kt new file mode 100644 index 000000000..6f5680479 --- /dev/null +++ b/app/src/googleplay/java/org/tasks/play/PlayServices.kt @@ -0,0 +1,48 @@ +package org.tasks.play + +import android.app.Activity +import android.content.Context +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability.getInstance +import com.google.android.play.core.ktx.launchReview +import com.google.android.play.core.ktx.requestReview +import com.google.android.play.core.review.ReviewManagerFactory +import com.todoroo.andlib.utility.DateUtilities.now +import dagger.hilt.android.qualifiers.ApplicationContext +import org.tasks.preferences.Preferences +import org.tasks.time.DateTimeUtils.printTimestamp +import timber.log.Timber +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class PlayServices @Inject constructor( + @ApplicationContext private val context: Context, + private val preferences: Preferences, +) { + fun isAvailable() = + getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS + + suspend fun requestReview(activity: Activity) { + val now = now() + val installCutoff = preferences.installDate + INSTALL_COOLDOWN + val reviewCutoff = preferences.lastReviewRequest + REVIEW_COOLDOWN + if (installCutoff > now || reviewCutoff > now) { + Timber.d("wait for review request: install=${printTimestamp(installCutoff)} review=${printTimestamp(reviewCutoff)}") + return + } + try { + with(ReviewManagerFactory.create(context)) { + val request = requestReview() + launchReview(activity, request) + preferences.lastReviewRequest = now + } + } catch (e: Exception) { + Timber.e(e) + } + } + + companion object { + private val INSTALL_COOLDOWN = TimeUnit.DAYS.toMillis(14) + private val REVIEW_COOLDOWN = TimeUnit.DAYS.toMillis(30) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt b/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt index 97a0babe1..b27ecc8f2 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt @@ -48,6 +48,7 @@ import org.tasks.fragments.CommentBarFragment.CommentBarFragmentCallback import org.tasks.injection.InjectingAppCompatActivity import org.tasks.intents.TaskIntents.getTaskListIntent import org.tasks.location.LocationPickerActivity +import org.tasks.play.PlayServices import org.tasks.preferences.DefaultFilterProvider import org.tasks.preferences.Preferences import org.tasks.themes.ColorProvider @@ -56,8 +57,8 @@ import org.tasks.themes.ThemeColor import org.tasks.ui.DeadlineControlSet.DueDateChangeListener import org.tasks.ui.EmptyTaskEditFragment.Companion.newEmptyTaskEditFragment import org.tasks.ui.ListFragment.OnListChanged -import org.tasks.ui.MainActivityEventBus import org.tasks.ui.MainActivityEvent +import org.tasks.ui.MainActivityEventBus import org.tasks.ui.NavigationDrawerFragment import org.tasks.ui.NavigationDrawerFragment.Companion.newNavigationDrawer import timber.log.Timber @@ -77,6 +78,7 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl @Inject lateinit var tagDataDao: TagDataDao @Inject lateinit var alarmDao: AlarmDao @Inject lateinit var eventBus: MainActivityEventBus + @Inject lateinit var playServices: PlayServices private var currentNightMode = 0 private var currentPro = false @@ -109,6 +111,8 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl private suspend fun process(event: MainActivityEvent) = when (event) { is MainActivityEvent.OpenTask -> onTaskListItemClicked(event.task) + is MainActivityEvent.RequestRating -> + playServices.requestReview(this) } public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { diff --git a/app/src/main/java/org/tasks/dialogs/WhatsNewDialog.kt b/app/src/main/java/org/tasks/dialogs/WhatsNewDialog.kt index 71cc0f476..c72464fca 100644 --- a/app/src/main/java/org/tasks/dialogs/WhatsNewDialog.kt +++ b/app/src/main/java/org/tasks/dialogs/WhatsNewDialog.kt @@ -16,21 +16,17 @@ import org.tasks.billing.PurchaseActivity import org.tasks.databinding.DialogWhatsNewBinding import org.tasks.extensions.Context.openUri import org.tasks.markdown.MarkdownProvider -import org.tasks.preferences.Preferences import java.io.BufferedReader import javax.inject.Inject -import kotlin.random.Random @AndroidEntryPoint class WhatsNewDialog : DialogFragment() { @Inject lateinit var dialogBuilder: DialogBuilder @Inject lateinit var firebase: Firebase - @Inject lateinit var preferences: Preferences @Inject lateinit var inventory: Inventory @Inject lateinit var markdownProvider: MarkdownProvider - private var displayedRate = false private var displayedSubscribe = false override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { @@ -43,10 +39,6 @@ class WhatsNewDialog : DialogFragment() { .markdown(linkify = true, force = true) .setMarkdown(binding.changelog, text) - val begForSubscription = !inventory.hasPro - val begForRating = !preferences.getBoolean(R.string.p_clicked_rate, false) - && (!begForSubscription || Random.nextBoolean()) - when { IS_GENERIC -> { binding.actionQuestion.setText(R.string.enjoying_tasks) @@ -54,13 +46,7 @@ class WhatsNewDialog : DialogFragment() { binding.actionButton.text = getString(R.string.TLA_menu_donate) binding.actionButton.setOnClickListener { onDonateClick() } } - begForRating -> { - displayedRate = true - binding.actionQuestion.setText(R.string.enjoying_tasks) - binding.actionButton.setText(R.string.rate_tasks) - binding.actionButton.setOnClickListener { onRateClick() } - } - begForSubscription -> { + !inventory.hasPro -> { displayedSubscribe = true binding.actionQuestion.setText(R.string.tasks_needs_your_support) binding.actionText.setText(R.string.support_development_subscribe) @@ -95,13 +81,6 @@ class WhatsNewDialog : DialogFragment() { startActivity(Intent(context, PurchaseActivity::class.java)) } - private fun onRateClick() { - logClick(true) - preferences.setBoolean(R.string.p_clicked_rate, true) - dismiss() - context?.openUri(R.string.market_url) - } - private fun onDonateClick() { dismiss() context?.openUri(R.string.url_donate) @@ -116,7 +95,6 @@ class WhatsNewDialog : DialogFragment() { firebase.logEvent( R.string.event_whats_new, Pair(R.string.param_click, click), - Pair(R.string.param_whats_new_display_rate, displayedRate), Pair(R.string.param_whats_new_display_subscribe, displayedSubscribe), Pair(R.string.param_user_pro, inventory.hasPro), ) diff --git a/app/src/main/java/org/tasks/preferences/Preferences.kt b/app/src/main/java/org/tasks/preferences/Preferences.kt index 48c05d517..29f30cdd5 100644 --- a/app/src/main/java/org/tasks/preferences/Preferences.kt +++ b/app/src/main/java/org/tasks/preferences/Preferences.kt @@ -532,6 +532,10 @@ class Preferences @JvmOverloads constructor( val isTopAppBar: Boolean get() = getIntegerFromString(R.string.p_app_bar_position, 1) == 0 + var lastReviewRequest: Long + get() = getLong(R.string.p_last_review_request, 0L) + set(value) = setLong(R.string.p_last_review_request, value) + companion object { private const val PREF_SORT_SORT = "sort_sort" // $NON-NLS-1$ diff --git a/app/src/main/java/org/tasks/ui/MainActivityEvent.kt b/app/src/main/java/org/tasks/ui/MainActivityEvent.kt index 0d20ee83f..91a7aff20 100644 --- a/app/src/main/java/org/tasks/ui/MainActivityEvent.kt +++ b/app/src/main/java/org/tasks/ui/MainActivityEvent.kt @@ -7,5 +7,6 @@ typealias MainActivityEventBus = MutableSharedFlow sealed interface MainActivityEvent { data class OpenTask(val task: Task) : MainActivityEvent + object RequestRating : MainActivityEvent } diff --git a/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt b/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt index 64f355204..9d80fe3bc 100644 --- a/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt +++ b/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt @@ -82,6 +82,7 @@ class TaskEditViewModel @Inject constructor( private val taskCompleter: TaskCompleter, private val alarmService: AlarmService, private val taskListEvents: TaskListEventBus, + private val mainActivityEvents: MainActivityEventBus, ) : ViewModel() { val cleared = MutableLiveData>() @@ -436,6 +437,7 @@ class TaskEditViewModel @Inject constructor( model.calendarURI?.takeIf { it.isNotBlank() }?.let { taskListEvents.emit(TaskListEvent.CalendarEventCreated(model.title, it)) } + mainActivityEvents.emit(MainActivityEvent.RequestRating) } } diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml index 473a2a514..e19199c0c 100644 --- a/app/src/main/res/values/keys.xml +++ b/app/src/main/res/values/keys.xml @@ -410,7 +410,7 @@ Hide check button show_whats_new just_updated - clicked_rate + last_review_request backups_enabled backups_ignore_warnings backups_android_backup_enabled diff --git a/deps_googleplay.txt b/deps_googleplay.txt index 93eeef37c..1768a5c76 100644 --- a/deps_googleplay.txt +++ b/deps_googleplay.txt @@ -220,6 +220,13 @@ +| +--- com.android.billingclient:billing:3.0.3 +| +--- org.jetbrains.kotlin:kotlin-stdlib:1.4.0 -> 1.6.10 (*) +| \--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9 -> 1.5.2 (*) +++--- com.google.android.play:core:1.10.3 +++--- com.google.android.play:core-ktx:1.8.1 ++| +--- com.google.android.play:core:1.8.0 -> 1.10.3 ++| +--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.72 -> 1.6.10 (*) ++| +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6 -> 1.5.2 (*) ++| +--- androidx.core:core:1.1.0 -> 1.7.0 (*) ++| \--- androidx.fragment:fragment:1.1.0 -> 1.4.0 (*) ++--- com.gitlab.abaker:dav4jvm:deb2c9aef8 +| +--- org.jetbrains.kotlin:kotlin-stdlib:1.4.10 -> 1.6.10 (*) +| \--- org.apache.commons:commons-lang3:3.8.1