Batch undo for completion snackbar

pull/1762/head
Alex Baker 2 years ago
parent 31797e2e9d
commit 7ddc681bf2

@ -12,7 +12,12 @@ import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.speech.RecognizerIntent
import android.view.*
import android.view.Gravity
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
@ -39,22 +44,40 @@ import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.adapter.TaskAdapter
import com.todoroo.astrid.adapter.TaskAdapterProvider
import com.todoroo.astrid.api.*
import com.todoroo.astrid.api.AstridApiConstants.EXTRAS_OLD_DUE_DATE
import com.todoroo.astrid.api.AstridApiConstants.EXTRAS_TASK_ID
import com.todoroo.astrid.api.CaldavFilter
import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.GtasksFilter
import com.todoroo.astrid.api.IdListFilter
import com.todoroo.astrid.api.SearchFilter
import com.todoroo.astrid.api.TagFilter
import com.todoroo.astrid.core.BuiltInFilterExposer
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.repeats.RepeatTaskHelper
import com.todoroo.astrid.service.*
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
import com.todoroo.astrid.utility.Flags
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.ShortcutManager
import org.tasks.activities.*
import org.tasks.activities.FilterSettingsActivity
import org.tasks.activities.GoogleTaskListSettingsActivity
import org.tasks.activities.ListPicker
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.caldav.BaseCaldavCalendarSettingsActivity
import org.tasks.data.CaldavDao
@ -76,12 +99,15 @@ import org.tasks.preferences.Device
import org.tasks.preferences.Preferences
import org.tasks.sync.SyncAdapters
import org.tasks.tags.TagPickerActivity
import org.tasks.tasklist.*
import org.tasks.tasklist.DragAndDropRecyclerAdapter
import org.tasks.tasklist.PagedListRecyclerAdapter
import org.tasks.tasklist.TaskListRecyclerAdapter
import org.tasks.tasklist.TaskViewHolder
import org.tasks.tasklist.ViewHolderFactory
import org.tasks.themes.ColorProvider
import org.tasks.themes.ThemeColor
import org.tasks.ui.TaskListViewModel
import java.time.format.FormatStyle
import java.util.*
import javax.inject.Inject
import kotlin.math.max
@ -859,35 +885,46 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
private inner class RepeatConfirmationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val taskId = intent.getLongExtra(AstridApiConstants.EXTRAS_TASK_ID, 0)
if (taskId > 0) {
lifecycleScope.launch {
val task = taskDao.fetch(taskId) ?: return@launch
try {
if (task.isRecurring && !task.isCompleted) {
val oldDueDate = intent.getLongExtra(AstridApiConstants.EXTRAS_OLD_DUE_DATE, 0)
val newDueDate = intent.getLongExtra(AstridApiConstants.EXTRAS_NEW_DUE_DATE, 0)
val dueDateString = DateUtilities.getRelativeDateTime(
context, newDueDate, locale.locale, FormatStyle.LONG, true)
makeSnackbar(R.string.repeat_snackbar, task.title, dueDateString)
?.setAction(R.string.DLG_undo) {
lifecycleScope.launch(NonCancellable) {
repeatTaskHelper.undoRepeat(task, oldDueDate, newDueDate)
}
}
?.show()
} else {
makeSnackbar(R.string.snackbar_task_completed)
?.setAction(R.string.DLG_undo) {
lifecycleScope.launch(NonCancellable) {
taskCompleter.setComplete(task, false)
}
}
?.show()
}
} catch (e: Exception) {
firebase.reportException(e)
lifecycleScope.launch {
val tasks =
(intent.getSerializableExtra(EXTRAS_TASK_ID) as? ArrayList<Long>)
?.let { taskDao.fetch(it) }
?.takeIf { it.isNotEmpty() }
?: return@launch
val isRecurringCompletion =
tasks.size == 1 && tasks.first().let { it.isRecurring && !it.isCompleted }
val oldDueDate = if (isRecurringCompletion) {
intent.getLongExtra(EXTRAS_OLD_DUE_DATE, 0)
} else {
0
}
val undoCompletion = View.OnClickListener {
lifecycleScope.launch {
tasks
.partition { it.isRecurring }
.let { (recurring, notRecurring) ->
recurring.forEach { repeatTaskHelper.undoRepeat(it, oldDueDate) }
taskCompleter.setComplete(notRecurring, 0L)
}
}
}
if (isRecurringCompletion) {
val task = tasks.first()
val text = getString(
R.string.repeat_snackbar,
task.title,
DateUtilities.getRelativeDateTime(
context, task.dueDate, locale.locale, FormatStyle.LONG, true
)
)
makeSnackbar(text)?.setAction(R.string.DLG_undo, undoCompletion)?.show()
} else {
val text = if (tasks.size == 1) {
context.getString(R.string.snackbar_task_completed)
} else {
context.getString(R.string.snackbar_tasks_completed, tasks.size)
}
makeSnackbar(text)?.setAction(R.string.DLG_undo, undoCompletion)?.show()
}
}
}

@ -22,17 +22,7 @@ class AlarmService @Inject constructor(
private val alarmDao: AlarmDao,
private val jobs: NotificationQueue) {
suspend fun rescheduleAlarms(taskId: Long, oldDueDate: Long, newDueDate: Long) {
if (oldDueDate <= 0 || newDueDate <= 0) {
return
}
getAlarms(taskId)
.takeIf { it.isNotEmpty() }
?.onEach { it.time += newDueDate - oldDueDate }
?.let { synchronizeAlarms(taskId, it.toMutableSet()) }
}
private suspend fun getAlarms(taskId: Long): List<Alarm> = alarmDao.getAlarms(taskId)
suspend fun getAlarms(taskId: Long): List<Alarm> = alarmDao.getAlarms(taskId)
/**
* Save the given array of alarms into the database
@ -94,4 +84,4 @@ class AlarmService @Inject constructor(
companion object {
private const val NO_ALARM = Long.MAX_VALUE
}
}
}

@ -21,7 +21,4 @@ public class AstridApiConstants {
/** Extras name for old task due date */
public static final String EXTRAS_OLD_DUE_DATE = "oldDueDate";
/** Extras name for new task due date */
public static final String EXTRAS_NEW_DUE_DATE = "newDueDate";
}

@ -19,7 +19,6 @@ import org.tasks.data.TaskContainer
import org.tasks.data.TaskDao
import org.tasks.date.DateTimeUtils.isAfterNow
import org.tasks.db.SuspendDbUtils.eachChunk
import org.tasks.jobs.WorkManager
import org.tasks.location.GeofenceApi
import org.tasks.notifications.NotificationManager
import org.tasks.preferences.Preferences
@ -28,7 +27,6 @@ import org.tasks.sync.SyncAdapters
import javax.inject.Inject
class TaskDao @Inject constructor(
private val workManager: WorkManager,
private val taskDao: TaskDao,
private val reminderService: ReminderService,
private val refreshScheduler: RefreshScheduler,
@ -100,10 +98,10 @@ class TaskDao @Inject constructor(
*/
suspend fun save(task: Task) = save(task, fetch(task.id))
suspend fun saved(original: Task, suppressRefresh: Boolean) =
suspend fun saved(original: Task) =
fetch(original.id)?.let {
afterUpdate(
it.apply { if (suppressRefresh) suppressRefresh() },
it.apply { if (original.isSuppressRefresh()) suppressRefresh() },
original
)
}
@ -119,11 +117,6 @@ class TaskDao @Inject constructor(
val deletionDateModified = task.deletionDate != original?.deletionDate ?: 0
val justCompleted = completionDateModified && task.isCompleted
val justDeleted = deletionDateModified && task.isDeleted
if (justCompleted && task.isRecurring) {
workManager.scheduleRepeat(task)
} else if (!task.calendarURI.isNullOrBlank()) {
workManager.updateCalendar(task)
}
coroutineScope {
launch(Dispatchers.Default) {
if (justCompleted || justDeleted) {
@ -143,13 +136,10 @@ class TaskDao @Inject constructor(
}
reminderService.scheduleAlarm(task)
refreshScheduler.scheduleRefresh(task)
if (!task.checkTransitory(Task.TRANS_SUPPRESS_REFRESH)) {
if (!task.isSuppressRefresh()) {
localBroadcastManager.broadcastRefresh()
}
syncAdapters.sync(task, original)
if (justCompleted && !task.isRecurring) {
localBroadcastManager.broadcastTaskCompleted(task.id, 0L, 0L)
}
}
}
}

@ -370,6 +370,8 @@ class Task : Parcelable {
putTransitory(TRANS_SUPPRESS_REFRESH, true)
}
fun isSuppressRefresh() = checkTransitory(TRANS_SUPPRESS_REFRESH)
@Synchronized
fun putTransitory(key: String, value: Any) {
if (transitoryData == null) {
@ -555,7 +557,7 @@ class Task : Parcelable {
const val URGENCY_NEXT_WEEK = 4
const val URGENCY_IN_TWO_WEEKS = 5
const val TRANS_SUPPRESS_REFRESH = "suppress-refresh"
private const val TRANS_SUPPRESS_REFRESH = "suppress-refresh"
private val INVALID_COUNT = ";?COUNT=-1".toRegex()
@ -620,4 +622,4 @@ class Task : Parcelable {
fun String?.withoutFrom(): String? = this?.replace(";?FROM=[^;]*".toRegex(), "")
}
}
}

@ -44,7 +44,7 @@ class RepeatTaskHelper @Inject constructor(
rrule = initRRule(recurrence)
count = rrule.count
if (count == 1) {
localBroadcastManager.broadcastTaskCompleted(task.id, 0, 0)
broadcastCompletion(task)
return
}
newDueDate = computeNextDueDate(task, recurrence, repeatAfterCompletion)
@ -58,7 +58,7 @@ class RepeatTaskHelper @Inject constructor(
val oldDueDate = task.dueDate
val repeatUntil = task.repeatUntil
if (repeatFinished(newDueDate, repeatUntil)) {
localBroadcastManager.broadcastTaskCompleted(task.id, 0, 0)
broadcastCompletion(task)
return
}
if (count > 1) {
@ -75,13 +75,18 @@ class RepeatTaskHelper @Inject constructor(
oldDueDate
.takeIf { it > 0 }
?: newDueDate - (computeNextDueDate(task, recurrence, repeatAfterCompletion) - newDueDate)
alarmService.rescheduleAlarms(task.id, previousDueDate, newDueDate)
rescheduleAlarms(task.id, previousDueDate, newDueDate)
taskCompleter.setComplete(task, false)
localBroadcastManager.broadcastTaskCompleted(task.id, previousDueDate, newDueDate)
broadcastCompletion(task, previousDueDate)
}
suspend fun undoRepeat(task: Task, oldDueDate: Long, newDueDate: Long) {
task.setDueDateAdjustingHideUntil(oldDueDate)
private fun broadcastCompletion(task: Task, oldDueDate: Long = 0L) {
if (!task.isSuppressRefresh()) {
localBroadcastManager.broadcastTaskCompleted(task.id, oldDueDate)
}
}
suspend fun undoRepeat(task: Task, oldDueDate: Long) {
task.completionDate = 0L
try {
val recur = newRecur(task.recurrence!!)
@ -90,13 +95,31 @@ class RepeatTaskHelper @Inject constructor(
recur.count = count + 1
}
task.setRecurrence(recur.toString(), task.repeatAfterCompletion())
val newDueDate = task.dueDate
task.setDueDateAdjustingHideUntil(
if (oldDueDate > 0) {
oldDueDate
} else {
newDueDate - (computeNextDueDate(task, task.recurrence!!, false) - newDueDate)
}
)
rescheduleAlarms(task.id, newDueDate, task.dueDate)
} catch (e: ParseException) {
Timber.e(e)
}
alarmService.rescheduleAlarms(task.id, newDueDate, oldDueDate)
taskDao.save(task)
}
private suspend fun rescheduleAlarms(taskId: Long, oldDueDate: Long, newDueDate: Long) {
if (oldDueDate <= 0 || newDueDate <= 0) {
return
}
alarmService.getAlarms(taskId)
.takeIf { it.isNotEmpty() }
?.onEach { it.time += newDueDate - oldDueDate }
?.let { alarmService.synchronizeAlarms(taskId, it.toMutableSet()) }
}
companion object {
private val weekdayCompare = Comparator { object1: WeekDay, object2: WeekDay -> WeekDay.getCalendarDay(object1) - WeekDay.getCalendarDay(object2) }
private fun repeatFinished(newDueDate: Long, repeatUntil: Long): Boolean {

@ -8,7 +8,9 @@ import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import dagger.hilt.android.qualifiers.ApplicationContext
import org.tasks.LocalBroadcastManager
import org.tasks.data.GoogleTaskDao
import org.tasks.jobs.WorkManager
import org.tasks.preferences.Preferences
import timber.log.Timber
import javax.inject.Inject
@ -19,6 +21,8 @@ class TaskCompleter @Inject internal constructor(
private val googleTaskDao: GoogleTaskDao,
private val preferences: Preferences,
private val notificationManager: NotificationManager,
private val localBroadcastManager: LocalBroadcastManager,
private val workManager: WorkManager,
) {
suspend fun setComplete(taskId: Long) =
taskDao
@ -26,41 +30,50 @@ class TaskCompleter @Inject internal constructor(
?.let { setComplete(it, true) }
?: Timber.e("Could not find task $taskId")
suspend fun setComplete(item: Task, completed: Boolean) {
suspend fun setComplete(item: Task, completed: Boolean, includeChildren: Boolean = true) {
val completionDate = if (completed) DateUtilities.now() else 0L
googleTaskDao
.getChildTasks(item.id)
.let {
if (completed) {
it
} else {
it
.plus(googleTaskDao.getParentTask(item.id))
.plus(taskDao.getParents(item.id).mapNotNull { ids -> taskDao.fetch(ids) })
ArrayList<Task?>()
.apply {
if (includeChildren) {
addAll(googleTaskDao.getChildTasks(item.id))
addAll(taskDao.getChildren(item.id).let { taskDao.fetch(it) })
}
if (!completed) {
add(googleTaskDao.getParentTask(item.id))
addAll(taskDao.getParents(item.id).let { taskDao.fetch(it) })
}
add(item)
}
.plus(
taskDao.getChildren(item.id)
.takeIf { it.isNotEmpty() }
?.let { taskDao.fetch(it) }
?: emptyList()
)
.plus(listOf(item))
.filterNotNull()
.filter { it.isCompleted != completionDate > 0 }
.let { setComplete(it, completionDate) }
.let {
setComplete(it, completionDate)
if (completed && !item.isRecurring) {
localBroadcastManager.broadcastTaskCompleted(ArrayList(it.map(Task::id)))
}
}
}
private suspend fun setComplete(tasks: List<Task>, completionDate: Long) {
suspend fun setComplete(tasks: List<Task>, completionDate: Long) {
if (tasks.isEmpty()) {
return
}
val completed = completionDate > 0
taskDao.setCompletionDate(tasks.mapNotNull { it.remoteId }, completionDate)
tasks.forEachIndexed { i, task ->
taskDao.saved(task, i < tasks.size - 1)
tasks.forEachIndexed { i, original ->
if (i < tasks.size - 1) {
original.suppressRefresh()
}
taskDao.saved(original)
}
tasks.forEach {
if (completed && it.isRecurring) {
workManager.scheduleRepeat(it)
} else if (!it.calendarURI.isNullOrBlank()) {
workManager.updateCalendar(it)
}
}
if (
tasks.isNotEmpty() &&
completionDate > 0 &&
notificationManager.currentInterruptionFilter == INTERRUPTION_FILTER_ALL
) {
if (completed && notificationManager.currentInterruptionFilter == INTERRUPTION_FILTER_ALL) {
preferences
.completionSound
?.takeUnless { preferences.isCurrentlyQuietHours }

@ -1,14 +1,22 @@
package org.tasks;
import static com.google.common.collect.Lists.newArrayList;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import com.todoroo.astrid.api.AstridApiConstants;
import dagger.hilt.android.qualifiers.ApplicationContext;
import javax.inject.Inject;
import org.tasks.widget.AppWidgetManager;
import java.util.ArrayList;
import javax.inject.Inject;
import dagger.hilt.android.qualifiers.ApplicationContext;
public class LocalBroadcastManager {
public static final String REFRESH = BuildConfig.APPLICATION_ID + ".REFRESH";
@ -63,11 +71,18 @@ public class LocalBroadcastManager {
localBroadcastManager.sendBroadcast(new Intent(REFRESH_PREFERENCES));
}
public void broadcastTaskCompleted(long id, long oldDueDate, long newDueDate) {
public void broadcastTaskCompleted(long id, long oldDueDate) {
broadcastTaskCompleted(newArrayList(id), oldDueDate);
}
public void broadcastTaskCompleted(ArrayList<Long> id) {
broadcastTaskCompleted(id, 0);
}
private void broadcastTaskCompleted(ArrayList<Long> id, long oldDueDate) {
Intent intent = new Intent(TASK_COMPLETED);
intent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, id);
intent.putExtra(AstridApiConstants.EXTRAS_OLD_DUE_DATE, oldDueDate);
intent.putExtra(AstridApiConstants.EXTRAS_NEW_DUE_DATE, newDueDate);
localBroadcastManager.sendBroadcast(intent);
}

@ -26,7 +26,9 @@ class AfterSaveWork @AssistedInject constructor(
override suspend fun run(): Result {
val taskId = inputData.getLong(EXTRA_ID, -1)
val task = taskDao.fetch(taskId) ?: return Result.failure()
if (inputData.getBoolean(EXTRA_SUPPRESS_COMPLETION_SNACKBAR, false)) {
task.suppressRefresh()
}
gCalHelper.updateEvent(task)
if (caldavDao.getAccountForTask(taskId)?.isSuppressRepeatingTasks != true) {
@ -37,5 +39,6 @@ class AfterSaveWork @AssistedInject constructor(
companion object {
const val EXTRA_ID = "extra_id"
const val EXTRA_SUPPRESS_COMPLETION_SNACKBAR = "extra_suppress_snackbar"
}
}

@ -6,9 +6,18 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.work.*
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ExistingWorkPolicy.APPEND_OR_REPLACE
import androidx.work.ExistingWorkPolicy.REPLACE
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequest
import androidx.work.PeriodicWorkRequest
import androidx.work.WorkContinuation
import androidx.work.WorkInfo
import androidx.work.WorkRequest
import androidx.work.Worker
import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.data.Task
@ -16,11 +25,15 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.tasks.BuildConfig
import org.tasks.R
import org.tasks.data.*
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavAccount.Companion.TYPE_CALDAV
import org.tasks.data.CaldavAccount.Companion.TYPE_ETEBASE
import org.tasks.data.CaldavAccount.Companion.TYPE_OPENTASKS
import org.tasks.data.CaldavAccount.Companion.TYPE_TASKS
import org.tasks.data.CaldavDao
import org.tasks.data.GoogleTaskListDao
import org.tasks.data.OpenTaskDao
import org.tasks.data.Place
import org.tasks.date.DateTimeUtils.midnight
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.jobs.MigrateLocalWork.Companion.EXTRA_ACCOUNT
@ -66,6 +79,10 @@ class WorkManagerImpl constructor(
OneTimeWorkRequest.Builder(AfterSaveWork::class.java)
.setInputData(Data.Builder()
.putLong(AfterSaveWork.EXTRA_ID, task.id)
.putBoolean(
AfterSaveWork.EXTRA_SUPPRESS_COMPLETION_SNACKBAR,
task.isSuppressRefresh()
)
.build()))
}

@ -714,4 +714,5 @@ File %1$s contained %2$s.\n\n
<string name="completed">Completed</string>
<string name="snackbar_task_completed">Task completed</string>
<string name="completed_tasks_at_bottom">Move completed tasks to bottom</string>
<string name="snackbar_tasks_completed">%d tasks completed</string>
</resources>

Loading…
Cancel
Save