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 package com.todoroo.astrid.service
import com.natpryce.makeiteasy.MakeItEasy.with import com.todoroo.astrid.data.Task
import com.todoroo.astrid.core.BuiltInFilterExposer.Companion.getMyTasksFilter
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.data.GoogleTaskDao
import org.tasks.data.TaskDao import org.tasks.data.TaskDao
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule 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 import javax.inject.Inject
@UninstallModules(ProductionModule::class) @UninstallModules(ProductionModule::class)
@ -24,75 +17,26 @@ import javax.inject.Inject
class TaskDeleterTest : InjectingTestCase() { class TaskDeleterTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var taskDeleter: TaskDeleter @Inject lateinit var taskDeleter: TaskDeleter
@Inject lateinit var googleTaskDao: GoogleTaskDao
@Test @Test
fun clearCompletedTask() = runBlocking { fun markTaskAsDeleted() = runBlocking {
val task = taskDao.createNew(newTask(with(COMPLETION_TIME, DateTime()))) val task = Task()
taskDao.createNew(task)
clearCompleted() taskDeleter.markDeleted(task)
assertTrue(taskDao.fetch(task)!!.isDeleted) assertTrue(taskDao.fetch(task.id)!!.isDeleted)
} }
@Test @Test
fun dontDeleteTaskWithRecurringParent() = runBlocking { fun dontDeleteReadOnlyTasks() = runBlocking {
val parent = taskDao.createNew(newTask(with(RECUR, "RRULE:FREQ=DAILY;INTERVAL=1"))) val task = Task(
val child = taskDao.createNew(newTask( readOnly = true
with(PARENT, parent), )
with(COMPLETION_TIME, DateTime()) 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.repeats.RepeatTaskHelper
import com.todoroo.astrid.service.TaskCompleter import com.todoroo.astrid.service.TaskCompleter
import com.todoroo.astrid.service.TaskCreator import com.todoroo.astrid.service.TaskCreator
import com.todoroo.astrid.service.TaskDeleter
import com.todoroo.astrid.service.TaskDuplicator import com.todoroo.astrid.service.TaskDuplicator
import com.todoroo.astrid.service.TaskMover import com.todoroo.astrid.service.TaskMover
import com.todoroo.astrid.timers.TimerPlugin 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.openUri
import org.tasks.extensions.Context.toast import org.tasks.extensions.Context.toast
import org.tasks.extensions.Fragment.safeStartActivityForResult import org.tasks.extensions.Fragment.safeStartActivityForResult
import org.tasks.extensions.formatNumber
import org.tasks.extensions.hideKeyboard import org.tasks.extensions.hideKeyboard
import org.tasks.extensions.setOnQueryTextListener import org.tasks.extensions.setOnQueryTextListener
import org.tasks.filters.PlaceFilter import org.tasks.filters.PlaceFilter
@ -136,7 +134,6 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
private val repeatConfirmationReceiver = RepeatConfirmationReceiver() private val repeatConfirmationReceiver = RepeatConfirmationReceiver()
@Inject lateinit var syncAdapters: SyncAdapters @Inject lateinit var syncAdapters: SyncAdapters
@Inject lateinit var taskDeleter: TaskDeleter
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@Inject lateinit var dialogBuilder: DialogBuilder @Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var taskCreator: TaskCreator @Inject lateinit var taskCreator: TaskCreator
@ -449,11 +446,28 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
true true
} }
R.id.menu_clear_completed -> { R.id.menu_clear_completed -> {
dialogBuilder lifecycleScope.launch {
.newDialog(R.string.clear_completed_tasks_confirmation) val tasks = listViewModel.getTasksToClear()
.setPositiveButton(R.string.ok) { _, _ -> clearCompleted() } val countString = requireContext().resources.getQuantityString(R.plurals.Ntasks, tasks.size, tasks.size)
.setNegativeButton(R.string.cancel, null) if (tasks.isEmpty()) {
.show() 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 true
} }
R.id.menu_filter_settings -> { 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() { private fun createNewTask() {
lifecycleScope.launch { lifecycleScope.launch {
shortcutManager.reportShortcutUsed(ShortcutManager.SHORTCUT_NEW_TASK) shortcutManager.reportShortcutUsed(ShortcutManager.SHORTCUT_NEW_TASK)
@ -845,7 +854,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
finishActionMode() finishActionMode()
val result = withContext(NonCancellable) { val result = withContext(NonCancellable) {
taskDeleter.markDeleted(tasks) listViewModel.markDeleted(tasks)
} }
result.forEach { onTaskDelete(it) } result.forEach { onTaskDelete(it) }
makeSnackbar(R.string.delete_multiple_tasks_confirmation, result.size.toString())?.show() 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 android.content.Context
import androidx.room.withTransaction import androidx.room.withTransaction
import com.todoroo.astrid.alarms.AlarmService 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.dao.Database
import com.todoroo.astrid.data.Task import com.todoroo.astrid.data.Task
import com.todoroo.astrid.timers.TimerPlugin import com.todoroo.astrid.timers.TimerPlugin
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.withContext
import org.tasks.BuildConfig import org.tasks.BuildConfig
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.caldav.VtodoCache import org.tasks.caldav.VtodoCache
@ -16,10 +16,8 @@ import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar import org.tasks.data.CaldavCalendar
import org.tasks.data.DeletionDao import org.tasks.data.DeletionDao
import org.tasks.data.LocationDao import org.tasks.data.LocationDao
import org.tasks.data.TaskContainer
import org.tasks.data.TaskDao import org.tasks.data.TaskDao
import org.tasks.data.UserActivityDao import org.tasks.data.UserActivityDao
import org.tasks.db.QueryUtils
import org.tasks.db.SuspendDbUtils.chunkedMap import org.tasks.db.SuspendDbUtils.chunkedMap
import org.tasks.files.FileHelper import org.tasks.files.FileHelper
import org.tasks.location.GeofenceApi 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(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 val ids = taskIds
.toSet() .toSet()
.plus(taskIds.chunkedMap(taskDao::getChildren)) .plus(taskIds.chunkedMap(taskDao::getChildren))
@ -60,21 +58,7 @@ class TaskDeleter @Inject constructor(
} }
syncAdapters.sync() syncAdapters.sync()
localBroadcastManager.broadcastRefresh() localBroadcastManager.broadcastRefresh()
return taskDao.fetch(ids) 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
} }
suspend fun delete(task: Task) = delete(task.id) suspend fun delete(task: Task) = delete(task.id)

@ -8,8 +8,11 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.todoroo.andlib.utility.DateUtilities import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.api.Filter import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.FilterImpl
import com.todoroo.astrid.api.SearchFilter import com.todoroo.astrid.api.SearchFilter
import com.todoroo.astrid.core.BuiltInFilterExposer 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.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -30,9 +33,11 @@ import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory import org.tasks.billing.Inventory
import org.tasks.billing.PurchaseActivity import org.tasks.billing.PurchaseActivity
import org.tasks.compose.throttleLatest import org.tasks.compose.throttleLatest
import org.tasks.data.DeletionDao
import org.tasks.data.TaskContainer import org.tasks.data.TaskContainer
import org.tasks.data.TaskDao import org.tasks.data.TaskDao
import org.tasks.data.TaskListQuery.getQuery import org.tasks.data.TaskListQuery.getQuery
import org.tasks.db.QueryUtils
import org.tasks.extensions.Context.openUri import org.tasks.extensions.Context.openUri
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import javax.inject.Inject import javax.inject.Inject
@ -43,6 +48,8 @@ class TaskListViewModel @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val preferences: Preferences, private val preferences: Preferences,
private val taskDao: TaskDao, private val taskDao: TaskDao,
private val taskDeleter: TaskDeleter,
private val deletionDao: DeletionDao,
private val localBroadcastManager: LocalBroadcastManager, private val localBroadcastManager: LocalBroadcastManager,
private val inventory: Inventory, private val inventory: Inventory,
private val firebase: Firebase, 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 { init {
localBroadcastManager.registerRefreshReceiver(refreshReceiver) 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_below_title">Below title</string>
<string name="widget_due_date_hidden">Hidden</string> <string name="widget_due_date_hidden">Hidden</string>
<string name="clear_completed_tasks_confirmation">Clear completed tasks?</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="copy_multiple_tasks_confirmation">%s copied</string>
<string name="delete_multiple_tasks_confirmation">%s deleted</string> <string name="delete_multiple_tasks_confirmation">%s deleted</string>
<string name="delete_selected_tasks">Delete selected tasks?</string> <string name="delete_selected_tasks">Delete selected tasks?</string>

Loading…
Cancel
Save