Read-only support

pull/2075/head
Alex Baker 3 years ago
parent 644eda1eef
commit 71f22dd05d

@ -164,7 +164,7 @@ dependencies {
implementation("com.github.bitfireAT:dav4jvm:2.2") { implementation("com.github.bitfireAT:dav4jvm:2.2") {
exclude(group = "junit") exclude(group = "junit")
} }
implementation("com.github.tasks:ical4android:2fb465b") { implementation("com.github.tasks:ical4android:ce7919d") {
exclude(group = "commons-logging") exclude(group = "commons-logging")
exclude(group = "org.json", module = "json") exclude(group = "org.json", module = "json")
exclude(group = "org.codehaus.groovy", module = "groovy") exclude(group = "org.codehaus.groovy", module = "groovy")

File diff suppressed because it is too large Load Diff

@ -7,6 +7,7 @@ import at.bitfire.ical4android.Task
import com.todoroo.astrid.helper.UUIDHelper import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import org.dmfs.tasks.contract.TaskContract import org.dmfs.tasks.contract.TaskContract
import org.dmfs.tasks.contract.TaskContract.TaskListColumns.ACCESS_LEVEL_OWNER
import org.tasks.caldav.iCalendar import org.tasks.caldav.iCalendar
import org.tasks.data.CaldavCalendar import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavDao import org.tasks.data.CaldavDao
@ -23,6 +24,7 @@ class TestOpenTaskDao @Inject constructor(
type: String = DEFAULT_TYPE, type: String = DEFAULT_TYPE,
account: String = DEFAULT_ACCOUNT, account: String = DEFAULT_ACCOUNT,
url: String = UUIDHelper.newUUID(), url: String = UUIDHelper.newUUID(),
accessLevel: Int = ACCESS_LEVEL_OWNER,
): Pair<Long, CaldavCalendar> { ): Pair<Long, CaldavCalendar> {
val uri = taskLists.buildUpon() val uri = taskLists.buildUpon()
.appendQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER, "true") .appendQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER, "true")
@ -34,6 +36,7 @@ class TestOpenTaskDao @Inject constructor(
.withValue(TaskContract.CommonSyncColumns._SYNC_ID, url) .withValue(TaskContract.CommonSyncColumns._SYNC_ID, url)
.withValue(TaskContract.TaskListColumns.LIST_NAME, name) .withValue(TaskContract.TaskListColumns.LIST_NAME, name)
.withValue(TaskContract.TaskLists.SYNC_ENABLED, "1") .withValue(TaskContract.TaskLists.SYNC_ENABLED, "1")
.withValue(TaskContract.TaskLists.ACCESS_LEVEL, accessLevel)
) )
val calendar = CaldavCalendar( val calendar = CaldavCalendar(
uuid = UUIDHelper.newUUID(), uuid = UUIDHelper.newUUID(),

@ -18,17 +18,16 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.Divider import androidx.compose.material.Divider
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidViewBinding import androidx.compose.ui.viewinterop.AndroidViewBinding
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
@ -126,26 +125,34 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
val view: View = binding.root val view: View = binding.root
val model = editViewModel.task val model = editViewModel.task
val toolbar = binding.toolbar val toolbar = binding.toolbar
toolbar.navigationIcon = context.getDrawable(R.drawable.ic_outline_save_24px) toolbar.navigationIcon = AppCompatResources.getDrawable(
context,
if (editViewModel.isReadOnly)
R.drawable.ic_outline_arrow_back_24px
else
R.drawable.ic_outline_save_24px
)
toolbar.setNavigationOnClickListener { toolbar.setNavigationOnClickListener {
lifecycleScope.launch { lifecycleScope.launch {
save() save()
} }
} }
val backButtonSavesTask = preferences.backButtonSavesTask() val backButtonSavesTask = preferences.backButtonSavesTask()
toolbar.setNavigationContentDescription(if (backButtonSavesTask) { toolbar.setNavigationContentDescription(
R.string.discard when {
} else { editViewModel.isReadOnly -> R.string.back
R.string.save backButtonSavesTask -> R.string.discard
}) else -> R.string.save
}
)
toolbar.inflateMenu(R.menu.menu_task_edit_fragment) toolbar.inflateMenu(R.menu.menu_task_edit_fragment)
val menu = toolbar.menu val menu = toolbar.menu
val delete = menu.findItem(R.id.menu_delete) val delete = menu.findItem(R.id.menu_delete)
delete.isVisible = !model.isNew delete.isVisible = !model.isNew && editViewModel.isWritable
delete.setShowAsAction( delete.setShowAsAction(
if (backButtonSavesTask) MenuItem.SHOW_AS_ACTION_NEVER else MenuItem.SHOW_AS_ACTION_IF_ROOM) if (backButtonSavesTask) MenuItem.SHOW_AS_ACTION_NEVER else MenuItem.SHOW_AS_ACTION_IF_ROOM)
val discard = menu.findItem(R.id.menu_discard) val discard = menu.findItem(R.id.menu_discard)
discard.isVisible = backButtonSavesTask discard.isVisible = backButtonSavesTask && editViewModel.isWritable
discard.setShowAsAction( discard.setShowAsAction(
if (model.isNew) MenuItem.SHOW_AS_ACTION_IF_ROOM else MenuItem.SHOW_AS_ACTION_NEVER) if (model.isNew) MenuItem.SHOW_AS_ACTION_IF_ROOM else MenuItem.SHOW_AS_ACTION_NEVER)
if (savedInstanceState == null) { if (savedInstanceState == null) {
@ -173,7 +180,11 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
title.setText(model.title) title.setText(model.title)
title.setHorizontallyScrolling(false) title.setHorizontallyScrolling(false)
title.maxLines = 5 title.maxLines = 5
if (model.isNew || preferences.getBoolean(R.string.p_hide_check_button, false)) { if (
model.isNew ||
preferences.getBoolean(R.string.p_hide_check_button, false) ||
editViewModel.isReadOnly
) {
binding.fab.visibility = View.INVISIBLE binding.fab.visibility = View.INVISIBLE
} else if (editViewModel.completed) { } else if (editViewModel.completed) {
title.paintFlags = title.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG title.paintFlags = title.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
@ -211,7 +222,7 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
} }
binding.composeView.setContent { binding.composeView.setContent {
MdcTheme { MdcTheme {
Column { Column(modifier = Modifier.gesturesDisabled(editViewModel.isReadOnly)) {
taskEditControlSetFragmentManager.displayOrder.forEachIndexed { index, tag -> taskEditControlSetFragmentManager.displayOrder.forEachIndexed { index, tag ->
if (index < taskEditControlSetFragmentManager.visibleSize) { if (index < taskEditControlSetFragmentManager.visibleSize) {
// TODO: remove ui-viewbinding library when these are all migrated // TODO: remove ui-viewbinding library when these are all migrated
@ -222,8 +233,12 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
TAG_LIST -> ListRow() TAG_LIST -> ListRow()
TAG_CREATION -> CreationRow() TAG_CREATION -> CreationRow()
CalendarControlSet.TAG -> AndroidViewBinding(TaskEditCalendarBinding::inflate) CalendarControlSet.TAG -> AndroidViewBinding(TaskEditCalendarBinding::inflate)
StartDateControlSet.TAG -> AndroidViewBinding(TaskEditStartDateBinding::inflate) StartDateControlSet.TAG -> AndroidViewBinding(
ReminderControlSet.TAG -> AndroidViewBinding(TaskEditRemindersBinding::inflate) TaskEditStartDateBinding::inflate
)
ReminderControlSet.TAG -> AndroidViewBinding(
TaskEditRemindersBinding::inflate
)
LocationControlSet.TAG -> AndroidViewBinding(TaskEditLocationBinding::inflate) LocationControlSet.TAG -> AndroidViewBinding(TaskEditLocationBinding::inflate)
FilesControlSet.TAG -> AndroidViewBinding(TaskEditFilesBinding::inflate) FilesControlSet.TAG -> AndroidViewBinding(TaskEditFilesBinding::inflate)
TimerControlSet.TAG -> AndroidViewBinding(TaskEditTimerBinding::inflate) TimerControlSet.TAG -> AndroidViewBinding(TaskEditTimerBinding::inflate)
@ -496,7 +511,8 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
const val EXTRA_TAGS = "extra_tags" const val EXTRA_TAGS = "extra_tags"
const val EXTRA_ALARMS = "extra_alarms" const val EXTRA_ALARMS = "extra_alarms"
private const val FRAG_TAG_GOOGLE_TASK_LIST_SELECTION = "frag_tag_google_task_list_selection" private const val FRAG_TAG_GOOGLE_TASK_LIST_SELECTION =
"frag_tag_google_task_list_selection"
const val FRAG_TAG_CALENDAR_PICKER = "frag_tag_calendar_picker" const val FRAG_TAG_CALENDAR_PICKER = "frag_tag_calendar_picker"
private const val FRAG_TAG_DATE_PICKER = "frag_tag_date_picker" private const val FRAG_TAG_DATE_PICKER = "frag_tag_date_picker"
const val REQUEST_CODE_PICK_CALENDAR = 70 const val REQUEST_CODE_PICK_CALENDAR = 70
@ -510,11 +526,11 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
} }
fun newTaskEditFragment( fun newTaskEditFragment(
task: Task, task: Task,
list: Filter, list: Filter,
location: Location?, location: Location?,
tags: ArrayList<TagData>, tags: ArrayList<TagData>,
alarms: ArrayList<Alarm>, alarms: ArrayList<Alarm>,
): TaskEditFragment { ): TaskEditFragment {
val taskEditFragment = TaskEditFragment() val taskEditFragment = TaskEditFragment()
val arguments = Bundle() val arguments = Bundle()
@ -526,5 +542,21 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
taskEditFragment.arguments = arguments taskEditFragment.arguments = arguments
return taskEditFragment return taskEditFragment
} }
fun Modifier.gesturesDisabled(disabled: Boolean = true) =
if (disabled) {
pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
awaitPointerEvent(pass = PointerEventPass.Initial)
.changes
.filter { it.position == it.previousPosition }
.forEach { it.consume() }
}
}
}
} else {
this
}
} }
} }

@ -243,14 +243,15 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentTaskListBinding.inflate(inflater, container, false) binding = FragmentTaskListBinding.inflate(inflater, container, false)
filter = getFilter()
with (binding) { with (binding) {
swipeRefreshLayout = bodyStandard.swipeLayout swipeRefreshLayout = bodyStandard.swipeLayout
emptyRefreshLayout = bodyEmpty.swipeLayoutEmpty emptyRefreshLayout = bodyEmpty.swipeLayoutEmpty
coordinatorLayout = taskListCoordinator coordinatorLayout = taskListCoordinator
recyclerView = bodyStandard.recyclerView recyclerView = bodyStandard.recyclerView
fab.setOnClickListener { createNewTask() } fab.setOnClickListener { createNewTask() }
fab.isVisible = filter.isWritable
} }
filter = getFilter()
themeColor = if (filter.tint != 0) colorProvider.getThemeColor(filter.tint, true) else defaultThemeColor themeColor = if (filter.tint != 0) colorProvider.getThemeColor(filter.tint, true) else defaultThemeColor
filter.setFilterQueryOverride(null) filter.setFilterQueryOverride(null)
@ -373,8 +374,9 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
menu.findItem(R.id.menu_collapse_subtasks).isVisible = false menu.findItem(R.id.menu_collapse_subtasks).isVisible = false
menu.findItem(R.id.menu_expand_subtasks).isVisible = false menu.findItem(R.id.menu_expand_subtasks).isVisible = false
} }
menu.findItem(R.id.menu_voice_add).isVisible = device.voiceInputAvailable() menu.findItem(R.id.menu_voice_add).isVisible = device.voiceInputAvailable() && filter.isWritable
search = binding.toolbar.menu.findItem(R.id.menu_search).setOnActionExpandListener(this) search = binding.toolbar.menu.findItem(R.id.menu_search).setOnActionExpandListener(this)
menu.findItem(R.id.menu_clear_completed).isVisible = filter.isWritable
} }
private fun openFilter(filter: Filter?) { private fun openFilter(filter: Filter?) {
@ -633,10 +635,12 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
REQUEST_TAG_TASKS -> if (resultCode == Activity.RESULT_OK) { REQUEST_TAG_TASKS -> if (resultCode == Activity.RESULT_OK) {
lifecycleScope.launch { lifecycleScope.launch {
val modified = tagDataDao.applyTags( val modified = tagDataDao.applyTags(
taskDao.fetch( taskDao
data!!.getSerializableExtra(TagPickerActivity.EXTRA_TASKS) as ArrayList<Long>), .fetch(data!!.getSerializableExtra(TagPickerActivity.EXTRA_TASKS) as ArrayList<Long>)
.filterNot { it.readOnly },
data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_PARTIALLY_SELECTED)!!, data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_PARTIALLY_SELECTED)!!,
data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED)!!) data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED)!!
)
taskDao.touch(modified) taskDao.touch(modified)
} }
finishActionMode() finishActionMode()
@ -693,6 +697,10 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
override fun onCreateActionMode(actionMode: ActionMode, menu: Menu): Boolean { override fun onCreateActionMode(actionMode: ActionMode, menu: Menu): Boolean {
val inflater = actionMode.menuInflater val inflater = actionMode.menuInflater
inflater.inflate(R.menu.menu_multi_select, menu) inflater.inflate(R.menu.menu_multi_select, menu)
if (filter.isReadOnly) {
listOf(R.id.edit_tags, R.id.move_tasks, R.id.reschedule, R.id.copy_tasks, R.id.delete)
.forEach { menu.findItem(it).isVisible = false }
}
return true return true
} }
@ -729,6 +737,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
lifecycleScope.launch { lifecycleScope.launch {
taskDao taskDao
.fetch(selected) .fetch(selected)
.filterNot { it.readOnly }
.takeIf { it.isNotEmpty() } .takeIf { it.isNotEmpty() }
?.let { ?.let {
newDateTimePicker( newDateTimePicker(
@ -800,7 +809,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
} }
} }
fun showDateTimePicker(task: TaskContainer) { private fun showDateTimePicker(task: TaskContainer) {
val fragmentManager = parentFragmentManager val fragmentManager = parentFragmentManager
if (fragmentManager.findFragmentByTag(FRAG_TAG_DATE_TIME_PICKER) == null) { if (fragmentManager.findFragmentByTag(FRAG_TAG_DATE_TIME_PICKER) == null) {
newDateTimePicker( newDateTimePicker(
@ -859,6 +868,9 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
fun clearCollapsed() = taskAdapter.clearCollapsed() fun clearCollapsed() = taskAdapter.clearCollapsed()
override fun onCompletedTask(task: TaskContainer, newState: Boolean) { override fun onCompletedTask(task: TaskContainer, newState: Boolean) {
if (task.isReadOnly) {
return
}
lifecycleScope.launch { lifecycleScope.launch {
taskCompleter.setComplete(task.getTask(), newState) taskCompleter.setComplete(task.getTask(), newState)
taskAdapter.onCompletedTask(task, newState) taskAdapter.onCompletedTask(task, newState)
@ -900,6 +912,9 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
} }
override fun onChangeDueDate(task: TaskContainer) { override fun onChangeDueDate(task: TaskContainer) {
if (task.isReadOnly) {
return
}
showDateTimePicker(task) showDateTimePicker(task)
} }
@ -921,6 +936,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
val tasks = val tasks =
(intent.getSerializableExtra(EXTRAS_TASK_ID) as? ArrayList<Long>) (intent.getSerializableExtra(EXTRAS_TASK_ID) as? ArrayList<Long>)
?.let { taskDao.fetch(it) } ?.let { taskDao.fetch(it) }
?.filterNot { it.readOnly }
?.takeIf { it.isNotEmpty() } ?.takeIf { it.isNotEmpty() }
?: return@launch ?: return@launch
val isRecurringCompletion = val isRecurringCompletion =

@ -2,19 +2,23 @@ package com.todoroo.astrid.api;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.todoroo.andlib.sql.Criterion; import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Join; import com.todoroo.andlib.sql.Join;
import com.todoroo.andlib.sql.QueryTemplate; import com.todoroo.andlib.sql.QueryTemplate;
import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.Task;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.tasks.R; import org.tasks.R;
import org.tasks.data.CaldavCalendar; import org.tasks.data.CaldavCalendar;
import org.tasks.data.CaldavTask; import org.tasks.data.CaldavTask;
import org.tasks.data.TaskDao; import org.tasks.data.TaskDao;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class CaldavFilter extends Filter { public class CaldavFilter extends Filter {
/** Parcelable Creator Object */ /** Parcelable Creator Object */
@ -82,6 +86,11 @@ public class CaldavFilter extends Filter {
return calendar; return calendar;
} }
@Override
public boolean isReadOnly() {
return calendar.getAccess() == CaldavCalendar.ACCESS_READ_ONLY;
}
/** {@inheritDoc} */ /** {@inheritDoc} */
@Override @Override
public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(Parcel dest, int flags) {

@ -196,6 +196,14 @@ public class Filter extends FilterListItem {
return true; return true;
} }
public boolean isWritable() {
return !isReadOnly();
}
public boolean isReadOnly() {
return false;
}
public boolean hasBeginningMenu() { public boolean hasBeginningMenu() {
return getBeginningMenu() != 0; return getBeginningMenu() != 0;
} }

@ -36,7 +36,7 @@ import org.tasks.notifications.NotificationDao
autoMigrations = [ autoMigrations = [
AutoMigration(from = 83, to = 84, spec = Migrations.AutoMigrate83to84::class), AutoMigration(from = 83, to = 84, spec = Migrations.AutoMigrate83to84::class),
], ],
version = 86 version = 87
) )
abstract class Database : RoomDatabase() { abstract class Database : RoomDatabase() {
abstract fun notificationDao(): NotificationDao abstract fun notificationDao(): NotificationDao

@ -103,6 +103,9 @@ class Task : Parcelable {
@Transient @Transient
var parent = 0L var parent = 0L
@ColumnInfo(name = "read_only", defaultValue = "0")
var readOnly: Boolean = false
@Ignore @Ignore
@Transient @Transient
private var transitoryData: HashMap<String, Any>? = null private var transitoryData: HashMap<String, Any>? = null
@ -132,6 +135,7 @@ class Task : Parcelable {
transitoryData = parcel.readHashMap(ContentValues::class.java.classLoader) as HashMap<String, Any>? transitoryData = parcel.readHashMap(ContentValues::class.java.classLoader) as HashMap<String, Any>?
isCollapsed = ParcelCompat.readBoolean(parcel) isCollapsed = ParcelCompat.readBoolean(parcel)
parent = parcel.readLong() parent = parcel.readLong()
readOnly = ParcelCompat.readBoolean(parcel)
} }
var uuid: String var uuid: String
@ -265,6 +269,7 @@ class Task : Parcelable {
dest.writeMap(transitoryData as Map<*, *>?) dest.writeMap(transitoryData as Map<*, *>?)
ParcelCompat.writeBoolean(dest, isCollapsed) ParcelCompat.writeBoolean(dest, isCollapsed)
dest.writeLong(parent) dest.writeLong(parent)
ParcelCompat.writeBoolean(dest, readOnly)
} }
fun insignificantChange(task: Task?): Boolean { fun insignificantChange(task: Task?): Boolean {
@ -408,6 +413,7 @@ class Task : Parcelable {
if (isCollapsed != other.isCollapsed) return false if (isCollapsed != other.isCollapsed) return false
if (parent != other.parent) return false if (parent != other.parent) return false
if (transitoryData != other.transitoryData) return false if (transitoryData != other.transitoryData) return false
if (readOnly != other.readOnly) return false
return true return true
} }
@ -434,11 +440,12 @@ class Task : Parcelable {
result = 31 * result + isCollapsed.hashCode() result = 31 * result + isCollapsed.hashCode()
result = 31 * result + parent.hashCode() result = 31 * result + parent.hashCode()
result = 31 * result + (transitoryData?.hashCode() ?: 0) result = 31 * result + (transitoryData?.hashCode() ?: 0)
result = 31 * result + readOnly.hashCode()
return result return result
} }
override fun toString(): String { override fun toString(): String {
return "Task(id=$id, title=$title, priority=$priority, dueDate=$dueDate, hideUntil=$hideUntil, creationDate=$creationDate, modificationDate=$modificationDate, completionDate=$completionDate, deletionDate=$deletionDate, notes=$notes, estimatedSeconds=$estimatedSeconds, elapsedSeconds=$elapsedSeconds, timerStart=$timerStart, ringFlags=$ringFlags, reminderLast=$reminderLast, recurrence=$recurrence, calendarURI=$calendarURI, remoteId='$remoteId', isCollapsed=$isCollapsed, parent=$parent, transitoryData=$transitoryData)" return "Task(id=$id, title=$title, priority=$priority, dueDate=$dueDate, hideUntil=$hideUntil, creationDate=$creationDate, modificationDate=$modificationDate, completionDate=$completionDate, deletionDate=$deletionDate, notes=$notes, estimatedSeconds=$estimatedSeconds, elapsedSeconds=$elapsedSeconds, timerStart=$timerStart, ringFlags=$ringFlags, reminderLast=$reminderLast, recurrence=$recurrence, calendarURI=$calendarURI, remoteId='$remoteId', isCollapsed=$isCollapsed, parent=$parent, transitoryData=$transitoryData, readOnly=$readOnly)"
} }
@Retention(AnnotationRetention.SOURCE) @Retention(AnnotationRetention.SOURCE)

@ -46,6 +46,7 @@ class TaskCompleter @Inject internal constructor(
} }
.filterNotNull() .filterNotNull()
.filter { it.isCompleted != completionDate > 0 } .filter { it.isCompleted != completionDate > 0 }
.filterNot { it.readOnly }
.let { .let {
setComplete(it, completionDate) setComplete(it, completionDate)
if (completed && !item.isRecurring) { if (completed && !item.isRecurring) {

@ -5,14 +5,7 @@ import com.todoroo.astrid.data.Task
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.caldav.VtodoCache import org.tasks.caldav.VtodoCache
import org.tasks.data.CaldavAccount import org.tasks.data.*
import org.tasks.data.CaldavCalendar
import org.tasks.data.DeletionDao
import org.tasks.data.GoogleTaskAccount
import org.tasks.data.GoogleTaskDao
import org.tasks.data.GoogleTaskList
import org.tasks.data.TaskContainer
import org.tasks.data.TaskDao
import org.tasks.db.QueryUtils import org.tasks.db.QueryUtils
import org.tasks.db.SuspendDbUtils.chunkedMap import org.tasks.db.SuspendDbUtils.chunkedMap
import org.tasks.jobs.WorkManager import org.tasks.jobs.WorkManager
@ -34,14 +27,18 @@ 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> {
val ids: MutableSet<Long> = HashSet(taskIds) val ids = taskIds
ids.addAll(taskIds.chunkedMap(googleTaskDao::getChildren)) .toSet()
ids.addAll(taskIds.chunkedMap(taskDao::getChildren)) .plus(taskIds.chunkedMap(googleTaskDao::getChildren))
.plus(taskIds.chunkedMap(taskDao::getChildren))
.let { taskDao.fetch(it.toList()) }
.filterNot { it.readOnly }
.map { it.id }
deletionDao.markDeleted(ids) deletionDao.markDeleted(ids)
workManager.cleanup(ids) workManager.cleanup(ids)
syncAdapters.sync() syncAdapters.sync()
localBroadcastManager.broadcastRefresh() localBroadcastManager.broadcastRefresh()
return ids.chunkedMap(taskDao::fetch) return taskDao.fetch(ids)
} }
suspend fun clearCompleted(filter: Filter): Int { suspend fun clearCompleted(filter: Filter): Int {
@ -50,6 +47,7 @@ class TaskDeleter @Inject constructor(
QueryUtils.removeOrder(QueryUtils.showHiddenAndCompleted(filter.originalSqlQuery))) QueryUtils.removeOrder(QueryUtils.showHiddenAndCompleted(filter.originalSqlQuery)))
val completed = taskDao.fetchTasks(preferences, deleteFilter) val completed = taskDao.fetchTasks(preferences, deleteFilter)
.filter(TaskContainer::isCompleted) .filter(TaskContainer::isCompleted)
.filterNot(TaskContainer::isReadOnly)
.map(TaskContainer::getId) .map(TaskContainer::getId)
.toMutableList() .toMutableList()
completed.removeAll(deletionDao.hasRecurringAncestors(completed)) completed.removeAll(deletionDao.hasRecurringAncestors(completed))

@ -25,17 +25,16 @@ class TaskDuplicator @Inject constructor(
) { ) {
suspend fun duplicate(taskIds: List<Long>): List<Task> { suspend fun duplicate(taskIds: List<Long>): List<Task> {
val result: MutableList<Task> = ArrayList() return taskIds
val tasks = ArrayList(taskIds) .dbchunk()
taskIds.dbchunk().forEach { .flatMap {
tasks.removeAll(googleTaskDao.getChildren(it)) it.minus(googleTaskDao.getChildren(it).toSet())
tasks.removeAll(taskDao.getChildren(it)) .minus(taskDao.getChildren(it).toSet())
} }
for (task in taskDao.fetch(tasks)) { .let { taskDao.fetch(it) }
result.add(clone(task, task.parent)) .filterNot { it.readOnly }
} .map { clone(it, it.parent) }
localBroadcastManager.broadcastRefresh() .also { localBroadcastManager.broadcastRefresh() }
return result
} }
private suspend fun clone(clone: Task, parentId: Long): Task { private suspend fun clone(clone: Task, parentId: Long): Task {

@ -10,12 +10,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
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
import org.tasks.data.CaldavDao import org.tasks.data.*
import org.tasks.data.CaldavTask
import org.tasks.data.GoogleTask
import org.tasks.data.GoogleTaskDao
import org.tasks.data.GoogleTaskListDao
import org.tasks.data.TaskDao
import org.tasks.db.DbUtils.dbchunk import org.tasks.db.DbUtils.dbchunk
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.sync.SyncAdapters import org.tasks.sync.SyncAdapters
@ -49,19 +44,21 @@ class TaskMover @Inject constructor(
} }
suspend fun move(ids: List<Long>, selectedList: Filter) { suspend fun move(ids: List<Long>, selectedList: Filter) {
val tasks = ArrayList(ids) val tasks = ids
ids.dbchunk().forEach { .dbchunk()
tasks.removeAll(googleTaskDao.getChildren(it)) .flatMap {
tasks.removeAll(taskDao.getChildren(it)) it.minus(googleTaskDao.getChildren(it).toSet())
} .minus(taskDao.getChildren(it).toSet())
taskDao.setParent(0, tasks) }
for (task in taskDao.fetch(tasks)) { .let { taskDao.fetch(it) }
performMove(task, selectedList) .filterNot { it.readOnly }
} val taskIds = tasks.map { it.id }
taskDao.setParent(0, ids.intersect(taskIds.toSet()).toList())
tasks.forEach { performMove(it, selectedList) }
if (selectedList is CaldavFilter) { if (selectedList is CaldavFilter) {
caldavDao.updateParents(selectedList.uuid) caldavDao.updateParents(selectedList.uuid)
} }
tasks.dbchunk().forEach { taskIds.dbchunk().forEach {
taskDao.touch(it) taskDao.touch(it)
} }
localBroadcastManager.broadcastRefresh() localBroadcastManager.broadcastRefresh()

@ -30,6 +30,8 @@ import org.tasks.billing.Inventory
import org.tasks.caldav.iCalendar.Companion.fromVtodo import org.tasks.caldav.iCalendar.Companion.fromVtodo
import org.tasks.caldav.property.* import org.tasks.caldav.property.*
import org.tasks.caldav.property.PropertyUtils.register import org.tasks.caldav.property.PropertyUtils.register
import org.tasks.caldav.property.ShareAccess.Companion.NOT_SHARED
import org.tasks.caldav.property.ShareAccess.Companion.NO_ACCESS
import org.tasks.caldav.property.ShareAccess.Companion.READ import org.tasks.caldav.property.ShareAccess.Companion.READ
import org.tasks.caldav.property.ShareAccess.Companion.READ_WRITE import org.tasks.caldav.property.ShareAccess.Companion.READ_WRITE
import org.tasks.caldav.property.ShareAccess.Companion.SHARED_OWNER import org.tasks.caldav.property.ShareAccess.Companion.SHARED_OWNER
@ -427,9 +429,9 @@ class CaldavSynchronizer @Inject constructor(
get() { get() {
this[ShareAccess::class.java]?.let { this[ShareAccess::class.java]?.let {
return when (it.access) { return when (it.access) {
SHARED_OWNER -> ACCESS_OWNER NOT_SHARED, SHARED_OWNER -> ACCESS_OWNER
READ_WRITE -> ACCESS_READ_WRITE READ_WRITE -> ACCESS_READ_WRITE
READ -> ACCESS_READ_ONLY NO_ACCESS, READ -> ACCESS_READ_ONLY
else -> ACCESS_UNKNOWN else -> ACCESS_UNKNOWN
} }
} }

@ -27,6 +27,7 @@ import org.tasks.caldav.extensions.toVAlarms
import org.tasks.data.* import org.tasks.data.*
import org.tasks.data.Alarm.Companion.TYPE_RANDOM import org.tasks.data.Alarm.Companion.TYPE_RANDOM
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_ONLY
import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.date.DateTimeUtils.toDateTime import org.tasks.date.DateTimeUtils.toDateTime
import org.tasks.date.DateTimeUtils.toLocal import org.tasks.date.DateTimeUtils.toLocal
@ -175,6 +176,7 @@ class iCalendar @Inject constructor(
) { ) {
val task = existing?.task?.let { taskDao.fetch(it) } val task = existing?.task?.let { taskDao.fetch(it) }
?: taskCreator.createWithValues("").apply { ?: taskCreator.createWithValues("").apply {
readOnly = calendar.access == ACCESS_READ_ONLY
taskDao.createNew(this) taskDao.createNew(this)
existing?.task = id existing?.task = id
} }

@ -16,6 +16,7 @@ data class ShareAccess(val access: Property.Name): Property {
val SHARED_OWNER = Property.Name(XmlUtils.NS_WEBDAV, "shared-owner") val SHARED_OWNER = Property.Name(XmlUtils.NS_WEBDAV, "shared-owner")
val READ_WRITE = Property.Name(XmlUtils.NS_WEBDAV, "read-write") val READ_WRITE = Property.Name(XmlUtils.NS_WEBDAV, "read-write")
val NOT_SHARED = Property.Name(XmlUtils.NS_WEBDAV, "not-shared") val NOT_SHARED = Property.Name(XmlUtils.NS_WEBDAV, "not-shared")
val NO_ACCESS = Property.Name(XmlUtils.NS_WEBDAV, "no-access")
val READ = Property.Name(XmlUtils.NS_WEBDAV, "read") val READ = Property.Name(XmlUtils.NS_WEBDAV, "read")
} }

@ -56,7 +56,12 @@ open class OpenTaskDao @Inject constructor(
url = it.getString(CommonSyncColumns._SYNC_ID), url = it.getString(CommonSyncColumns._SYNC_ID),
ctag = it.getString(TaskLists.SYNC_VERSION) ctag = it.getString(TaskLists.SYNC_VERSION)
?.let(::JSONObject) ?.let(::JSONObject)
?.getString("value") ?.getString("value"),
access = when (it.getInt(TaskLists.ACCESS_LEVEL)) {
TaskLists.ACCESS_LEVEL_OWNER -> CaldavCalendar.ACCESS_OWNER
TaskLists.ACCESS_LEVEL_READ -> CaldavCalendar.ACCESS_READ_ONLY
else -> CaldavCalendar.ACCESS_READ_WRITE
},
) )
) )
} }
@ -174,6 +179,7 @@ open class OpenTaskDao @Inject constructor(
color = color, color = color,
name = name, name = name,
account = account, account = account,
access = access,
) )
} }
} }

@ -108,6 +108,10 @@ public class TaskContainer {
targetIndent = indent; targetIndent = indent;
} }
public boolean isReadOnly() {
return task.getReadOnly();
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) { if (this == o) {

@ -16,6 +16,7 @@ import org.tasks.data.Alarm.Companion.TYPE_REL_END
import org.tasks.data.Alarm.Companion.TYPE_REL_START import org.tasks.data.Alarm.Companion.TYPE_REL_START
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
import org.tasks.data.CaldavAccount.Companion.SERVER_UNKNOWN import org.tasks.data.CaldavAccount.Companion.SERVER_UNKNOWN
import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_ONLY
import org.tasks.data.OpenTaskDao.Companion.getLong import org.tasks.data.OpenTaskDao.Companion.getLong
import org.tasks.extensions.getLongOrNull import org.tasks.extensions.getLongOrNull
import org.tasks.extensions.getString import org.tasks.extensions.getString
@ -580,6 +581,13 @@ object Migrations {
} }
} }
private val MIGRATION_86_87 = object : Migration(86, 87) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `tasks` ADD COLUMN `read_only` INTEGER NOT NULL DEFAULT 0")
database.execSQL("UPDATE `tasks` SET `read_only` = 1 WHERE `_id` IN (SELECT `cd_task` FROM `caldav_tasks` INNER JOIN `caldav_lists` ON `caldav_tasks`.`cd_calendar` = `caldav_lists`.`cdl_uuid` WHERE `cdl_access` = $ACCESS_READ_ONLY)")
}
}
fun migrations(fileStorage: FileStorage) = arrayOf( fun migrations(fileStorage: FileStorage) = arrayOf(
MIGRATION_35_36, MIGRATION_35_36,
MIGRATION_36_37, MIGRATION_36_37,
@ -622,6 +630,7 @@ object Migrations {
MIGRATION_82_83, MIGRATION_82_83,
MIGRATION_84_85, MIGRATION_84_85,
MIGRATION_85_86, MIGRATION_85_86,
MIGRATION_86_87,
) )
private fun noop(from: Int, to: Int): Migration = object : Migration(from, to) { private fun noop(from: Int, to: Int): Migration = object : Migration(from, to) {

@ -58,7 +58,7 @@ class FilterPickerViewModel @Inject constructor(
private fun refresh() = viewModelScope.launch { private fun refresh() = viewModelScope.launch {
val items = if (listsOnly) { val items = if (listsOnly) {
filterProvider.listPickerItems() filterProvider.listPickerItems().filterNot { it is Filter && it.isReadOnly }
} else { } else {
filterProvider.filterPickerItems() filterProvider.filterPickerItems()
} }

@ -65,7 +65,10 @@ class CommentBarFragment : Fragment() {
} }
commentField.setHorizontallyScrolling(false) commentField.setHorizontallyScrolling(false)
commentField.maxLines = Int.MAX_VALUE commentField.maxLines = Int.MAX_VALUE
if (preferences.getBoolean(R.string.p_show_task_edit_comments, true)) { if (
preferences.getBoolean(R.string.p_show_task_edit_comments, true) &&
viewModel.isWritable
) {
commentBar.visibility = View.VISIBLE commentBar.visibility = View.VISIBLE
} }
commentBar.setBackgroundColor(themeColor.primaryColor) commentBar.setBackgroundColor(themeColor.primaryColor)

@ -317,7 +317,7 @@ class NotificationManager @Inject constructor(
.setOnlyAlertOnce(false) .setOnlyAlertOnce(false)
.setShowWhen(true) .setShowWhen(true)
.setTicker(taskTitle) .setTicker(taskTitle)
val intent = NotificationActivity.newIntent(context, taskTitle.toString(), id) val intent = NotificationActivity.newIntent(context, taskTitle.toString(), id, task.readOnly)
builder.setContentIntent( builder.setContentIntent(
PendingIntent.getActivity( PendingIntent.getActivity(
context, context,
@ -360,7 +360,9 @@ class NotificationManager @Inject constructor(
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
) )
val wearableExtender = NotificationCompat.WearableExtender() val wearableExtender = NotificationCompat.WearableExtender()
wearableExtender.addAction(completeAction) if (!task.readOnly) {
wearableExtender.addAction(completeAction)
}
for (snoozeOption in SnoozeDialog.getSnoozeOptions(preferences)) { for (snoozeOption in SnoozeDialog.getSnoozeOptions(preferences)) {
val timestamp = snoozeOption.dateTime.millis val timestamp = snoozeOption.dateTime.millis
val wearableIntent = SnoozeActivity.newIntent(context, id) val wearableIntent = SnoozeActivity.newIntent(context, id)
@ -379,8 +381,10 @@ class NotificationManager @Inject constructor(
wearablePendingIntent) wearablePendingIntent)
.build()) .build())
} }
if (!task.readOnly) {
builder.addAction(completeAction)
}
return builder return builder
.addAction(completeAction)
.addAction( .addAction(
R.drawable.ic_snooze_white_24dp, R.drawable.ic_snooze_white_24dp,
context.getString(R.string.rmd_NoA_snooze), context.getString(R.string.rmd_NoA_snooze),

@ -15,12 +15,7 @@ import org.tasks.analytics.Constants
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory import org.tasks.billing.Inventory
import org.tasks.caldav.iCalendar import org.tasks.caldav.iCalendar
import org.tasks.data.CaldavAccount import org.tasks.data.*
import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavDao
import org.tasks.data.CaldavTask
import org.tasks.data.MyAndroidTask
import org.tasks.data.OpenTaskDao
import org.tasks.data.OpenTaskDao.Companion.filterActive import org.tasks.data.OpenTaskDao.Companion.filterActive
import org.tasks.data.OpenTaskDao.Companion.isDavx5 import org.tasks.data.OpenTaskDao.Companion.isDavx5
import org.tasks.data.OpenTaskDao.Companion.isDecSync import org.tasks.data.OpenTaskDao.Companion.isDecSync
@ -90,7 +85,9 @@ class OpenTasksSynchronizer @Inject constructor(
.forEach { taskDeleter.delete(it) } .forEach { taskDeleter.delete(it) }
lists.forEach { lists.forEach {
val calendar = toLocalCalendar(it) val calendar = toLocalCalendar(it)
pushChanges(account, calendar, it.id) if (calendar.access != CaldavCalendar.ACCESS_READ_ONLY) {
pushChanges(account, calendar, it.id)
}
fetchChanges(account, calendar, it.ctag, it.id) fetchChanges(account, calendar, it.ctag, it.id)
} }
} }
@ -102,9 +99,14 @@ class OpenTasksSynchronizer @Inject constructor(
caldavDao.insert(local) caldavDao.insert(local)
Timber.d("Created calendar: $local") Timber.d("Created calendar: $local")
localBroadcastManager.broadcastRefreshList() localBroadcastManager.broadcastRefreshList()
} else if (local.name != remote.name || local.color != remote.color) { } else if (
local.name != remote.name ||
local.color != remote.color ||
local.access != remote.access
) {
local.color = remote.color local.color = remote.color
local.name = remote.name local.name = remote.name
local.access = remote.access
caldavDao.update(local) caldavDao.update(local)
Timber.d("Updated calendar: $local") Timber.d("Updated calendar: $local")
localBroadcastManager.broadcastRefreshList() localBroadcastManager.broadcastRefreshList()

@ -11,6 +11,7 @@ import kotlinx.coroutines.runBlocking
import org.tasks.R import org.tasks.R
import org.tasks.Strings.isNullOrEmpty import org.tasks.Strings.isNullOrEmpty
import org.tasks.data.* import org.tasks.data.*
import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_ONLY
import org.tasks.filters.PlaceFilter import org.tasks.filters.PlaceFilter
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -48,6 +49,7 @@ class DefaultFilterProvider @Inject constructor(
suspend fun getDefaultList() = suspend fun getDefaultList() =
getFilterFromPreference(preferences.getStringValue(R.string.p_default_list), null) getFilterFromPreference(preferences.getStringValue(R.string.p_default_list), null)
?.takeIf { it.isWritable }
?: getAnyList() ?: getAnyList()
suspend fun getLastViewedFilter() = getFilterFromPreference(R.string.p_last_viewed_list) suspend fun getLastViewedFilter() = getFilterFromPreference(R.string.p_last_viewed_list)
@ -77,7 +79,7 @@ class DefaultFilterProvider @Inject constructor(
private suspend fun getAnyList(): Filter { private suspend fun getAnyList(): Filter {
val filter = googleTaskListDao.getAllLists().getOrNull(0)?.let(::GtasksFilter) val filter = googleTaskListDao.getAllLists().getOrNull(0)?.let(::GtasksFilter)
?: caldavDao.getCalendars().getOrElse(0) { caldavDao.getLocalList(context) }.let(::CaldavFilter) ?: caldavDao.getCalendars().filterNot { it.access == ACCESS_READ_ONLY }.getOrElse(0) { caldavDao.getLocalList(context) }.let(::CaldavFilter)
defaultList = filter defaultList = filter
return filter return filter
} }
@ -158,6 +160,7 @@ class DefaultFilterProvider @Inject constructor(
} }
} else if (task.hasTransitory(CaldavTask.KEY)) { } else if (task.hasTransitory(CaldavTask.KEY)) {
val caldav = caldavDao.getCalendarByUuid(task.getTransitory(CaldavTask.KEY)!!) val caldav = caldavDao.getCalendarByUuid(task.getTransitory(CaldavTask.KEY)!!)
?.takeIf { it.access != ACCESS_READ_ONLY }
if (caldav != null) { if (caldav != null) {
originalList = CaldavFilter(caldav) originalList = CaldavFilter(caldav)
} }

@ -32,6 +32,9 @@ class NotificationActivity : InjectingAppCompatActivity(), NotificationDialog.No
var fragment = fragmentManager.findFragmentByTag(FRAG_TAG_NOTIFICATION_FRAGMENT) as NotificationDialog? var fragment = fragmentManager.findFragmentByTag(FRAG_TAG_NOTIFICATION_FRAGMENT) as NotificationDialog?
if (fragment == null) { if (fragment == null) {
fragment = NotificationDialog() fragment = NotificationDialog()
fragment.arguments = Bundle().apply {
putBoolean(EXTRA_READ_ONLY, intent.getBooleanExtra(EXTRA_READ_ONLY, false))
}
fragment.show(fragmentManager, FRAG_TAG_NOTIFICATION_FRAGMENT) fragment.show(fragmentManager, FRAG_TAG_NOTIFICATION_FRAGMENT)
} }
fragment.setTitle(intent.getStringExtra(EXTRA_TITLE)) fragment.setTitle(intent.getStringExtra(EXTRA_TITLE))
@ -69,12 +72,14 @@ class NotificationActivity : InjectingAppCompatActivity(), NotificationDialog.No
companion object { companion object {
const val EXTRA_TITLE = "extra_title" const val EXTRA_TITLE = "extra_title"
const val EXTRA_TASK_ID = "extra_task_id" const val EXTRA_TASK_ID = "extra_task_id"
const val EXTRA_READ_ONLY = "extra_read_only"
private const val FRAG_TAG_NOTIFICATION_FRAGMENT = "frag_tag_notification_fragment" private const val FRAG_TAG_NOTIFICATION_FRAGMENT = "frag_tag_notification_fragment"
fun newIntent(context: Context?, title: String?, id: Long): Intent { fun newIntent(context: Context?, title: String?, id: Long, readOnly: Boolean): Intent {
val intent = Intent(context, NotificationActivity::class.java) val intent = Intent(context, NotificationActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
intent.putExtra(EXTRA_TASK_ID, id) intent.putExtra(EXTRA_TASK_ID, id)
intent.putExtra(EXTRA_TITLE, title) intent.putExtra(EXTRA_TITLE, title)
intent.putExtra(EXTRA_READ_ONLY, readOnly)
return intent return intent
} }
} }

@ -1,18 +1,24 @@
package org.tasks.reminders; package org.tasks.reminders;
import static org.tasks.reminders.NotificationActivity.EXTRA_READ_ONLY;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import android.app.Dialog; import android.app.Dialog;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import dagger.hilt.android.AndroidEntryPoint;
import java.util.List;
import javax.inject.Inject;
import org.tasks.R; import org.tasks.R;
import org.tasks.dialogs.DialogBuilder; import org.tasks.dialogs.DialogBuilder;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint @AndroidEntryPoint
public class NotificationDialog extends DialogFragment { public class NotificationDialog extends DialogFragment {
@ -30,11 +36,11 @@ public class NotificationDialog extends DialogFragment {
getString(R.string.rmd_NoA_done)); getString(R.string.rmd_NoA_done));
handler = (NotificationHandler) getActivity(); handler = (NotificationHandler) getActivity();
boolean readOnly = getArguments().getBoolean(EXTRA_READ_ONLY);
return dialogBuilder return dialogBuilder
.newDialog(title) .newDialog(title)
.setItems( .setItems(
items, readOnly ? items.subList(0, 2) : items,
(dialog, which) -> { (dialog, which) -> {
switch (which) { switch (which) {
case 0: case 0:

@ -178,6 +178,10 @@ class TaskEditViewModel @Inject constructor(
} }
} }
val isReadOnly = task.readOnly
val isWritable = !isReadOnly
fun hasChanges(): Boolean = fun hasChanges(): Boolean =
(task.title != title || (isNew && title?.isNotBlank() == true)) || (task.title != title || (isNew && title?.isNotBlank() == true)) ||
task.isCompleted != completed || task.isCompleted != completed ||
@ -221,7 +225,7 @@ class TaskEditViewModel @Inject constructor(
if (cleared) { if (cleared) {
return@withContext false return@withContext false
} }
if (!hasChanges()) { if (!hasChanges() || isReadOnly) {
discard(remove) discard(remove)
return@withContext false return@withContext false
} }

@ -117,6 +117,10 @@ class TasksWidget : AppWidgetProvider() {
setRipple( setRipple(
remoteViews, color, R.id.widget_button, R.id.widget_change_list, R.id.widget_reconfigure) remoteViews, color, R.id.widget_button, R.id.widget_change_list, R.id.widget_reconfigure)
remoteViews.setOnClickPendingIntent(R.id.widget_title, getOpenListIntent(context, filter, id)) remoteViews.setOnClickPendingIntent(R.id.widget_title, getOpenListIntent(context, filter, id))
remoteViews.setViewVisibility(
R.id.widget_button,
if (filter.isWritable) View.VISIBLE else View.GONE
)
remoteViews.setOnClickPendingIntent(R.id.widget_button, getNewTaskIntent(context, filter, id)) remoteViews.setOnClickPendingIntent(R.id.widget_button, getNewTaskIntent(context, filter, id))
remoteViews.setOnClickPendingIntent(R.id.widget_change_list, getChooseListIntent(context, filter, id)) remoteViews.setOnClickPendingIntent(R.id.widget_change_list, getChooseListIntent(context, filter, id))
remoteViews.setOnClickPendingIntent( remoteViews.setOnClickPendingIntent(

@ -75,7 +75,7 @@
+| | \--- org.jetbrains.kotlin:kotlin-stdlib:1.6.20 -> 1.7.20 (*) +| | \--- org.jetbrains.kotlin:kotlin-stdlib:1.6.20 -> 1.7.20 (*)
+| \--- org.ogce:xpp3:1.1.6 +| \--- org.ogce:xpp3:1.1.6
+| \--- jakarta-regexp:jakarta-regexp:1.4 +| \--- jakarta-regexp:jakarta-regexp:1.4
++--- com.github.tasks:ical4android:2fb465b ++--- com.github.tasks:ical4android:ce7919d
+| +--- org.mnode.ical4j:ical4j:3.2.5 +| +--- org.mnode.ical4j:ical4j:3.2.5
+| | +--- javax.cache:cache-api:1.1.1 +| | +--- javax.cache:cache-api:1.1.1
+| | +--- org.threeten:threeten-extra:1.7.0 +| | +--- org.threeten:threeten-extra:1.7.0

@ -322,7 +322,7 @@
+| | \--- org.jetbrains.kotlin:kotlin-stdlib:1.6.20 -> 1.7.20 (*) +| | \--- org.jetbrains.kotlin:kotlin-stdlib:1.6.20 -> 1.7.20 (*)
+| \--- org.ogce:xpp3:1.1.6 +| \--- org.ogce:xpp3:1.1.6
+| \--- jakarta-regexp:jakarta-regexp:1.4 +| \--- jakarta-regexp:jakarta-regexp:1.4
++--- com.github.tasks:ical4android:2fb465b ++--- com.github.tasks:ical4android:ce7919d
+| +--- org.mnode.ical4j:ical4j:3.2.5 +| +--- org.mnode.ical4j:ical4j:3.2.5
+| | +--- javax.cache:cache-api:1.1.1 +| | +--- javax.cache:cache-api:1.1.1
+| | +--- org.threeten:threeten-extra:1.7.0 +| | +--- org.threeten:threeten-extra:1.7.0

Loading…
Cancel
Save