Read-only support

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

@ -164,7 +164,7 @@ dependencies {
implementation("com.github.bitfireAT:dav4jvm:2.2") {
exclude(group = "junit")
}
implementation("com.github.tasks:ical4android:2fb465b") {
implementation("com.github.tasks:ical4android:ce7919d") {
exclude(group = "commons-logging")
exclude(group = "org.json", module = "json")
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 dagger.hilt.android.qualifiers.ApplicationContext
import org.dmfs.tasks.contract.TaskContract
import org.dmfs.tasks.contract.TaskContract.TaskListColumns.ACCESS_LEVEL_OWNER
import org.tasks.caldav.iCalendar
import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavDao
@ -23,6 +24,7 @@ class TestOpenTaskDao @Inject constructor(
type: String = DEFAULT_TYPE,
account: String = DEFAULT_ACCOUNT,
url: String = UUIDHelper.newUUID(),
accessLevel: Int = ACCESS_LEVEL_OWNER,
): Pair<Long, CaldavCalendar> {
val uri = taskLists.buildUpon()
.appendQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER, "true")
@ -34,6 +36,7 @@ class TestOpenTaskDao @Inject constructor(
.withValue(TaskContract.CommonSyncColumns._SYNC_ID, url)
.withValue(TaskContract.TaskListColumns.LIST_NAME, name)
.withValue(TaskContract.TaskLists.SYNC_ENABLED, "1")
.withValue(TaskContract.TaskLists.ACCESS_LEVEL, accessLevel)
)
val calendar = CaldavCalendar(
uuid = UUIDHelper.newUUID(),

@ -18,17 +18,16 @@ import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.Toolbar
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.*
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
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.viewinterop.AndroidViewBinding
import androidx.coordinatorlayout.widget.CoordinatorLayout
@ -126,26 +125,34 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
val view: View = binding.root
val model = editViewModel.task
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 {
lifecycleScope.launch {
save()
}
}
val backButtonSavesTask = preferences.backButtonSavesTask()
toolbar.setNavigationContentDescription(if (backButtonSavesTask) {
R.string.discard
} else {
R.string.save
})
toolbar.setNavigationContentDescription(
when {
editViewModel.isReadOnly -> R.string.back
backButtonSavesTask -> R.string.discard
else -> R.string.save
}
)
toolbar.inflateMenu(R.menu.menu_task_edit_fragment)
val menu = toolbar.menu
val delete = menu.findItem(R.id.menu_delete)
delete.isVisible = !model.isNew
delete.isVisible = !model.isNew && editViewModel.isWritable
delete.setShowAsAction(
if (backButtonSavesTask) MenuItem.SHOW_AS_ACTION_NEVER else MenuItem.SHOW_AS_ACTION_IF_ROOM)
val discard = menu.findItem(R.id.menu_discard)
discard.isVisible = backButtonSavesTask
discard.isVisible = backButtonSavesTask && editViewModel.isWritable
discard.setShowAsAction(
if (model.isNew) MenuItem.SHOW_AS_ACTION_IF_ROOM else MenuItem.SHOW_AS_ACTION_NEVER)
if (savedInstanceState == null) {
@ -173,7 +180,11 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
title.setText(model.title)
title.setHorizontallyScrolling(false)
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
} else if (editViewModel.completed) {
title.paintFlags = title.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
@ -211,7 +222,7 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
binding.composeView.setContent {
MdcTheme {
Column {
Column(modifier = Modifier.gesturesDisabled(editViewModel.isReadOnly)) {
taskEditControlSetFragmentManager.displayOrder.forEachIndexed { index, tag ->
if (index < taskEditControlSetFragmentManager.visibleSize) {
// TODO: remove ui-viewbinding library when these are all migrated
@ -222,8 +233,12 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
TAG_LIST -> ListRow()
TAG_CREATION -> CreationRow()
CalendarControlSet.TAG -> AndroidViewBinding(TaskEditCalendarBinding::inflate)
StartDateControlSet.TAG -> AndroidViewBinding(TaskEditStartDateBinding::inflate)
ReminderControlSet.TAG -> AndroidViewBinding(TaskEditRemindersBinding::inflate)
StartDateControlSet.TAG -> AndroidViewBinding(
TaskEditStartDateBinding::inflate
)
ReminderControlSet.TAG -> AndroidViewBinding(
TaskEditRemindersBinding::inflate
)
LocationControlSet.TAG -> AndroidViewBinding(TaskEditLocationBinding::inflate)
FilesControlSet.TAG -> AndroidViewBinding(TaskEditFilesBinding::inflate)
TimerControlSet.TAG -> AndroidViewBinding(TaskEditTimerBinding::inflate)
@ -496,7 +511,8 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
const val EXTRA_TAGS = "extra_tags"
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"
private const val FRAG_TAG_DATE_PICKER = "frag_tag_date_picker"
const val REQUEST_CODE_PICK_CALENDAR = 70
@ -510,11 +526,11 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
fun newTaskEditFragment(
task: Task,
list: Filter,
location: Location?,
tags: ArrayList<TagData>,
alarms: ArrayList<Alarm>,
task: Task,
list: Filter,
location: Location?,
tags: ArrayList<TagData>,
alarms: ArrayList<Alarm>,
): TaskEditFragment {
val taskEditFragment = TaskEditFragment()
val arguments = Bundle()
@ -526,5 +542,21 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
taskEditFragment.arguments = arguments
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(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentTaskListBinding.inflate(inflater, container, false)
filter = getFilter()
with (binding) {
swipeRefreshLayout = bodyStandard.swipeLayout
emptyRefreshLayout = bodyEmpty.swipeLayoutEmpty
coordinatorLayout = taskListCoordinator
recyclerView = bodyStandard.recyclerView
fab.setOnClickListener { createNewTask() }
fab.isVisible = filter.isWritable
}
filter = getFilter()
themeColor = if (filter.tint != 0) colorProvider.getThemeColor(filter.tint, true) else defaultThemeColor
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_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)
menu.findItem(R.id.menu_clear_completed).isVisible = filter.isWritable
}
private fun openFilter(filter: Filter?) {
@ -633,10 +635,12 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
REQUEST_TAG_TASKS -> if (resultCode == Activity.RESULT_OK) {
lifecycleScope.launch {
val modified = tagDataDao.applyTags(
taskDao.fetch(
data!!.getSerializableExtra(TagPickerActivity.EXTRA_TASKS) as ArrayList<Long>),
taskDao
.fetch(data!!.getSerializableExtra(TagPickerActivity.EXTRA_TASKS) as ArrayList<Long>)
.filterNot { it.readOnly },
data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_PARTIALLY_SELECTED)!!,
data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED)!!)
data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED)!!
)
taskDao.touch(modified)
}
finishActionMode()
@ -693,6 +697,10 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
override fun onCreateActionMode(actionMode: ActionMode, menu: Menu): Boolean {
val inflater = actionMode.menuInflater
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
}
@ -729,6 +737,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
lifecycleScope.launch {
taskDao
.fetch(selected)
.filterNot { it.readOnly }
.takeIf { it.isNotEmpty() }
?.let {
newDateTimePicker(
@ -800,7 +809,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
}
}
fun showDateTimePicker(task: TaskContainer) {
private fun showDateTimePicker(task: TaskContainer) {
val fragmentManager = parentFragmentManager
if (fragmentManager.findFragmentByTag(FRAG_TAG_DATE_TIME_PICKER) == null) {
newDateTimePicker(
@ -859,6 +868,9 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
fun clearCollapsed() = taskAdapter.clearCollapsed()
override fun onCompletedTask(task: TaskContainer, newState: Boolean) {
if (task.isReadOnly) {
return
}
lifecycleScope.launch {
taskCompleter.setComplete(task.getTask(), newState)
taskAdapter.onCompletedTask(task, newState)
@ -900,6 +912,9 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
}
override fun onChangeDueDate(task: TaskContainer) {
if (task.isReadOnly) {
return
}
showDateTimePicker(task)
}
@ -921,6 +936,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
val tasks =
(intent.getSerializableExtra(EXTRAS_TASK_ID) as? ArrayList<Long>)
?.let { taskDao.fetch(it) }
?.filterNot { it.readOnly }
?.takeIf { it.isNotEmpty() }
?: return@launch
val isRecurringCompletion =

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

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

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

@ -103,6 +103,9 @@ class Task : Parcelable {
@Transient
var parent = 0L
@ColumnInfo(name = "read_only", defaultValue = "0")
var readOnly: Boolean = false
@Ignore
@Transient
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>?
isCollapsed = ParcelCompat.readBoolean(parcel)
parent = parcel.readLong()
readOnly = ParcelCompat.readBoolean(parcel)
}
var uuid: String
@ -265,6 +269,7 @@ class Task : Parcelable {
dest.writeMap(transitoryData as Map<*, *>?)
ParcelCompat.writeBoolean(dest, isCollapsed)
dest.writeLong(parent)
ParcelCompat.writeBoolean(dest, readOnly)
}
fun insignificantChange(task: Task?): Boolean {
@ -408,6 +413,7 @@ class Task : Parcelable {
if (isCollapsed != other.isCollapsed) return false
if (parent != other.parent) return false
if (transitoryData != other.transitoryData) return false
if (readOnly != other.readOnly) return false
return true
}
@ -434,11 +440,12 @@ class Task : Parcelable {
result = 31 * result + isCollapsed.hashCode()
result = 31 * result + parent.hashCode()
result = 31 * result + (transitoryData?.hashCode() ?: 0)
result = 31 * result + readOnly.hashCode()
return result
}
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)

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

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

@ -25,17 +25,16 @@ class TaskDuplicator @Inject constructor(
) {
suspend fun duplicate(taskIds: List<Long>): List<Task> {
val result: MutableList<Task> = ArrayList()
val tasks = ArrayList(taskIds)
taskIds.dbchunk().forEach {
tasks.removeAll(googleTaskDao.getChildren(it))
tasks.removeAll(taskDao.getChildren(it))
}
for (task in taskDao.fetch(tasks)) {
result.add(clone(task, task.parent))
}
localBroadcastManager.broadcastRefresh()
return result
return taskIds
.dbchunk()
.flatMap {
it.minus(googleTaskDao.getChildren(it).toSet())
.minus(taskDao.getChildren(it).toSet())
}
.let { taskDao.fetch(it) }
.filterNot { it.readOnly }
.map { clone(it, it.parent) }
.also { localBroadcastManager.broadcastRefresh() }
}
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.LocalBroadcastManager
import org.tasks.caldav.VtodoCache
import org.tasks.data.CaldavDao
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.data.*
import org.tasks.db.DbUtils.dbchunk
import org.tasks.preferences.Preferences
import org.tasks.sync.SyncAdapters
@ -49,19 +44,21 @@ class TaskMover @Inject constructor(
}
suspend fun move(ids: List<Long>, selectedList: Filter) {
val tasks = ArrayList(ids)
ids.dbchunk().forEach {
tasks.removeAll(googleTaskDao.getChildren(it))
tasks.removeAll(taskDao.getChildren(it))
}
taskDao.setParent(0, tasks)
for (task in taskDao.fetch(tasks)) {
performMove(task, selectedList)
}
val tasks = ids
.dbchunk()
.flatMap {
it.minus(googleTaskDao.getChildren(it).toSet())
.minus(taskDao.getChildren(it).toSet())
}
.let { taskDao.fetch(it) }
.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) {
caldavDao.updateParents(selectedList.uuid)
}
tasks.dbchunk().forEach {
taskIds.dbchunk().forEach {
taskDao.touch(it)
}
localBroadcastManager.broadcastRefresh()

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

@ -27,6 +27,7 @@ import org.tasks.caldav.extensions.toVAlarms
import org.tasks.data.*
import org.tasks.data.Alarm.Companion.TYPE_RANDOM
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.toDateTime
import org.tasks.date.DateTimeUtils.toLocal
@ -175,6 +176,7 @@ class iCalendar @Inject constructor(
) {
val task = existing?.task?.let { taskDao.fetch(it) }
?: taskCreator.createWithValues("").apply {
readOnly = calendar.access == ACCESS_READ_ONLY
taskDao.createNew(this)
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 READ_WRITE = Property.Name(XmlUtils.NS_WEBDAV, "read-write")
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")
}

@ -56,7 +56,12 @@ open class OpenTaskDao @Inject constructor(
url = it.getString(CommonSyncColumns._SYNC_ID),
ctag = it.getString(TaskLists.SYNC_VERSION)
?.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,
name = name,
account = account,
access = access,
)
}
}

@ -108,6 +108,10 @@ public class TaskContainer {
targetIndent = indent;
}
public boolean isReadOnly() {
return task.getReadOnly();
}
@Override
public boolean equals(Object 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_SNOOZE
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.extensions.getLongOrNull
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(
MIGRATION_35_36,
MIGRATION_36_37,
@ -622,6 +630,7 @@ object Migrations {
MIGRATION_82_83,
MIGRATION_84_85,
MIGRATION_85_86,
MIGRATION_86_87,
)
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 {
val items = if (listsOnly) {
filterProvider.listPickerItems()
filterProvider.listPickerItems().filterNot { it is Filter && it.isReadOnly }
} else {
filterProvider.filterPickerItems()
}

@ -65,7 +65,10 @@ class CommentBarFragment : Fragment() {
}
commentField.setHorizontallyScrolling(false)
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.setBackgroundColor(themeColor.primaryColor)

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

@ -15,12 +15,7 @@ import org.tasks.analytics.Constants
import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory
import org.tasks.caldav.iCalendar
import org.tasks.data.CaldavAccount
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.*
import org.tasks.data.OpenTaskDao.Companion.filterActive
import org.tasks.data.OpenTaskDao.Companion.isDavx5
import org.tasks.data.OpenTaskDao.Companion.isDecSync
@ -90,7 +85,9 @@ class OpenTasksSynchronizer @Inject constructor(
.forEach { taskDeleter.delete(it) }
lists.forEach {
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)
}
}
@ -102,9 +99,14 @@ class OpenTasksSynchronizer @Inject constructor(
caldavDao.insert(local)
Timber.d("Created calendar: $local")
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.name = remote.name
local.access = remote.access
caldavDao.update(local)
Timber.d("Updated calendar: $local")
localBroadcastManager.broadcastRefreshList()

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

@ -32,6 +32,9 @@ class NotificationActivity : InjectingAppCompatActivity(), NotificationDialog.No
var fragment = fragmentManager.findFragmentByTag(FRAG_TAG_NOTIFICATION_FRAGMENT) as NotificationDialog?
if (fragment == null) {
fragment = NotificationDialog()
fragment.arguments = Bundle().apply {
putBoolean(EXTRA_READ_ONLY, intent.getBooleanExtra(EXTRA_READ_ONLY, false))
}
fragment.show(fragmentManager, FRAG_TAG_NOTIFICATION_FRAGMENT)
}
fragment.setTitle(intent.getStringExtra(EXTRA_TITLE))
@ -69,12 +72,14 @@ class NotificationActivity : InjectingAppCompatActivity(), NotificationDialog.No
companion object {
const val EXTRA_TITLE = "extra_title"
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"
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)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
intent.putExtra(EXTRA_TASK_ID, id)
intent.putExtra(EXTRA_TITLE, title)
intent.putExtra(EXTRA_READ_ONLY, readOnly)
return intent
}
}

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

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

@ -117,6 +117,10 @@ class TasksWidget : AppWidgetProvider() {
setRipple(
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.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_change_list, getChooseListIntent(context, filter, id))
remoteViews.setOnClickPendingIntent(

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

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

Loading…
Cancel
Save