mirror of https://github.com/etesync/android
This adds support for tasks via OpenTasks. https://github.com/dmfs/opentasks Need the OpenTasks client for it to be used. Currently you can't create new task lists. You can only have the default one, but that's just a UI thing. Fixes #7pull/61/head
parent
c15f894a71
commit
7f2ab44bca
@ -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<String, JournalEntity>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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<LocalTask>(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
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
<!--
|
||||
~ 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
|
||||
-->
|
||||
|
||||
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:accountType="@string/account_type"
|
||||
android:contentAuthority="org.dmfs.tasks"
|
||||
android:supportsUploading="true" />
|
Loading…
Reference in new issue