diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 985eced2..85836024 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -106,6 +106,19 @@ android:name="android.content.SyncAdapter" android:resource="@xml/sync_calendars" /> + + + + + + + + data, String account, CollectionInfo.Type type) { - return data.select(ServiceEntity.class).where(ServiceEntity.ACCOUNT.eq(account).and(ServiceEntity.TYPE.eq(type))).limit(1).get().firstOrNull(); + ServiceEntity service = data.select(ServiceEntity.class).where(ServiceEntity.ACCOUNT.eq(account).and(ServiceEntity.TYPE.eq(type))).limit(1).get().firstOrNull(); + if (service == null) { + // If our first time, create service and a journal + ServiceEntity serviceEntity = new ServiceEntity(); + serviceEntity.account = account; + serviceEntity.type = CollectionInfo.Type.TASKS; + service = data.insert(serviceEntity); + } + return service; } } diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt index 0518b3e0..efdaeab3 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt @@ -151,7 +151,7 @@ class LocalAddressBook( _mainAccount = newMainAccount } - var url: String + override var url: String get() = AccountManager.get(context).getUserData(account, USER_DATA_URL) ?: throw IllegalStateException("Address book has no URL") set(url) = AccountManager.get(context).setUserData(account, USER_DATA_URL, url) diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt index 44ee06e7..fb3cee13 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt @@ -88,6 +88,9 @@ class LocalCalendar private constructor( } } + override val url: String? + get() = name + fun update(journalEntity: JournalEntity, updateColor: Boolean) = update(valuesFromCollectionInfo(journalEntity, updateColor)) diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.kt index fc2c77b1..38bfcc94 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.kt @@ -9,6 +9,8 @@ package com.etesync.syncadapter.resource interface LocalCollection> { + val url: String? + fun findDeleted(): List fun findDirty(): List fun findWithoutFileName(): List diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt index 9ddd0b7a..24e67995 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalTask.kt @@ -48,7 +48,7 @@ class LocalTask : AndroidTask, LocalResource { val uuid: String? get() = fileName - constructor(taskList: AndroidTaskList<*>, task: Task, fileName: String?, eTag: String?, flags: Int) + constructor(taskList: AndroidTaskList<*>, task: Task, fileName: String?, eTag: String?) : super(taskList, task) { this.fileName = fileName this.eTag = eTag @@ -63,6 +63,15 @@ class LocalTask : AndroidTask, LocalResource { /* process LocalTask-specific fields */ + override fun populateTask(values: ContentValues) { + super.populateTask(values) + fileName = values.getAsString(TaskContract.Tasks._SYNC_ID) + eTag = values.getAsString(COLUMN_ETAG) + task?.uid = values.getAsString(COLUMN_UID) + + task?.sequence = values.getAsInteger(COLUMN_SEQUENCE) + } + override fun buildTask(builder: ContentProviderOperation.Builder, update: Boolean) { super.buildTask(builder, update) builder.withValue(TaskContract.Tasks._SYNC_ID, fileName) @@ -75,7 +84,14 @@ class LocalTask : AndroidTask, LocalResource { /* custom queries */ override fun prepareForUpload() { - val uid = UUID.randomUUID().toString() + var uid: String? = null + val c = taskList.provider.client.query(taskSyncURI(), arrayOf(COLUMN_UID), null, null, null) + if (c.moveToNext()) + uid = c.getString(0) + if (uid == null) + uid = UUID.randomUUID().toString() + + c.close() val values = ContentValues(2) values.put(TaskContract.Tasks._SYNC_ID, uid) diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt index 24a51e87..d2425271 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt @@ -62,6 +62,9 @@ class LocalTaskList private constructor( } + override val url: String? + get() = syncId + fun update(journalEntity: JournalEntity, updateColor: Boolean) = update(valuesFromCollectionInfo(journalEntity, updateColor)) diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt index 2fce6e9f..e7d63666 100644 --- a/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/SyncManager.kt @@ -10,9 +10,7 @@ package com.etesync.syncadapter.syncadapter import android.accounts.Account import android.annotation.TargetApi import android.content.Context -import android.content.Intent import android.content.SyncResult -import android.content.res.Resources import android.os.Bundle import com.etesync.syncadapter.AccountSettings @@ -40,7 +38,6 @@ import com.etesync.syncadapter.ui.ViewCollectionActivity import java.io.FileNotFoundException import java.io.IOException import java.util.ArrayList -import java.util.Arrays import java.util.LinkedList import java.util.Locale import java.util.logging.Level diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncAdapterService.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncAdapterService.kt new file mode 100644 index 00000000..168a4195 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncAdapterService.kt @@ -0,0 +1,152 @@ +/* + * Copyright © Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ +package com.etesync.syncadapter.syncadapter + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.ContentProviderClient +import android.content.ContentResolver +import android.content.Context +import android.content.SyncResult +import android.database.sqlite.SQLiteException +import android.os.Build +import android.os.Bundle +import at.bitfire.ical4android.AndroidTaskList +import at.bitfire.ical4android.TaskProvider +import com.etesync.syncadapter.* +import com.etesync.syncadapter.journalmanager.Exceptions +import com.etesync.syncadapter.model.CollectionInfo +import com.etesync.syncadapter.model.JournalEntity +import com.etesync.syncadapter.model.JournalModel +import com.etesync.syncadapter.model.ServiceEntity +import com.etesync.syncadapter.resource.LocalTaskList +import com.etesync.syncadapter.ui.DebugInfoActivity +import okhttp3.HttpUrl +import org.dmfs.tasks.contract.TaskContract +import java.util.* +import java.util.logging.Level + +/** + * Synchronization manager for CalDAV collections; handles tasks ({@code VTODO}). + */ +class TasksSyncAdapterService: SyncAdapterService() { + + override fun syncAdapter() = TasksSyncAdapter(this) + + + class TasksSyncAdapter( + context: Context + ): SyncAdapter(context) { + + override fun onPerformSync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) { + super.onPerformSync(account, extras, authority, provider, syncResult) + + val notificationManager = NotificationHelper(context, "journals-tasks", Constants.NOTIFICATION_TASK_SYNC) + notificationManager.cancel() + + try { + val taskProvider = TaskProvider.fromProviderClient(context, provider) + + // make sure account can be seen by OpenTasks + if (Build.VERSION.SDK_INT >= 26) + AccountManager.get(context).setAccountVisibility(account, taskProvider.name.packageName, AccountManager.VISIBILITY_VISIBLE) + + val accountSettings = AccountSettings(context, account) + /* don't run sync if + - sync conditions (e.g. "sync only in WiFi") are not met AND + - this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions) + */ + if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings)) + return + + RefreshCollections(account, CollectionInfo.Type.TASKS).run() + + updateLocalTaskLists(taskProvider, account, accountSettings) + + val principal = HttpUrl.get(accountSettings.uri!!)!! + + for (taskList in AndroidTaskList.find(account, taskProvider, LocalTaskList.Factory, "${TaskContract.TaskLists.SYNC_ENABLED}!=0", null)) { + App.log.info("Synchronizing task list #${taskList.id} [${taskList.syncId}]") + val tasksSyncManager = TasksSyncManager(context, account, accountSettings, extras, authority, syncResult, taskList, principal); + tasksSyncManager.performSync() + } + } catch (e: Exceptions.ServiceUnavailableException) { + syncResult.stats.numIoExceptions++ + syncResult.delayUntil = if (e.retryAfter > 0) e.retryAfter else Constants.DEFAULT_RETRY_DELAY + } catch (e: Exception) { + if (e is SQLiteException) { + App.log.log(Level.SEVERE, "Couldn't prepare local task list", e) + syncResult.databaseError = true + } + + val syncPhase = R.string.sync_phase_journals + val title = context.getString(R.string.sync_error_tasks, account.name) + + notificationManager.setThrowable(e) + + val detailsIntent = notificationManager.detailsIntent + detailsIntent.putExtra(Constants.KEY_ACCOUNT, account) + if (e !is Exceptions.UnauthorizedException) { + detailsIntent.putExtra(DebugInfoActivity.KEY_AUTHORITY, authority) + detailsIntent.putExtra(DebugInfoActivity.KEY_PHASE, syncPhase) + } + + notificationManager.notify(title, context.getString(syncPhase)) + } catch (e: OutOfMemoryError) { + val syncPhase = R.string.sync_phase_journals + val title = context.getString(R.string.sync_error_tasks, account.name) + notificationManager.setThrowable(e) + val detailsIntent = notificationManager.detailsIntent + detailsIntent.putExtra(Constants.KEY_ACCOUNT, account) + notificationManager.notify(title, context.getString(syncPhase)) + } + + App.log.info("Task sync complete") + } + + private fun updateLocalTaskLists(provider: TaskProvider, account: Account, settings: AccountSettings) { + val data = (context.applicationContext as App).data + var service = JournalModel.Service.fetch(data, account.name, CollectionInfo.Type.TASKS) + + val remote = HashMap() + val remoteJournals = JournalEntity.getJournals(data, service) + for (journalEntity in remoteJournals) { + remote[journalEntity.uid] = journalEntity + } + + val local = AndroidTaskList.find(account, provider, LocalTaskList.Factory, null, null) + + val updateColors = settings.manageCalendarColors + + // delete obsolete local TaskList + for (taskList in local) { + val url = taskList.url + val journalEntity = remote[url] + if (journalEntity == null) { + App.log.fine("Deleting obsolete local task list $url") + taskList.delete() + } else { + // remote CollectionInfo found for this local collection, update data + App.log.fine("Updating local task list $url with $journalEntity") + taskList.update(journalEntity, updateColors) + // we already have a local tasks for this remote collection, don't take into consideration anymore + remote.remove(url) + } + } + + // create new local taskss + for (url in remote.keys) { + val journalEntity = remote[url]!! + App.log.info("Adding local task list $journalEntity") + LocalTaskList.create(account, provider, journalEntity) + } + } + + } + +} diff --git a/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncManager.kt b/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncManager.kt new file mode 100644 index 00000000..8e36e9f2 --- /dev/null +++ b/app/src/main/java/com/etesync/syncadapter/syncadapter/TasksSyncManager.kt @@ -0,0 +1,119 @@ +/* + * Copyright © Ricki Hirner (bitfire web engineering). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package com.etesync.syncadapter.syncadapter + +import android.accounts.Account +import android.content.Context +import android.content.SyncResult +import android.os.Bundle +import at.bitfire.ical4android.Event +import at.bitfire.ical4android.InvalidCalendarException +import at.bitfire.ical4android.Task +import com.etesync.syncadapter.AccountSettings +import com.etesync.syncadapter.App +import com.etesync.syncadapter.Constants +import com.etesync.syncadapter.R +import com.etesync.syncadapter.journalmanager.JournalEntryManager +import com.etesync.syncadapter.model.CollectionInfo +import com.etesync.syncadapter.model.SyncEntry +import com.etesync.syncadapter.resource.LocalEvent +import com.etesync.syncadapter.resource.LocalTask +import com.etesync.syncadapter.resource.LocalTaskList +import okhttp3.HttpUrl +import java.io.Reader +import java.io.StringReader +import java.util.logging.Level + +/** + * Synchronization manager for CalDAV collections; handles tasks (VTODO) + */ +class TasksSyncManager( + context: Context, + account: Account, + accountSettings: AccountSettings, + extras: Bundle, + authority: String, + syncResult: SyncResult, + taskList: LocalTaskList, + private val remote: HttpUrl +): SyncManager(context, account, accountSettings, extras, authority, syncResult, taskList.url!!, CollectionInfo.Type.TASKS, account.name) { + + override val syncErrorTitle: String + get() = context.getString(R.string.sync_error_tasks, account.name) + + override val syncSuccessfullyTitle: String + get() = context.getString(R.string.sync_successfully_tasks, info.displayName, + account.name) + + init { + localCollection = taskList + } + + override fun notificationId(): Int { + return Constants.NOTIFICATION_TASK_SYNC + } + + override fun prepare(): Boolean { + if (!super.prepare()) + return false + + journal = JournalEntryManager(httpClient, remote, localTaskList().url!!) + return true + } + + // helpers + + private fun localTaskList(): LocalTaskList { + return localCollection as LocalTaskList + } + + override fun processSyncEntry(cEntry: SyncEntry) { + val inputReader = StringReader(cEntry.content) + + val tasks = Task.fromReader(inputReader) + if (tasks.size == 0) { + App.log.warning("Received VCard without data, ignoring") + return + } else if (tasks.size > 1) { + App.log.warning("Received multiple VCALs, using first one") + } + + val event = tasks[0] + val local = localCollection!!.findByUid(event.uid!!) + + if (cEntry.isAction(SyncEntry.Actions.ADD) || cEntry.isAction(SyncEntry.Actions.CHANGE)) { + processTask(event, local) + } else { + if (local != null) { + App.log.info("Removing local record #" + local.id + " which has been deleted on the server") + local.delete() + } else { + App.log.warning("Tried deleting a non-existent record: " + event.uid) + } + } + } + + private fun processTask(newData: Task, localTask: LocalTask?): LocalTask { + var localTask = localTask + // delete local Task, if it exists + if (localTask != null) { + App.log.info("Updating " + newData.uid + " in local calendar") + localTask.eTag = newData.uid + localTask.update(newData) + syncResult.stats.numUpdates++ + } else { + App.log.info("Adding " + newData.uid + " to local calendar") + localTask = LocalTask(localTaskList(), newData, newData.uid, newData.uid) + localTask.add() + syncResult.stats.numInserts++ + } + + return localTask + } +} \ No newline at end of file diff --git a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt index 089e0616..303c3cc3 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/AccountActivity.kt @@ -278,29 +278,32 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe for (serviceEntity in data.select(ServiceEntity::class.java).where(ServiceEntity.ACCOUNT.eq(account.name)).get()) { val id = serviceEntity.id.toLong() val service = serviceEntity.type - if (service == CollectionInfo.Type.ADDRESS_BOOK) { - info.carddav = AccountInfo.ServiceInfo() - info.carddav!!.id = id - info.carddav!!.refreshing = davService != null && davService!!.isRefreshing(id) || ContentResolver.isSyncActive(account, App.addressBooksAuthority) - info.carddav!!.journals = JournalEntity.getJournals(data, serviceEntity) - - val accountManager = AccountManager.get(context) - for (addrBookAccount in accountManager.getAccountsByType(App.addressBookAccountType)) { - val addressBook = LocalAddressBook(context, addrBookAccount, null) - try { - if (account == addressBook.mainAccount) - info.carddav!!.refreshing = info.carddav!!.refreshing or ContentResolver.isSyncActive(addrBookAccount, ContactsContract.AUTHORITY) - } catch (e: ContactsStorageException) { - } + when (service) { + CollectionInfo.Type.ADDRESS_BOOK -> { + info.carddav = AccountInfo.ServiceInfo() + info.carddav!!.id = id + info.carddav!!.refreshing = davService != null && davService!!.isRefreshing(id) || ContentResolver.isSyncActive(account, App.addressBooksAuthority) + info.carddav!!.journals = JournalEntity.getJournals(data, serviceEntity) + + val accountManager = AccountManager.get(context) + for (addrBookAccount in accountManager.getAccountsByType(App.addressBookAccountType)) { + val addressBook = LocalAddressBook(context, addrBookAccount, null) + try { + if (account == addressBook.mainAccount) + info.carddav!!.refreshing = info.carddav!!.refreshing or ContentResolver.isSyncActive(addrBookAccount, ContactsContract.AUTHORITY) + } catch (e: ContactsStorageException) { + } + } + } + CollectionInfo.Type.CALENDAR -> { + info.caldav = AccountInfo.ServiceInfo() + info.caldav!!.id = id + info.caldav!!.refreshing = davService != null && davService!!.isRefreshing(id) || + ContentResolver.isSyncActive(account, CalendarContract.AUTHORITY) || + ContentResolver.isSyncActive(account, TaskProvider.ProviderName.OpenTasks.authority) + info.caldav!!.journals = JournalEntity.getJournals(data, serviceEntity) } - } else if (service == CollectionInfo.Type.CALENDAR) { - info.caldav = AccountInfo.ServiceInfo() - info.caldav!!.id = id - info.caldav!!.refreshing = davService != null && davService!!.isRefreshing(id) || - ContentResolver.isSyncActive(account, CalendarContract.AUTHORITY) || - ContentResolver.isSyncActive(account, TaskProvider.ProviderName.OpenTasks.authority) - info.caldav!!.journals = JournalEntity.getJournals(data, serviceEntity) } } return info diff --git a/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionActivity.kt index a85e7fdd..43901e8d 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionActivity.kt @@ -43,26 +43,29 @@ open class CreateCollectionActivity : BaseActivity() { setContentView(R.layout.activity_create_collection) val displayName = findViewById(R.id.display_name) as EditText - if (info!!.type == CollectionInfo.Type.CALENDAR) { - setTitle(R.string.create_calendar) - displayName.setHint(R.string.create_calendar_display_name_hint) - - val colorSquare = findViewById(R.id.color) - colorSquare.setOnClickListener { - AmbilWarnaDialog(this@CreateCollectionActivity, (colorSquare.background as ColorDrawable).color, true, object : AmbilWarnaDialog.OnAmbilWarnaListener { - override fun onCancel(dialog: AmbilWarnaDialog) {} - - override fun onOk(dialog: AmbilWarnaDialog, color: Int) { - colorSquare.setBackgroundColor(color) - } - }).show() + when (info.type) { + CollectionInfo.Type.CALENDAR, CollectionInfo.Type.TASKS -> { + setTitle(R.string.create_calendar) + displayName.setHint(R.string.create_calendar_display_name_hint) + + val colorSquare = findViewById(R.id.color) + colorSquare.setOnClickListener { + AmbilWarnaDialog(this@CreateCollectionActivity, (colorSquare.background as ColorDrawable).color, true, object : AmbilWarnaDialog.OnAmbilWarnaListener { + override fun onCancel(dialog: AmbilWarnaDialog) {} + + override fun onOk(dialog: AmbilWarnaDialog, color: Int) { + colorSquare.setBackgroundColor(color) + } + }).show() + } } - } else { - setTitle(R.string.create_addressbook) - displayName.setHint(R.string.create_addressbook_display_name_hint) + CollectionInfo.Type.ADDRESS_BOOK -> { + setTitle(R.string.create_addressbook) + displayName.setHint(R.string.create_addressbook_display_name_hint) - val colorGroup = findViewById(R.id.color_group) - colorGroup.visibility = View.GONE + val colorGroup = findViewById(R.id.color_group) + colorGroup.visibility = View.GONE + } } } diff --git a/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionFragment.kt b/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionFragment.kt index b5ad7386..8da65548 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionFragment.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/CreateCollectionFragment.kt @@ -19,6 +19,7 @@ import android.support.v4.app.DialogFragment import android.support.v4.app.LoaderManager import android.support.v4.content.AsyncTaskLoader import android.support.v4.content.Loader +import at.bitfire.ical4android.TaskProvider import com.etesync.syncadapter.* import com.etesync.syncadapter.journalmanager.Crypto import com.etesync.syncadapter.journalmanager.Exceptions @@ -83,17 +84,15 @@ class CreateCollectionFragment : DialogFragment(), LoaderManager.LoaderCallbacks override fun loadInBackground(): Exception? { try { - var authority: String + var authority: String = "" val data = (context.applicationContext as App).data // 1. find service ID - if (info.type == CollectionInfo.Type.ADDRESS_BOOK) { - authority = App.addressBooksAuthority - } else if (info.type == CollectionInfo.Type.CALENDAR) { - authority = CalendarContract.AUTHORITY - } else { - throw IllegalArgumentException("Collection must be an address book or calendar") + when (info.type){ + CollectionInfo.Type.ADDRESS_BOOK -> authority = App.addressBooksAuthority + CollectionInfo.Type.CALENDAR -> authority = CalendarContract.AUTHORITY + CollectionInfo.Type.TASKS -> authority = TaskProvider.ProviderName.OpenTasks.authority } val serviceEntity = JournalModel.Service.fetch(data, account.name, info.type) diff --git a/app/src/main/java/com/etesync/syncadapter/ui/PermissionsActivity.kt b/app/src/main/java/com/etesync/syncadapter/ui/PermissionsActivity.kt index d616f85e..61ec0b4a 100644 --- a/app/src/main/java/com/etesync/syncadapter/ui/PermissionsActivity.kt +++ b/app/src/main/java/com/etesync/syncadapter/ui/PermissionsActivity.kt @@ -80,7 +80,7 @@ class PermissionsActivity : BaseActivity() { val PERMISSION_WRITE_TASKS = "org.dmfs.permission.WRITE_TASKS" fun requestAllPermissions(activity: Activity) { - ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS), REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS) + ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS, PERMISSION_READ_TASKS, PERMISSION_WRITE_TASKS), REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a641e263..ee10083a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -306,6 +306,7 @@ Additional permissions required Calendar sync failed (%s) Contacts sync failed (%s) + Tasks sync failed (%s) Error while %s Integrity error while %s Server error while %s @@ -325,6 +326,7 @@ User is inactive Calendar \"%s\" modified (%s) Contacts modified (%s) + Tasks \"%s\" modified (%s) %s modified. %s added.\n%s updated.\n%s deleted. diff --git a/app/src/main/res/xml/sync_tasks.xml b/app/src/main/res/xml/sync_tasks.xml new file mode 100644 index 00000000..d5436d28 --- /dev/null +++ b/app/src/main/res/xml/sync_tasks.xml @@ -0,0 +1,12 @@ + + +