Display number of tasks to be cleared

pull/2744/head
Alex Baker 4 months ago
parent db889d233a
commit cf182aceab

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

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

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

@ -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<Long>): List<Task> {
suspend fun markDeleted(taskIds: List<Long>): List<Task> = 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)

@ -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<Long> {
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<Long>): List<Task> =
taskDeleter.markDeleted(tasks)
init {
localBroadcastManager.registerRefreshReceiver(refreshReceiver)

@ -408,6 +408,7 @@ File %1$s contained %2$s.\n\n
<string name="widget_due_date_below_title">Below title</string>
<string name="widget_due_date_hidden">Hidden</string>
<string name="clear_completed_tasks_confirmation">Clear completed tasks?</string>
<string name="clear_completed_tasks_count">%s will be deleted</string>
<string name="copy_multiple_tasks_confirmation">%s copied</string>
<string name="delete_multiple_tasks_confirmation">%s deleted</string>
<string name="delete_selected_tasks">Delete selected tasks?</string>

Loading…
Cancel
Save