diff --git a/app/src/androidTest/java/com/todoroo/astrid/service/TaskDeleterTest.kt b/app/src/androidTest/java/com/todoroo/astrid/service/TaskDeleterTest.kt index 3ad55d8b3..6a0021c8e 100644 --- a/app/src/androidTest/java/com/todoroo/astrid/service/TaskDeleterTest.kt +++ b/app/src/androidTest/java/com/todoroo/astrid/service/TaskDeleterTest.kt @@ -1,22 +1,15 @@ package com.todoroo.astrid.service -import com.natpryce.makeiteasy.MakeItEasy.with -import com.todoroo.astrid.core.BuiltInFilterExposer.Companion.getMyTasksFilter +import com.todoroo.astrid.data.Task import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules import kotlinx.coroutines.runBlocking import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test -import org.tasks.data.GoogleTaskDao import org.tasks.data.TaskDao import org.tasks.injection.InjectingTestCase import org.tasks.injection.ProductionModule -import org.tasks.makers.TaskMaker.COMPLETION_TIME -import org.tasks.makers.TaskMaker.PARENT -import org.tasks.makers.TaskMaker.RECUR -import org.tasks.makers.TaskMaker.newTask -import org.tasks.time.DateTime import javax.inject.Inject @UninstallModules(ProductionModule::class) @@ -24,75 +17,26 @@ import javax.inject.Inject class TaskDeleterTest : InjectingTestCase() { @Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDeleter: TaskDeleter - @Inject lateinit var googleTaskDao: GoogleTaskDao @Test - fun clearCompletedTask() = runBlocking { - val task = taskDao.createNew(newTask(with(COMPLETION_TIME, DateTime()))) + fun markTaskAsDeleted() = runBlocking { + val task = Task() + taskDao.createNew(task) - clearCompleted() + taskDeleter.markDeleted(task) - assertTrue(taskDao.fetch(task)!!.isDeleted) + assertTrue(taskDao.fetch(task.id)!!.isDeleted) } @Test - fun dontDeleteTaskWithRecurringParent() = runBlocking { - val parent = taskDao.createNew(newTask(with(RECUR, "RRULE:FREQ=DAILY;INTERVAL=1"))) - val child = taskDao.createNew(newTask( - with(PARENT, parent), - with(COMPLETION_TIME, DateTime()) - )) + fun dontDeleteReadOnlyTasks() = runBlocking { + val task = Task( + readOnly = true + ) + taskDao.createNew(task) - clearCompleted() + taskDeleter.markDeleted(task) - assertFalse(taskDao.fetch(child)!!.isDeleted) + assertFalse(taskDao.fetch(task.id)!!.isDeleted) } - - @Test - fun dontDeleteTaskWithRecurringGrandparent() = runBlocking { - val grandparent = taskDao.createNew(newTask(with(RECUR, "RRULE:FREQ=DAILY;INTERVAL=1"))) - val parent = taskDao.createNew(newTask(with(PARENT, grandparent))) - val child = taskDao.createNew(newTask( - with(PARENT, parent), - with(COMPLETION_TIME, DateTime()) - )) - - clearCompleted() - - assertFalse(taskDao.fetch(child)!!.isDeleted) - } - - @Test - fun clearGrandchildWithNoRecurringAncestors() = runBlocking { - val grandparent = taskDao.createNew(newTask()) - val parent = taskDao.createNew(newTask(with(PARENT, grandparent))) - val child = taskDao.createNew(newTask( - with(PARENT, parent), - with(COMPLETION_TIME, DateTime()) - )) - - clearCompleted() - - assertTrue(taskDao.fetch(child)!!.isDeleted) - } - - @Test - fun clearGrandchildWithCompletedRecurringAncestor() = runBlocking { - val grandparent = taskDao.createNew(newTask( - with(RECUR, "RRULE:FREQ=DAILY;INTERVAL=1"), - with(COMPLETION_TIME, DateTime()) - )) - val parent = taskDao.createNew(newTask(with(PARENT, grandparent))) - val child = taskDao.createNew(newTask( - with(PARENT, parent), - with(COMPLETION_TIME, DateTime()) - )) - - clearCompleted() - - assertTrue(taskDao.fetch(child)!!.isDeleted) - } - - private suspend fun clearCompleted() = - taskDeleter.clearCompleted(getMyTasksFilter(context.resources)) -} \ No newline at end of file +} diff --git a/app/src/androidTest/java/org/tasks/ui/editviewmodel/TaskListViewModelTest.kt b/app/src/androidTest/java/org/tasks/ui/editviewmodel/TaskListViewModelTest.kt new file mode 100644 index 000000000..5c7eec3da --- /dev/null +++ b/app/src/androidTest/java/org/tasks/ui/editviewmodel/TaskListViewModelTest.kt @@ -0,0 +1,145 @@ +package org.tasks.ui.editviewmodel + +import com.todoroo.andlib.utility.DateUtilities.now +import com.todoroo.astrid.core.BuiltInFilterExposer +import com.todoroo.astrid.data.Task +import com.todoroo.astrid.service.TaskDeleter +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.UninstallModules +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.tasks.LocalBroadcastManager +import org.tasks.analytics.Firebase +import org.tasks.billing.Inventory +import org.tasks.data.DeletionDao +import org.tasks.data.TaskDao +import org.tasks.injection.InjectingTestCase +import org.tasks.injection.ProductionModule +import org.tasks.preferences.Preferences +import org.tasks.ui.TaskListViewModel +import javax.inject.Inject + +@UninstallModules(ProductionModule::class) +@HiltAndroidTest +class TaskListViewModelTest : InjectingTestCase() { + private lateinit var viewModel: TaskListViewModel + @Inject lateinit var preferences: Preferences + @Inject lateinit var taskDao: TaskDao + @Inject lateinit var taskDeleter: TaskDeleter + @Inject lateinit var deletionDao: DeletionDao + @Inject lateinit var localBroadcastManager: LocalBroadcastManager + @Inject lateinit var inventory: Inventory + @Inject lateinit var firebase: Firebase + + @Before + override fun setUp() { + super.setUp() + viewModel = TaskListViewModel( + context = context, + preferences = preferences, + taskDao = taskDao, + deletionDao = deletionDao, + taskDeleter = taskDeleter, + localBroadcastManager = localBroadcastManager, + inventory = inventory, + firebase = firebase, + ) + viewModel.setFilter(BuiltInFilterExposer.getMyTasksFilter(context.resources)) + } + + @Test + fun clearCompletedTask() = runBlocking { + val task = taskDao.createNew( + Task(completionDate = now()) + ) + + clearCompleted() + + assertTrue(taskDao.fetch(task)!!.isDeleted) + } + + @Test + fun dontDeleteTaskWithRecurringParent() = runBlocking { + val parent = taskDao.createNew( + Task( + recurrence = "RRULE:FREQ=DAILY;INTERVAL=1" + ) + ) + val child = taskDao.createNew( + Task( + parent = parent, + completionDate = now(), + ) + ) + + clearCompleted() + + assertFalse(taskDao.fetch(child)!!.isDeleted) + } + + @Test + fun dontDeleteTaskWithRecurringGrandparent() = runBlocking { + val grandparent = taskDao.createNew( + Task(recurrence = "RRULE:FREQ=DAILY;INTERVAL=1") + ) + val parent = taskDao.createNew( + Task(parent = grandparent) + ) + val child = taskDao.createNew( + Task( + parent = parent, + completionDate = now(), + ) + ) + + clearCompleted() + + assertFalse(taskDao.fetch(child)!!.isDeleted) + } + + @Test + fun clearGrandchildWithNoRecurringAncestors() = runBlocking { + val grandparent = taskDao.createNew(Task()) + val parent = taskDao.createNew( + Task(parent = grandparent) + ) + val child = taskDao.createNew( + Task( + parent = parent, + completionDate = now(), + ) + ) + + clearCompleted() + + assertTrue(taskDao.fetch(child)!!.isDeleted) + } + + @Test + fun clearGrandchildWithCompletedRecurringAncestor() = runBlocking { + val grandparent = taskDao.createNew( + Task( + recurrence = "RRULE:FREQ=DAILY;INTERVAL=1", + completionDate = now(), + ) + ) + val parent = taskDao.createNew( + Task(parent = grandparent) + ) + val child = taskDao.createNew( + Task( + parent = parent, + completionDate = now(), + ) + ) + + clearCompleted() + + assertTrue(taskDao.fetch(child)!!.isDeleted) + } + + private suspend fun clearCompleted() = viewModel.markDeleted(viewModel.getTasksToClear()) +} \ No newline at end of file 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 1928737ee..ed35f96be 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt @@ -68,7 +68,6 @@ import com.todoroo.astrid.data.Task import com.todoroo.astrid.repeats.RepeatTaskHelper import com.todoroo.astrid.service.TaskCompleter import com.todoroo.astrid.service.TaskCreator -import com.todoroo.astrid.service.TaskDeleter import com.todoroo.astrid.service.TaskDuplicator import com.todoroo.astrid.service.TaskMover import com.todoroo.astrid.timers.TimerPlugin @@ -105,7 +104,6 @@ import org.tasks.dialogs.SortSettingsActivity import org.tasks.extensions.Context.openUri import org.tasks.extensions.Context.toast import org.tasks.extensions.Fragment.safeStartActivityForResult -import org.tasks.extensions.formatNumber import org.tasks.extensions.hideKeyboard import org.tasks.extensions.setOnQueryTextListener import org.tasks.filters.PlaceFilter @@ -136,7 +134,6 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL private val repeatConfirmationReceiver = RepeatConfirmationReceiver() @Inject lateinit var syncAdapters: SyncAdapters - @Inject lateinit var taskDeleter: TaskDeleter @Inject lateinit var preferences: Preferences @Inject lateinit var dialogBuilder: DialogBuilder @Inject lateinit var taskCreator: TaskCreator @@ -449,11 +446,28 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL true } R.id.menu_clear_completed -> { - dialogBuilder - .newDialog(R.string.clear_completed_tasks_confirmation) - .setPositiveButton(R.string.ok) { _, _ -> clearCompleted() } - .setNegativeButton(R.string.cancel, null) - .show() + lifecycleScope.launch { + val tasks = listViewModel.getTasksToClear() + val countString = requireContext().resources.getQuantityString(R.plurals.Ntasks, tasks.size, tasks.size) + if (tasks.isEmpty()) { + context?.toast(R.string.delete_multiple_tasks_confirmation, countString) + } else { + dialogBuilder + .newDialog(R.string.clear_completed_tasks_confirmation) + .setMessage(R.string.clear_completed_tasks_count, countString) + .setPositiveButton(R.string.ok) { _, _ -> + lifecycleScope.launch { + listViewModel.markDeleted(tasks) + context?.toast( + R.string.delete_multiple_tasks_confirmation, + countString + ) + } + } + .setNegativeButton(R.string.cancel, null) + .show() + } + } true } R.id.menu_filter_settings -> { @@ -525,11 +539,6 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL } } - private fun clearCompleted() = lifecycleScope.launch { - val count = taskDeleter.clearCompleted(filter) - context?.toast(R.string.delete_multiple_tasks_confirmation, locale.formatNumber(count)) - } - private fun createNewTask() { lifecycleScope.launch { shortcutManager.reportShortcutUsed(ShortcutManager.SHORTCUT_NEW_TASK) @@ -845,7 +854,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL finishActionMode() val result = withContext(NonCancellable) { - taskDeleter.markDeleted(tasks) + listViewModel.markDeleted(tasks) } result.forEach { onTaskDelete(it) } makeSnackbar(R.string.delete_multiple_tasks_confirmation, result.size.toString())?.show() diff --git a/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.kt b/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.kt index 6b90b4094..5a271fd83 100644 --- a/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.kt +++ b/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.kt @@ -3,12 +3,12 @@ package com.todoroo.astrid.service import android.content.Context import androidx.room.withTransaction import com.todoroo.astrid.alarms.AlarmService -import com.todoroo.astrid.api.Filter -import com.todoroo.astrid.api.FilterImpl import com.todoroo.astrid.dao.Database import com.todoroo.astrid.data.Task import com.todoroo.astrid.timers.TimerPlugin import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.withContext import org.tasks.BuildConfig import org.tasks.LocalBroadcastManager import org.tasks.caldav.VtodoCache @@ -16,10 +16,8 @@ import org.tasks.data.CaldavAccount import org.tasks.data.CaldavCalendar import org.tasks.data.DeletionDao import org.tasks.data.LocationDao -import org.tasks.data.TaskContainer import org.tasks.data.TaskDao import org.tasks.data.UserActivityDao -import org.tasks.db.QueryUtils import org.tasks.db.SuspendDbUtils.chunkedMap import org.tasks.files.FileHelper import org.tasks.location.GeofenceApi @@ -47,7 +45,7 @@ class TaskDeleter @Inject constructor( suspend fun markDeleted(item: Task) = markDeleted(listOf(item.id)) - suspend fun markDeleted(taskIds: List): List { + suspend fun markDeleted(taskIds: List): List = withContext(NonCancellable) { val ids = taskIds .toSet() .plus(taskIds.chunkedMap(taskDao::getChildren)) @@ -60,21 +58,7 @@ class TaskDeleter @Inject constructor( } syncAdapters.sync() localBroadcastManager.broadcastRefresh() - return taskDao.fetch(ids) - } - - suspend fun clearCompleted(filter: Filter): Int { - val deleteFilter = FilterImpl( - sql = QueryUtils.removeOrder(QueryUtils.showHiddenAndCompleted(filter.sql!!)), - ) - val completed = taskDao.fetchTasks(preferences, deleteFilter) - .filter(TaskContainer::isCompleted) - .filterNot(TaskContainer::isReadOnly) - .map(TaskContainer::id) - .toMutableList() - completed.removeAll(deletionDao.hasRecurringAncestors(completed)) - markDeleted(completed) - return completed.size + taskDao.fetch(ids) } suspend fun delete(task: Task) = delete(task.id) diff --git a/app/src/main/java/org/tasks/ui/TaskListViewModel.kt b/app/src/main/java/org/tasks/ui/TaskListViewModel.kt index c76861eb9..1c09bce9c 100644 --- a/app/src/main/java/org/tasks/ui/TaskListViewModel.kt +++ b/app/src/main/java/org/tasks/ui/TaskListViewModel.kt @@ -8,8 +8,11 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.todoroo.andlib.utility.DateUtilities import com.todoroo.astrid.api.Filter +import com.todoroo.astrid.api.FilterImpl import com.todoroo.astrid.api.SearchFilter import com.todoroo.astrid.core.BuiltInFilterExposer +import com.todoroo.astrid.data.Task +import com.todoroo.astrid.service.TaskDeleter import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers @@ -30,9 +33,11 @@ import org.tasks.analytics.Firebase import org.tasks.billing.Inventory import org.tasks.billing.PurchaseActivity import org.tasks.compose.throttleLatest +import org.tasks.data.DeletionDao import org.tasks.data.TaskContainer import org.tasks.data.TaskDao import org.tasks.data.TaskListQuery.getQuery +import org.tasks.db.QueryUtils import org.tasks.extensions.Context.openUri import org.tasks.preferences.Preferences import javax.inject.Inject @@ -43,6 +48,8 @@ class TaskListViewModel @Inject constructor( @ApplicationContext private val context: Context, private val preferences: Preferences, private val taskDao: TaskDao, + private val taskDeleter: TaskDeleter, + private val deletionDao: DeletionDao, private val localBroadcastManager: LocalBroadcastManager, private val inventory: Inventory, private val firebase: Firebase, @@ -106,6 +113,23 @@ class TaskListViewModel @Inject constructor( } } + suspend fun getTasksToClear(): List { + val filter = _state.value.filter ?: return emptyList() + val deleteFilter = FilterImpl( + sql = QueryUtils.removeOrder(QueryUtils.showHiddenAndCompleted(filter.sql!!)), + ) + val completed = taskDao.fetchTasks(preferences, deleteFilter) + .filter(TaskContainer::isCompleted) + .filterNot(TaskContainer::isReadOnly) + .map(TaskContainer::id) + .toMutableList() + completed.removeAll(deletionDao.hasRecurringAncestors(completed)) + return completed + } + + suspend fun markDeleted(tasks: List): List = + taskDeleter.markDeleted(tasks) + init { localBroadcastManager.registerRefreshReceiver(refreshReceiver) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8425758d0..699a78948 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -408,6 +408,7 @@ File %1$s contained %2$s.\n\n Below title Hidden Clear completed tasks? + %s will be deleted %s copied %s deleted Delete selected tasks?