Use In-App Review API

pull/1817/head
Alex Baker 4 years ago
parent ce191d3325
commit 2aee91a04b

@ -237,6 +237,8 @@ dependencies {
googleplayImplementation("com.google.android.gms:play-services-location:19.0.1") googleplayImplementation("com.google.android.gms:play-services-location:19.0.1")
googleplayImplementation("com.google.android.gms:play-services-maps:18.0.2") googleplayImplementation("com.google.android.gms:play-services-maps:18.0.2")
googleplayImplementation("com.android.billingclient:billing-ktx:3.0.3") 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}") androidTestImplementation("com.google.dagger:hilt-android-testing:${Versions.hilt}")
kaptAndroidTest("com.google.dagger:hilt-compiler:${Versions.hilt}") kaptAndroidTest("com.google.dagger:hilt-compiler:${Versions.hilt}")

@ -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) {}
}

@ -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
}

@ -1,31 +1,36 @@
package org.tasks.injection package org.tasks.injection
import android.content.Context
import dagger.Lazy import dagger.Lazy
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import org.tasks.gtasks.PlayServices import org.tasks.location.Geocoder
import org.tasks.location.* 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 @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
class FlavorModule { class FlavorModule {
@Provides @Provides
fun getLocationService( fun getLocationService(
@ApplicationContext context: Context,
google: Lazy<LocationServiceGooglePlay>, google: Lazy<LocationServiceGooglePlay>,
android: Lazy<LocationServiceAndroid> android: Lazy<LocationServiceAndroid>,
): LocationService = if (PlayServices.isAvailable(context)) google.get() else android.get() playServices: PlayServices,
): LocationService = if (playServices.isAvailable()) google.get() else android.get()
@Provides @Provides
fun getMapFragment( fun getMapFragment(
@ApplicationContext context: Context,
osm: Lazy<OsmMapFragment>, osm: Lazy<OsmMapFragment>,
google: Lazy<GoogleMapFragment>, google: Lazy<GoogleMapFragment>,
): MapFragment = if (PlayServices.isAvailable(context)) google.get() else osm.get() playServices: PlayServices,
): MapFragment = if (playServices.isAvailable()) google.get() else osm.get()
@Provides @Provides
fun getGeocoder(mapbox: GeocoderMapbox): Geocoder = mapbox fun getGeocoder(mapbox: GeocoderMapbox): Geocoder = mapbox

@ -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)
}
}

@ -48,6 +48,7 @@ import org.tasks.fragments.CommentBarFragment.CommentBarFragmentCallback
import org.tasks.injection.InjectingAppCompatActivity import org.tasks.injection.InjectingAppCompatActivity
import org.tasks.intents.TaskIntents.getTaskListIntent import org.tasks.intents.TaskIntents.getTaskListIntent
import org.tasks.location.LocationPickerActivity import org.tasks.location.LocationPickerActivity
import org.tasks.play.PlayServices
import org.tasks.preferences.DefaultFilterProvider import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.themes.ColorProvider import org.tasks.themes.ColorProvider
@ -56,8 +57,8 @@ import org.tasks.themes.ThemeColor
import org.tasks.ui.DeadlineControlSet.DueDateChangeListener import org.tasks.ui.DeadlineControlSet.DueDateChangeListener
import org.tasks.ui.EmptyTaskEditFragment.Companion.newEmptyTaskEditFragment import org.tasks.ui.EmptyTaskEditFragment.Companion.newEmptyTaskEditFragment
import org.tasks.ui.ListFragment.OnListChanged import org.tasks.ui.ListFragment.OnListChanged
import org.tasks.ui.MainActivityEventBus
import org.tasks.ui.MainActivityEvent import org.tasks.ui.MainActivityEvent
import org.tasks.ui.MainActivityEventBus
import org.tasks.ui.NavigationDrawerFragment import org.tasks.ui.NavigationDrawerFragment
import org.tasks.ui.NavigationDrawerFragment.Companion.newNavigationDrawer import org.tasks.ui.NavigationDrawerFragment.Companion.newNavigationDrawer
import timber.log.Timber import timber.log.Timber
@ -77,6 +78,7 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl
@Inject lateinit var tagDataDao: TagDataDao @Inject lateinit var tagDataDao: TagDataDao
@Inject lateinit var alarmDao: AlarmDao @Inject lateinit var alarmDao: AlarmDao
@Inject lateinit var eventBus: MainActivityEventBus @Inject lateinit var eventBus: MainActivityEventBus
@Inject lateinit var playServices: PlayServices
private var currentNightMode = 0 private var currentNightMode = 0
private var currentPro = false private var currentPro = false
@ -109,6 +111,8 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl
private suspend fun process(event: MainActivityEvent) = when (event) { private suspend fun process(event: MainActivityEvent) = when (event) {
is MainActivityEvent.OpenTask -> is MainActivityEvent.OpenTask ->
onTaskListItemClicked(event.task) onTaskListItemClicked(event.task)
is MainActivityEvent.RequestRating ->
playServices.requestReview(this)
} }
public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

