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