diff --git a/app/src/googleplay/java/org/tasks/analytics/Firebase.kt b/app/src/googleplay/java/org/tasks/analytics/Firebase.kt
index 3d69de119..04627f1e1 100644
--- a/app/src/googleplay/java/org/tasks/analytics/Firebase.kt
+++ b/app/src/googleplay/java/org/tasks/analytics/Firebase.kt
@@ -66,6 +66,9 @@ class Firebase @Inject constructor(
val reviewCooldown: Boolean
get() = preferences.lastReviewRequest + days("review_cooldown", 30L) > now()
+ val subscribeCooldown: Boolean
+ get() = preferences.lastSubscribeRequest + days("subscribe_cooldown", 30L) > now()
+
private fun days(key: String, default: Long): Long =
TimeUnit.DAYS.toMillis(remoteConfig?.getLong(key) ?: default)
diff --git a/app/src/googleplay/res/xml/remote_config_defaults.xml b/app/src/googleplay/res/xml/remote_config_defaults.xml
index 860a062f0..f425ec46d 100644
--- a/app/src/googleplay/res/xml/remote_config_defaults.xml
+++ b/app/src/googleplay/res/xml/remote_config_defaults.xml
@@ -9,4 +9,8 @@
install_cooldown
14
+
+ subscribe_cooldown
+ 30
+
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 2c88aed76..354acd0a2 100644
--- a/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt
+++ b/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt
@@ -13,9 +13,7 @@ import android.os.Bundle
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.view.ActionMode
-import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.astrid.activity.TaskEditFragment.Companion.newTaskEditFragment
import com.todoroo.astrid.activity.TaskListFragment.TaskListFragmentCallbackHandler
@@ -28,13 +26,15 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.tasks.BuildConfig
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.activities.TagSettingsActivity
+import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory
import org.tasks.data.AlarmDao
import org.tasks.data.LocationDao
@@ -61,6 +61,8 @@ import org.tasks.ui.MainActivityEvent
import org.tasks.ui.MainActivityEventBus
import org.tasks.ui.NavigationDrawerFragment
import org.tasks.ui.NavigationDrawerFragment.Companion.newNavigationDrawer
+import org.tasks.ui.TaskListEvent
+import org.tasks.ui.TaskListEventBus
import timber.log.Timber
import javax.inject.Inject
@@ -78,7 +80,9 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl
@Inject lateinit var tagDataDao: TagDataDao
@Inject lateinit var alarmDao: AlarmDao
@Inject lateinit var eventBus: MainActivityEventBus
+ @Inject lateinit var taskListEventBus: TaskListEventBus
@Inject lateinit var playServices: PlayServices
+ @Inject lateinit var firebase: Firebase
private var currentNightMode = 0
private var currentPro = false
@@ -101,11 +105,9 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl
}
handleIntent()
- lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.RESUMED) {
- eventBus.collect(this@MainActivity::process)
- }
- }
+ eventBus
+ .onEach(this::process)
+ .launchIn(lifecycleScope)
}
private suspend fun process(event: MainActivityEvent) = when (event) {
@@ -477,6 +479,16 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl
taskEditFragment!!.onRemoteListChanged(filter)
}
+ override fun onStart() {
+ super.onStart()
+
+ lifecycleScope.launch {
+ if (!inventory.hasPro && !firebase.subscribeCooldown) {
+ taskListEventBus.tryEmit(TaskListEvent.BegForSubscription)
+ }
+ }
+ }
+
companion object {
/** For indicating the new list screen should be launched at fragment setup time */
const val TOKEN_CREATE_NEW_LIST_NAME = "newListName" // $NON-NLS-1$
diff --git a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt
index 25d85aa7b..16098920a 100644
--- a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt
+++ b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt
@@ -23,6 +23,9 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ShareCompat
import androidx.core.view.forEach
@@ -30,9 +33,7 @@ import androidx.core.view.isVisible
import androidx.core.view.setMargins
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
-import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
import androidx.paging.PagedList
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
@@ -41,9 +42,11 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.bottomappbar.BottomAppBar
+import com.google.android.material.composethemeadapter.MdcTheme
import com.google.android.material.snackbar.Snackbar
import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.andlib.utility.DateUtilities
+import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.adapter.TaskAdapter
import com.todoroo.astrid.adapter.TaskAdapterProvider
import com.todoroo.astrid.api.AstridApiConstants.EXTRAS_OLD_DUE_DATE
@@ -69,12 +72,14 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.ShortcutManager
+import org.tasks.Tasks.Companion.IS_GOOGLE_PLAY
import org.tasks.activities.FilterSettingsActivity
import org.tasks.activities.GoogleTaskListSettingsActivity
import org.tasks.activities.ListPicker
@@ -82,7 +87,9 @@ import org.tasks.activities.ListPicker.Companion.newListPicker
import org.tasks.activities.PlaceSettingsActivity
import org.tasks.activities.TagSettingsActivity
import org.tasks.analytics.Firebase
+import org.tasks.billing.PurchaseActivity
import org.tasks.caldav.BaseCaldavCalendarSettingsActivity
+import org.tasks.compose.AnimatedBanner
import org.tasks.data.CaldavDao
import org.tasks.data.TagDataDao
import org.tasks.data.TaskContainer
@@ -169,6 +176,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
private lateinit var callbacks: TaskListFragmentCallbackHandler
private lateinit var binding: FragmentTaskListBinding
+ @OptIn(ExperimentalAnimationApi::class)
private fun process(event: TaskListEvent) = when (event) {
is TaskListEvent.TaskCreated ->
onTaskCreated(event.uuid)
@@ -176,6 +184,33 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
makeSnackbar(R.string.calendar_event_created, event.title)
?.setAction(R.string.action_open) { context?.openUri(event.uri) }
?.show()
+ is TaskListEvent.BegForSubscription -> {
+ binding.banner.setContent {
+ val showBanner = rememberSaveable { mutableStateOf(true) }
+ MdcTheme {
+ AnimatedBanner(
+ isVisible = showBanner,
+ dismiss = {
+ preferences.lastSubscribeRequest = now()
+ showBanner.value = false
+ },
+ subscribe = {
+ purchase()
+ showBanner.value = false
+ },
+ )
+ }
+ }
+ }
+ }
+
+ private fun purchase() {
+ if (IS_GOOGLE_PLAY) {
+ startActivity(Intent(context, PurchaseActivity::class.java))
+ } else {
+ preferences.lastSubscribeRequest = now()
+ context?.openUri(R.string.url_donate)
+ }
}
override fun onRefresh() {
@@ -217,8 +252,16 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
outState.putLongArray(EXTRA_COLLAPSED, taskAdapter.getCollapsed().toLongArray())
}
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ taskListEventBus
+ .onEach(this::process)
+ .launchIn(viewLifecycleOwner.lifecycleScope)
+ }
+
override fun onCreateView(
- inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentTaskListBinding.inflate(inflater, container, false)
with (binding) {
swipeRefreshLayout = bodyStandard.swipeLayout
@@ -283,12 +326,6 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
toolbar.setNavigationOnClickListener { callbacks.onNavigationIconClicked() }
setupMenu(toolbar)
- lifecycleScope.launch {
- viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
- taskListEventBus.collect(this@TaskListFragment::process)
- }
- }
-
return binding.root
}
diff --git a/app/src/main/java/com/todoroo/astrid/service/Upgrader.kt b/app/src/main/java/com/todoroo/astrid/service/Upgrader.kt
index 73b088f74..dabf3f8c6 100644
--- a/app/src/main/java/com/todoroo/astrid/service/Upgrader.kt
+++ b/app/src/main/java/com/todoroo/astrid/service/Upgrader.kt
@@ -114,6 +114,7 @@ class Upgrader @Inject constructor(
} else {
setInstallDetails(to)
}
+ preferences.lastSubscribeRequest = 0L
preferences.setCurrentVersion(to)
}
diff --git a/app/src/main/java/org/tasks/billing/PurchaseActivity.kt b/app/src/main/java/org/tasks/billing/PurchaseActivity.kt
index 26d489e0f..998331652 100644
--- a/app/src/main/java/org/tasks/billing/PurchaseActivity.kt
+++ b/app/src/main/java/org/tasks/billing/PurchaseActivity.kt
@@ -9,6 +9,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.window.Dialog
import androidx.lifecycle.lifecycleScope
import com.google.android.material.composethemeadapter.MdcTheme
+import com.todoroo.andlib.utility.DateUtilities
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.LocalBroadcastManager
@@ -16,6 +17,7 @@ import org.tasks.Tasks.Companion.IS_GENERIC
import org.tasks.compose.PurchaseText.PurchaseText
import org.tasks.extensions.Context.toast
import org.tasks.injection.InjectingAppCompatActivity
+import org.tasks.preferences.Preferences
import org.tasks.themes.Theme
import javax.inject.Inject
@@ -25,6 +27,7 @@ class PurchaseActivity : InjectingAppCompatActivity(), OnPurchasesUpdated {
@Inject lateinit var billingClient: BillingClient
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var inventory: Inventory
+ @Inject lateinit var preferences: Preferences
private var currentSubscription: Purchase? = null
private val purchaseReceiver: BroadcastReceiver = object : BroadcastReceiver() {
@@ -38,6 +41,8 @@ class PurchaseActivity : InjectingAppCompatActivity(), OnPurchasesUpdated {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ preferences.lastSubscribeRequest = DateUtilities.now()
+
val github = intent?.extras?.getBoolean(EXTRA_GITHUB) ?: false
theme.applyToContext(this)
diff --git a/app/src/main/java/org/tasks/compose/Banner.kt b/app/src/main/java/org/tasks/compose/Banner.kt
new file mode 100644
index 000000000..6a6a06731
--- /dev/null
+++ b/app/src/main/java/org/tasks/compose/Banner.kt
@@ -0,0 +1,108 @@
+package org.tasks.compose
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Divider
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.delay
+import org.tasks.R
+import org.tasks.Tasks.Companion.IS_GENERIC
+
+@ExperimentalAnimationApi
+@Composable
+fun AnimatedBanner(
+ isVisible: MutableState,
+ dismiss: () -> Unit,
+ subscribe: () -> Unit,
+) {
+ var show by rememberSaveable { mutableStateOf(false) }
+ if (isVisible.value) {
+ LaunchedEffect(key1 = isVisible, block = {
+ delay(500)
+ show = true
+ })
+ } else {
+ show = false
+ }
+ AnimatedVisibility(
+ visible = show,
+ enter = expandVertically(
+ expandFrom = Alignment.Top
+ ),
+ exit = shrinkVertically(),
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth(),
+ ) {
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ text = stringResource(
+ id = if (IS_GENERIC) {
+ R.string.enjoying_tasks
+ } else {
+ R.string.tasks_needs_your_support
+ }
+ ),
+ style = MaterialTheme.typography.body1,
+ modifier = Modifier.padding(horizontal = 16.dp),
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ text = stringResource(
+ id = if (IS_GENERIC) {
+ R.string.tasks_needs_your_support
+ } else {
+ R.string.support_development_subscribe
+ }
+ ),
+ style = MaterialTheme.typography.body2,
+ modifier = Modifier.padding(horizontal = 16.dp),
+ )
+ Row(
+ modifier = Modifier
+ .padding(vertical = 8.dp, horizontal = 16.dp)
+ .align(Alignment.End),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ TextButton(onClick = dismiss) {
+ Text(text = stringResource(id = R.string.dismiss))
+ }
+ TextButton(onClick = subscribe) {
+ Text(
+ text = stringResource(
+ id = if (IS_GENERIC) {
+ R.string.TLA_menu_donate
+ } else {
+ R.string.button_subscribe
+ }
+ )
+ )
+ }
+ }
+ Divider()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/tasks/preferences/Preferences.kt b/app/src/main/java/org/tasks/preferences/Preferences.kt
index 29f30cdd5..106b41969 100644
--- a/app/src/main/java/org/tasks/preferences/Preferences.kt
+++ b/app/src/main/java/org/tasks/preferences/Preferences.kt
@@ -536,6 +536,10 @@ class Preferences @JvmOverloads constructor(
get() = getLong(R.string.p_last_review_request, 0L)
set(value) = setLong(R.string.p_last_review_request, value)
+ var lastSubscribeRequest: Long
+ get() = getLong(R.string.p_last_subscribe_request, 0L)
+ set(value) = setLong(R.string.p_last_subscribe_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/TaskListEvent.kt b/app/src/main/java/org/tasks/ui/TaskListEvent.kt
index cc2b9fa63..49bc70f36 100644
--- a/app/src/main/java/org/tasks/ui/TaskListEvent.kt
+++ b/app/src/main/java/org/tasks/ui/TaskListEvent.kt
@@ -7,4 +7,5 @@ typealias TaskListEventBus = MutableSharedFlow
sealed interface TaskListEvent {
data class TaskCreated(val uuid: String) : TaskListEvent
data class CalendarEventCreated(val title: String?, val uri: String) : TaskListEvent
+ object BegForSubscription : TaskListEvent
}
diff --git a/app/src/main/res/layout/fragment_task_list.xml b/app/src/main/res/layout/fragment_task_list.xml
index a92c210c0..188539bd4 100644
--- a/app/src/main/res/layout/fragment_task_list.xml
+++ b/app/src/main/res/layout/fragment_task_list.xml
@@ -38,6 +38,10 @@
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
+
show_whats_new
just_updated
last_review_request
+ last_subscribe_request
backups_enabled
backups_ignore_warnings
backups_android_backup_enabled
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b48659abb..a2458d283 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -733,4 +733,5 @@ File %1$s contained %2$s.\n\n
Other
Server type
Snoozed
+ Dismiss