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 768a54604..1776b9462 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt @@ -57,8 +57,6 @@ 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 @@ -76,7 +74,6 @@ class MainActivity : AppCompatActivity(), TaskListFragmentCallbackHandler, Timer @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 @@ -476,16 +473,6 @@ class MainActivity : AppCompatActivity(), TaskListFragmentCallbackHandler, Timer actionMode = null } - 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 33f474fde..5c928425f 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt @@ -24,10 +24,6 @@ import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.Toolbar import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.app.ShareCompat import androidx.core.view.forEach @@ -49,7 +45,6 @@ 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 @@ -82,15 +77,14 @@ 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.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.SubscriptionNagBanner +import org.tasks.compose.collectAsStateLifecycleAware import org.tasks.data.CaldavDao import org.tasks.data.TagDataDao import org.tasks.data.TaskContainer @@ -179,7 +173,6 @@ 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) @@ -187,36 +180,6 @@ 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 { - var showBanner by rememberSaveable { mutableStateOf(true) } - MdcTheme { - SubscriptionNagBanner( - visible = showBanner, - subscribe = { - showBanner = false - preferences.lastSubscribeRequest = now() - purchase() - firebase.logEvent(R.string.event_banner_sub, R.string.param_click to true) - }, - dismiss = { - showBanner = false - preferences.lastSubscribeRequest = now() - firebase.logEvent(R.string.event_banner_sub, R.string.param_click to 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() { @@ -266,6 +229,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL .launchIn(viewLifecycleOwner.lifecycleScope) } + @OptIn(ExperimentalAnimationApi::class) override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = FragmentTaskListBinding.inflate(inflater, container, false) @@ -292,9 +256,9 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL recyclerView.layoutManager = LinearLayoutManager(context) lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { - listViewModel.tasks.collect { - submitList(it) - if (it.isEmpty()) { + listViewModel.state.collect { + submitList(it.tasks) + if (it.tasks.isEmpty()) { swipeRefreshLayout.visibility = View.GONE emptyRefreshLayout.visibility = View.VISIBLE } else { @@ -343,6 +307,16 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL } finishActionMode() } + binding.banner.setContent { + val showBanner = listViewModel.state.collectAsStateLifecycleAware().value.begForSubscription + MdcTheme { + SubscriptionNagBanner( + visible = showBanner, + subscribe = { listViewModel.dismissBanner(clickedPurchase = true) }, + dismiss = { listViewModel.dismissBanner(clickedPurchase = false) }, + ) + } + } return binding.root } diff --git a/app/src/main/java/org/tasks/ui/SubtaskControlSet.kt b/app/src/main/java/org/tasks/ui/SubtaskControlSet.kt index db6c4a9b2..06bdfcbab 100644 --- a/app/src/main/java/org/tasks/ui/SubtaskControlSet.kt +++ b/app/src/main/java/org/tasks/ui/SubtaskControlSet.kt @@ -58,7 +58,7 @@ class SubtaskControlSet : TaskEditControlFragment() { filter = viewModel.selectedList.collectAsStateLifecycleAware().value, hasParent = viewModel.hasParent, desaturate = preferences.desaturateDarkMode, - existingSubtasks = listViewModel.tasks.collectAsStateLifecycleAware(initial = emptyList()).value, + existingSubtasks = listViewModel.state.collectAsStateLifecycleAware().value.tasks, newSubtasks = viewModel.newSubtasks.collectAsStateLifecycleAware().value, openSubtask = this@SubtaskControlSet::openSubtask, completeExistingSubtask = this@SubtaskControlSet::complete, diff --git a/app/src/main/java/org/tasks/ui/TaskListEvent.kt b/app/src/main/java/org/tasks/ui/TaskListEvent.kt index 49bc70f36..cc2b9fa63 100644 --- a/app/src/main/java/org/tasks/ui/TaskListEvent.kt +++ b/app/src/main/java/org/tasks/ui/TaskListEvent.kt @@ -7,5 +7,4 @@ 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/java/org/tasks/ui/TaskListViewModel.kt b/app/src/main/java/org/tasks/ui/TaskListViewModel.kt index 542a8c5f0..8653c4951 100644 --- a/app/src/main/java/org/tasks/ui/TaskListViewModel.kt +++ b/app/src/main/java/org/tasks/ui/TaskListViewModel.kt @@ -1,44 +1,59 @@ package org.tasks.ui +import android.annotation.SuppressLint import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.todoroo.andlib.utility.DateUtilities import com.todoroo.astrid.api.Filter import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.Flow +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import org.tasks.LocalBroadcastManager +import org.tasks.R +import org.tasks.Tasks +import org.tasks.analytics.Firebase +import org.tasks.billing.Inventory +import org.tasks.billing.PurchaseActivity import org.tasks.compose.throttleLatest import org.tasks.data.TaskContainer import org.tasks.data.TaskDao import org.tasks.data.TaskListQuery.getQuery +import org.tasks.extensions.Context.openUri import org.tasks.preferences.Preferences import javax.inject.Inject @HiltViewModel +@SuppressLint("StaticFieldLeak") class TaskListViewModel @Inject constructor( - private val preferences: Preferences, - private val taskDao: TaskDao, - private val localBroadcastManager: LocalBroadcastManager, + @ApplicationContext private val context: Context, + private val preferences: Preferences, + private val taskDao: TaskDao, + private val localBroadcastManager: LocalBroadcastManager, + private val inventory: Inventory, + private val firebase: Firebase, ) : ViewModel() { data class State( val filter: Filter? = null, val now: Long = DateUtilities.now(), + val tasks: List = emptyList(), + val begForSubscription: Boolean = false, ) private val _state = MutableStateFlow(State()) - - val tasks: Flow> = - _state - .filter { it.filter != null } - .throttleLatest(333) - .map { taskDao.fetchTasks { getQuery(preferences, it.filter!!) } } + val state = _state.asStateFlow() private val refreshReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -56,8 +71,44 @@ class TaskListViewModel @Inject constructor( _state.update { it.copy(now = DateUtilities.now()) } } + fun dismissBanner(clickedPurchase: Boolean) { + _state.update { + it.copy(begForSubscription = false) + } + preferences.lastSubscribeRequest = DateUtilities.now() + firebase.logEvent(R.string.event_banner_sub, R.string.param_click to clickedPurchase) + if (clickedPurchase) { + if (Tasks.IS_GOOGLE_PLAY) { + context.startActivity(Intent(context, PurchaseActivity::class.java)) + } else { + preferences.lastSubscribeRequest = DateUtilities.now() + context.openUri(R.string.url_donate) + } + } + } + init { localBroadcastManager.registerRefreshReceiver(refreshReceiver) + + _state + .filter { it.filter != null } + .throttleLatest(333) + .map { taskDao.fetchTasks { getQuery(preferences, it.filter!!) } } + .onEach { tasks -> + _state.update { + it.copy(tasks = tasks) + } + } + .flowOn(Dispatchers.Default) + .launchIn(viewModelScope) + + viewModelScope.launch(Dispatchers.Default) { + if (!inventory.hasPro && !firebase.subscribeCooldown) { + _state.update { + it.copy(begForSubscription = true) + } + } + } } override fun onCleared() {