@ -16,21 +16,17 @@ import org.tasks.billing.PurchaseActivity
import org.tasks.databinding.DialogWhatsNewBinding import org.tasks.databinding.DialogWhatsNewBinding
import org.tasks.extensions.Context.openUri import org.tasks.extensions.Context.openUri
import org.tasks.markdown.MarkdownProvider import org.tasks.markdown.MarkdownProvider
import org.tasks.preferences.Preferences
import java.io.BufferedReader import java.io.BufferedReader
import javax.inject.Inject import javax.inject.Inject
import kotlin.random.Random
@AndroidEntryPoint @AndroidEntryPoint
class WhatsNewDialog : DialogFragment() { class WhatsNewDialog : DialogFragment() {
@Inject lateinit var dialogBuilder: DialogBuilder @Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var firebase: Firebase @Inject lateinit var firebase: Firebase
@Inject lateinit var preferences: Preferences
@Inject lateinit var inventory: Inventory @Inject lateinit var inventory: Inventory
@Inject lateinit var markdownProvider: MarkdownProvider @Inject lateinit var markdownProvider: MarkdownProvider
private var displayedRate = false
private var displayedSubscribe = false private var displayedSubscribe = false
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@ -43,10 +39,6 @@ class WhatsNewDialog : DialogFragment() {
.markdown(linkify = true, force = true) .markdown(linkify = true, force = true)
.setMarkdown(binding.changelog, text) .setMarkdown(binding.changelog, text)
val begForSubscription = !inventory.hasPro
val begForRating = !preferences.getBoolean(R.string.p_clicked_rate, false)
&& (!begForSubscription || Random.nextBoolean())
when { when {
IS_GENERIC -> { IS_GENERIC -> {
binding.actionQuestion.setText(R.string.enjoying_tasks) 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.text = getString(R.string.TLA_menu_donate)
binding.actionButton.setOnClickListener { onDonateClick() } binding.actionButton.setOnClickListener { onDonateClick() }
} }
begForRating -> { !inventory.hasPro -> {
displayedRate = true
binding.actionQuestion.setText(R.string.enjoying_tasks)
binding.actionButton.setText(R.string.rate_tasks)
binding.actionButton.setOnClickListener { onRateClick() }
}
begForSubscription -> {
displayedSubscribe = true displayedSubscribe = true
binding.actionQuestion.setText(R.string.tasks_needs_your_support) binding.actionQuestion.setText(R.string.tasks_needs_your_support)
binding.actionText.setText(R.string.support_development_subscribe) binding.actionText.setText(R.string.support_development_subscribe)
@ -95,13 +81,6 @@ class WhatsNewDialog : DialogFragment() {
startActivity(Intent(context, PurchaseActivity::class.java)) 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() { private fun onDonateClick() {
dismiss() dismiss()
context?.openUri(R.string.url_donate) context?.openUri(R.string.url_donate)
@ -116,7 +95,6 @@ class WhatsNewDialog : DialogFragment() {
firebase.logEvent( firebase.logEvent(
R.string.event_whats_new, R.string.event_whats_new,
Pair(R.string.param_click, click), 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_whats_new_display_subscribe, displayedSubscribe),
Pair(R.string.param_user_pro, inventory.hasPro), Pair(R.string.param_user_pro, inventory.hasPro),
) )

@ -532,6 +532,10 @@ class Preferences @JvmOverloads constructor(
val isTopAppBar: Boolean val isTopAppBar: Boolean
get() = getIntegerFromString(R.string.p_app_bar_position, 1) == 0 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 { companion object {
private const val PREF_SORT_SORT = "sort_sort" // $NON-NLS-1$ private const val PREF_SORT_SORT = "sort_sort" // $NON-NLS-1$

@ -7,5 +7,6 @@ typealias MainActivityEventBus = MutableSharedFlow<MainActivityEvent>
sealed interface MainActivityEvent { sealed interface MainActivityEvent {
data class OpenTask(val task: Task) : MainActivityEvent data class OpenTask(val task: Task) : MainActivityEvent
object RequestRating : MainActivityEvent
} }

@ -82,6 +82,7 @@ class TaskEditViewModel @Inject constructor(
private val taskCompleter: TaskCompleter, private val taskCompleter: TaskCompleter,
private val alarmService: AlarmService, private val alarmService: AlarmService,
private val taskListEvents: TaskListEventBus, private val taskListEvents: TaskListEventBus,
private val mainActivityEvents: MainActivityEventBus,
) : ViewModel() { ) : ViewModel() {
val cleared = MutableLiveData<Event<Boolean>>() val cleared = MutableLiveData<Event<Boolean>>()
@ -436,6 +437,7 @@ class TaskEditViewModel @Inject constructor(
model.calendarURI?.takeIf { it.isNotBlank() }?.let { model.calendarURI?.takeIf { it.isNotBlank() }?.let {
taskListEvents.emit(TaskListEvent.CalendarEventCreated(model.title, it)) taskListEvents.emit(TaskListEvent.CalendarEventCreated(model.title, it))
} }
mainActivityEvents.emit(MainActivityEvent.RequestRating)
} }
} }

@ -410,7 +410,7 @@
<string name="p_hide_check_button">Hide check button</string> <string name="p_hide_check_button">Hide check button</string>
<string name="p_show_whats_new">show_whats_new</string> <string name="p_show_whats_new">show_whats_new</string>
<string name="p_just_updated">just_updated</string> <string name="p_just_updated">just_updated</string>
<string name="p_clicked_rate">clicked_rate</string> <string name="p_last_review_request">last_review_request</string>
<string name="p_backups_enabled">backups_enabled</string> <string name="p_backups_enabled">backups_enabled</string>
<string name="p_backups_ignore_warnings">backups_ignore_warnings</string> <string name="p_backups_ignore_warnings">backups_ignore_warnings</string>
<string name="p_backups_android_backup_enabled">backups_android_backup_enabled</string> <string name="p_backups_android_backup_enabled">backups_android_backup_enabled</string>

@ -220,6 +220,13 @@
+| +--- com.android.billingclient:billing:3.0.3 +| +--- com.android.billingclient:billing:3.0.3
+| +--- org.jetbrains.kotlin:kotlin-stdlib:1.4.0 -> 1.6.10 (*) +| +--- org.jetbrains.kotlin:kotlin-stdlib:1.4.0 -> 1.6.10 (*)
+| \--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9 -> 1.5.2 (*) +| \--- 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 ++--- com.gitlab.abaker:dav4jvm:deb2c9aef8
+| +--- org.jetbrains.kotlin:kotlin-stdlib:1.4.10 -> 1.6.10 (*) +| +--- org.jetbrains.kotlin:kotlin-stdlib:1.4.10 -> 1.6.10 (*)
+| \--- org.apache.commons:commons-lang3:3.8.1 +| \--- org.apache.commons:commons-lang3:3.8.1

Loading…
Cancel
Save