mirror of
https://github.com/etesync/android
synced 2024-12-23 23:18:46 +00:00
Upgrade vcard4android and ical4android.
This is a monster commit because to be honest, it's a monster change. It was impossible to do it in smaller steps because things just wouldn't compile. We couldn't do the migration step by step because they moved to Kotlin which was causing a lot of troubles. Now we are all on Kotlin, so things should hopefully work just fine.
This commit is contained in:
parent
b1d974af5a
commit
c4daed9391
@ -18,7 +18,7 @@ android {
|
||||
defaultConfig {
|
||||
applicationId "com.etesync.syncadapter"
|
||||
|
||||
minSdkVersion 16
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 26
|
||||
|
||||
versionCode 43
|
||||
|
@ -67,6 +67,7 @@ import java.util.logging.Logger
|
||||
import javax.net.ssl.HostnameVerifier
|
||||
|
||||
import at.bitfire.cert4android.CustomCertManager
|
||||
import at.bitfire.ical4android.AndroidCalendar
|
||||
import at.bitfire.ical4android.CalendarStorageException
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
import io.requery.Persistable
|
||||
@ -296,8 +297,8 @@ class App : Application() {
|
||||
// Generate account settings to make sure account is migrated.
|
||||
AccountSettings(this, account)
|
||||
|
||||
val calendars = LocalCalendar.find(account, this.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)!!,
|
||||
LocalCalendar.Factory.INSTANCE, null, null) as Array<LocalCalendar>
|
||||
val calendars = AndroidCalendar.find(account, this.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)!!,
|
||||
LocalCalendar.Factory, null, null)
|
||||
for (calendar in calendars) {
|
||||
calendar.fixEtags()
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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.resource
|
||||
|
||||
import at.bitfire.vcard4android.Contact
|
||||
|
||||
interface LocalAddress: LocalResource<Contact> {
|
||||
|
||||
fun resetDeleted()
|
||||
|
||||
}
|
@ -12,6 +12,7 @@ import android.accounts.AccountManager
|
||||
import android.accounts.AccountManagerCallback
|
||||
import android.accounts.AccountManagerFuture
|
||||
import android.accounts.AuthenticatorException
|
||||
import android.annotation.TargetApi
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentUris
|
||||
@ -47,8 +48,78 @@ import at.bitfire.vcard4android.CachedGroupMembership
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
|
||||
|
||||
class LocalAddressBook(protected val context: Context, account: Account, provider: ContentProviderClient?) : AndroidAddressBook(account, provider, LocalGroup.Factory.INSTANCE, LocalContact.Factory.INSTANCE), LocalCollection<LocalResource> {
|
||||
private val syncState = Bundle()
|
||||
class LocalAddressBook(
|
||||
private val context: Context,
|
||||
account: Account,
|
||||
provider: ContentProviderClient?
|
||||
): AndroidAddressBook<LocalContact, LocalGroup>(account, provider, LocalContact.Factory, LocalGroup.Factory), LocalCollection<LocalAddress> {
|
||||
|
||||
companion object {
|
||||
val USER_DATA_MAIN_ACCOUNT_TYPE = "real_account_type"
|
||||
val USER_DATA_MAIN_ACCOUNT_NAME = "real_account_name"
|
||||
val USER_DATA_URL = "url"
|
||||
const val USER_DATA_READ_ONLY = "read_only"
|
||||
|
||||
fun create(context: Context, provider: ContentProviderClient, mainAccount: Account, journalEntity: JournalEntity): LocalAddressBook {
|
||||
val info = journalEntity.info
|
||||
val accountManager = AccountManager.get(context)
|
||||
|
||||
val account = Account(accountName(mainAccount, info), App.addressBookAccountType)
|
||||
if (!accountManager.addAccountExplicitly(account, null, initialUserData(mainAccount, info.uid!!)))
|
||||
throw ContactsStorageException("Couldn't create address book account")
|
||||
|
||||
val addressBook = LocalAddressBook(context, account, provider)
|
||||
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
|
||||
|
||||
val values = ContentValues(2)
|
||||
values.put(ContactsContract.Settings.SHOULD_SYNC, 1)
|
||||
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1)
|
||||
addressBook.settings = values
|
||||
|
||||
return addressBook
|
||||
}
|
||||
|
||||
|
||||
fun find(context: Context, provider: ContentProviderClient?, mainAccount: Account?) = AccountManager.get(context)
|
||||
.getAccountsByType(App.addressBookAccountType)
|
||||
.map { LocalAddressBook(context, it, provider) }
|
||||
.filter { mainAccount == null || it.mainAccount == mainAccount }
|
||||
.toList()
|
||||
|
||||
|
||||
fun findByUid(context: Context, provider: ContentProviderClient, mainAccount: Account?, uid: String): LocalAddressBook? {
|
||||
val accountManager = AccountManager.get(context)
|
||||
|
||||
for (account in accountManager.getAccountsByType(App.addressBookAccountType)) {
|
||||
val addressBook = LocalAddressBook(context, account, provider)
|
||||
if (addressBook.url == uid && (mainAccount == null || addressBook.mainAccount == mainAccount))
|
||||
return addressBook
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// HELPERS
|
||||
|
||||
fun accountName(mainAccount: Account, info: CollectionInfo): String {
|
||||
val displayName = if (info.displayName != null) info.displayName else info.uid
|
||||
val sb = StringBuilder(displayName)
|
||||
sb.append(" (")
|
||||
.append(mainAccount.name)
|
||||
.append(" ")
|
||||
.append(info.uid!!.substring(0, 4))
|
||||
.append(")")
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
fun initialUserData(mainAccount: Account, url: String): Bundle {
|
||||
val bundle = Bundle(3)
|
||||
bundle.putString(USER_DATA_MAIN_ACCOUNT_NAME, mainAccount.name)
|
||||
bundle.putString(USER_DATA_MAIN_ACCOUNT_TYPE, mainAccount.type)
|
||||
bundle.putString(USER_DATA_URL, url)
|
||||
return bundle
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether contact groups (LocalGroup resources) are included in query results for
|
||||
@ -57,100 +128,44 @@ class LocalAddressBook(protected val context: Context, account: Account, provide
|
||||
*/
|
||||
var includeGroups = true
|
||||
|
||||
/**
|
||||
* Returns an array of local contacts/groups which have been deleted locally. (DELETED != 0).
|
||||
*/
|
||||
override val deleted: Array<LocalResource>
|
||||
@Throws(ContactsStorageException::class)
|
||||
get() {
|
||||
val deleted = LinkedList<LocalResource>()
|
||||
Collections.addAll(deleted, *deletedContacts)
|
||||
if (includeGroups)
|
||||
Collections.addAll(deleted, *deletedGroups)
|
||||
return deleted.toTypedArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of local contacts/groups which have been changed locally (DIRTY != 0).
|
||||
*/
|
||||
override val dirty: Array<LocalResource>
|
||||
@Throws(ContactsStorageException::class)
|
||||
get() {
|
||||
val dirty = LinkedList<LocalResource>()
|
||||
Collections.addAll(dirty, *dirtyContacts)
|
||||
if (includeGroups)
|
||||
Collections.addAll(dirty, *dirtyGroups)
|
||||
return dirty.toTypedArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of local contacts which don't have a file name yet.
|
||||
*/
|
||||
override val withoutFileName: Array<LocalResource>
|
||||
@Throws(ContactsStorageException::class)
|
||||
get() {
|
||||
val nameless = LinkedList<LocalResource>()
|
||||
Collections.addAll(nameless, *queryContacts(AndroidContact.COLUMN_FILENAME + " IS NULL", null) as Array<LocalContact>)
|
||||
if (includeGroups)
|
||||
Collections.addAll(nameless, *queryGroups(AndroidGroup.COLUMN_FILENAME + " IS NULL", null) as Array<LocalGroup>)
|
||||
return nameless.toTypedArray()
|
||||
}
|
||||
|
||||
val deletedContacts: Array<LocalContact>
|
||||
@Throws(ContactsStorageException::class)
|
||||
get() = queryContacts(RawContacts.DELETED + "!= 0", null) as Array<LocalContact>
|
||||
|
||||
val dirtyContacts: Array<LocalContact>
|
||||
@Throws(ContactsStorageException::class)
|
||||
get() = queryContacts(RawContacts.DIRTY + "!= 0 AND " + RawContacts.DELETED + "== 0", null) as Array<LocalContact>
|
||||
|
||||
val all: Array<LocalContact>
|
||||
@Throws(ContactsStorageException::class)
|
||||
get() = queryContacts(RawContacts.DELETED + "== 0", null) as Array<LocalContact>
|
||||
|
||||
val deletedGroups: Array<LocalGroup>
|
||||
@Throws(ContactsStorageException::class)
|
||||
get() = queryGroups(Groups.DELETED + "!= 0", null) as Array<LocalGroup>
|
||||
|
||||
val dirtyGroups: Array<LocalGroup>
|
||||
@Throws(ContactsStorageException::class)
|
||||
get() = queryGroups(Groups.DIRTY + "!= 0 AND " + Groups.DELETED + "== 0", null) as Array<LocalGroup>
|
||||
|
||||
private var _mainAccount: Account? = null
|
||||
var mainAccount: Account
|
||||
@Throws(ContactsStorageException::class)
|
||||
get() {
|
||||
val accountManager = AccountManager.get(context)
|
||||
val name = accountManager.getUserData(account, USER_DATA_MAIN_ACCOUNT_NAME)
|
||||
val type = accountManager.getUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE)
|
||||
return if (name != null && type != null)
|
||||
Account(name, type)
|
||||
else
|
||||
throw ContactsStorageException("Address book doesn't exist anymore")
|
||||
_mainAccount?.let { return it }
|
||||
|
||||
AccountManager.get(context).let { accountManager ->
|
||||
val name = accountManager.getUserData(account, USER_DATA_MAIN_ACCOUNT_NAME)
|
||||
val type = accountManager.getUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE)
|
||||
if (name != null && type != null)
|
||||
return Account(name, type)
|
||||
else
|
||||
throw IllegalStateException("Address book doesn't exist anymore")
|
||||
}
|
||||
}
|
||||
@Throws(ContactsStorageException::class)
|
||||
set(mainAccount) {
|
||||
val accountManager = AccountManager.get(context)
|
||||
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_NAME, mainAccount.name)
|
||||
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE, mainAccount.type)
|
||||
set(newMainAccount) {
|
||||
AccountManager.get(context).let { accountManager ->
|
||||
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_NAME, newMainAccount.name)
|
||||
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE, newMainAccount.type)
|
||||
}
|
||||
|
||||
_mainAccount = newMainAccount
|
||||
}
|
||||
|
||||
var url: String?
|
||||
@Throws(ContactsStorageException::class)
|
||||
get() {
|
||||
val accountManager = AccountManager.get(context)
|
||||
return accountManager.getUserData(account, USER_DATA_URL)
|
||||
}
|
||||
@Throws(ContactsStorageException::class)
|
||||
set(url) {
|
||||
val accountManager = AccountManager.get(context)
|
||||
accountManager.setUserData(account, USER_DATA_URL, url)
|
||||
}
|
||||
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)
|
||||
|
||||
var readOnly: Boolean
|
||||
get() = AccountManager.get(context).getUserData(account, USER_DATA_READ_ONLY) != null
|
||||
set(readOnly) = AccountManager.get(context).setUserData(account, USER_DATA_READ_ONLY, if (readOnly) "1" else null)
|
||||
|
||||
@Throws(AuthenticatorException::class, OperationCanceledException::class, IOException::class, ContactsStorageException::class, android.accounts.OperationCanceledException::class)
|
||||
fun update(journalEntity: JournalEntity) {
|
||||
val info = journalEntity.info
|
||||
val newAccountName = accountName(mainAccount, info)
|
||||
if (account.name != newAccountName && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
if (account.name != newAccountName && Build.VERSION.SDK_INT >= 21) {
|
||||
val accountManager = AccountManager.get(context)
|
||||
val future = accountManager.renameAccount(account, newAccountName, {
|
||||
try {
|
||||
@ -168,22 +183,61 @@ class LocalAddressBook(protected val context: Context, account: Account, provide
|
||||
account = future.result
|
||||
}
|
||||
|
||||
App.log.info("Address book write permission? = ${journalEntity.isReadOnly}")
|
||||
readOnly = journalEntity.isReadOnly
|
||||
|
||||
// make sure it will still be synchronized when contacts are updated
|
||||
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
|
||||
}
|
||||
|
||||
fun delete() {
|
||||
val accountManager = AccountManager.get(context)
|
||||
AndroidCompat.removeAccount(accountManager, account)
|
||||
@Suppress("DEPRECATION")
|
||||
if (Build.VERSION.SDK_INT >= 22)
|
||||
accountManager.removeAccount(account, null, null, null)
|
||||
else
|
||||
accountManager.removeAccount(account, null, null)
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class, FileNotFoundException::class)
|
||||
fun findContactByUID(uid: String): LocalContact {
|
||||
val contacts = queryContacts(LocalContact.COLUMN_UID + "=?", arrayOf(uid)) as Array<LocalContact>
|
||||
if (contacts.size == 0)
|
||||
throw FileNotFoundException()
|
||||
return contacts[0]
|
||||
}
|
||||
override fun findAll(): List<LocalContact> = queryContacts(RawContacts.DELETED + "== 0", null)
|
||||
|
||||
/**
|
||||
* Returns an array of local contacts/groups which have been deleted locally. (DELETED != 0).
|
||||
* @throws RemoteException on content provider errors
|
||||
*/
|
||||
override fun findDeleted() =
|
||||
if (includeGroups)
|
||||
findDeletedContacts() + findDeletedGroups()
|
||||
else
|
||||
findDeletedContacts()
|
||||
|
||||
fun findDeletedContacts() = queryContacts("${RawContacts.DELETED}!=0", null)
|
||||
fun findDeletedGroups() = queryGroups("${Groups.DELETED}!=0", null)
|
||||
|
||||
/**
|
||||
* Returns an array of local contacts/groups which have been changed locally (DIRTY != 0).
|
||||
* @throws RemoteException on content provider errors
|
||||
*/
|
||||
override fun findDirty() =
|
||||
if (includeGroups)
|
||||
findDirtyContacts() + findDirtyGroups()
|
||||
else
|
||||
findDirtyContacts()
|
||||
|
||||
fun findDirtyContacts() = queryContacts("${RawContacts.DIRTY}!=0", null)
|
||||
fun findDirtyGroups() = queryGroups("${Groups.DIRTY}!=0", null)
|
||||
|
||||
/**
|
||||
* Returns an array of local contacts which don't have a file name yet.
|
||||
*/
|
||||
override fun findWithoutFileName() =
|
||||
if (includeGroups)
|
||||
findWithoutFileNameContacts() + findWithoutFileNameGroups()
|
||||
else
|
||||
findWithoutFileNameContacts()
|
||||
|
||||
fun findWithoutFileNameContacts() = queryContacts("${AndroidContact.COLUMN_FILENAME} IS NULL", null)
|
||||
fun findWithoutFileNameGroups() = queryGroups("${AndroidGroup.COLUMN_FILENAME} IS NULL", null)
|
||||
|
||||
/**
|
||||
* Queries all contacts with DIRTY flag and checks whether their data checksum has changed, i.e.
|
||||
@ -192,52 +246,39 @@ class LocalAddressBook(protected val context: Context, account: Account, provide
|
||||
* whose contact data checksum has not changed.
|
||||
* @return number of "really dirty" contacts
|
||||
*/
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun verifyDirty(): Int {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
|
||||
App.log.severe("verifyDirty() should not be called on Android <7")
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
throw IllegalStateException("verifyDirty() should not be called on Android != 7")
|
||||
|
||||
var reallyDirty = 0
|
||||
for (contact in dirtyContacts) {
|
||||
try {
|
||||
val lastHash = contact.lastHashCode
|
||||
val currentHash = contact.dataHashCode()
|
||||
if (lastHash == currentHash) {
|
||||
// hash is code still the same, contact is not "really dirty" (only metadata been have changed)
|
||||
App.log.log(Level.FINE, "Contact data hash has not changed, resetting dirty flag", contact)
|
||||
contact.resetDirty()
|
||||
} else {
|
||||
App.log.log(Level.FINE, "Contact data has changed from hash $lastHash to $currentHash", contact)
|
||||
reallyDirty++
|
||||
}
|
||||
} catch (e: FileNotFoundException) {
|
||||
throw ContactsStorageException("Couldn't calculate hash code", e)
|
||||
for (contact in findDirtyContacts()) {
|
||||
val lastHash = contact.getLastHashCode()
|
||||
val currentHash = contact.dataHashCode()
|
||||
if (lastHash == currentHash) {
|
||||
// hash is code still the same, contact is not "really dirty" (only metadata been have changed)
|
||||
App.log.log(Level.FINE, "Contact data hash has not changed, resetting dirty flag", contact)
|
||||
contact.resetDirty()
|
||||
} else {
|
||||
App.log.log(Level.FINE, "Contact data has changed from hash $lastHash to $currentHash", contact)
|
||||
reallyDirty++
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (includeGroups)
|
||||
reallyDirty += dirtyGroups.size
|
||||
reallyDirty += findDirtyGroups().size
|
||||
|
||||
return reallyDirty
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
override fun getByUid(uid: String): LocalResource? {
|
||||
val ret = queryContacts(AndroidContact.COLUMN_FILENAME + " =? ", arrayOf(uid)) as Array<LocalContact>
|
||||
return if (ret != null && ret.size > 0) {
|
||||
ret[0]
|
||||
} else null
|
||||
}
|
||||
override fun findByUid(uid: String): LocalAddress? = findContactByUID(uid)
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
override fun count(): Long {
|
||||
try {
|
||||
val cursor = provider.query(syncAdapterURI(RawContacts.CONTENT_URI), null, null, null, null)
|
||||
val cursor = provider?.query(syncAdapterURI(RawContacts.CONTENT_URI), null, null, null, null)
|
||||
try {
|
||||
return cursor.count.toLong()
|
||||
return cursor?.count?.toLong()!!
|
||||
} finally {
|
||||
cursor.close()
|
||||
cursor?.close()
|
||||
}
|
||||
} catch (e: RemoteException) {
|
||||
throw ContactsStorageException("Couldn't query contacts", e)
|
||||
@ -245,37 +286,10 @@ class LocalAddressBook(protected val context: Context, account: Account, provide
|
||||
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
internal fun getByGroupMembership(groupID: Long): Array<LocalContact> {
|
||||
try {
|
||||
val cursor = provider.query(syncAdapterURI(ContactsContract.Data.CONTENT_URI),
|
||||
arrayOf(RawContacts.Data.RAW_CONTACT_ID),
|
||||
"(" + GroupMembership.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?) OR (" + CachedGroupMembership.MIMETYPE + "=? AND " + CachedGroupMembership.GROUP_ID + "=?)",
|
||||
arrayOf(GroupMembership.CONTENT_ITEM_TYPE, groupID.toString(), CachedGroupMembership.CONTENT_ITEM_TYPE, groupID.toString()), null)
|
||||
|
||||
val ids = HashSet<Long>()
|
||||
while (cursor != null && cursor.moveToNext())
|
||||
ids.add(cursor.getLong(0))
|
||||
|
||||
cursor!!.close()
|
||||
|
||||
val contacts = arrayOfNulls<LocalContact>(ids.size)
|
||||
var i = 0
|
||||
for (id in ids)
|
||||
contacts[i++] = LocalContact(this, id, null, null)
|
||||
return contacts
|
||||
} catch (e: RemoteException) {
|
||||
throw ContactsStorageException("Couldn't query contacts", e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun deleteAll() {
|
||||
try {
|
||||
provider.delete(syncAdapterURI(RawContacts.CONTENT_URI), null, null)
|
||||
provider.delete(syncAdapterURI(Groups.CONTENT_URI), null, null)
|
||||
provider?.delete(syncAdapterURI(RawContacts.CONTENT_URI), null, null)
|
||||
provider?.delete(syncAdapterURI(Groups.CONTENT_URI), null, null)
|
||||
} catch (e: RemoteException) {
|
||||
throw ContactsStorageException("Couldn't delete all local contacts and groups", e)
|
||||
}
|
||||
@ -283,60 +297,56 @@ class LocalAddressBook(protected val context: Context, account: Account, provide
|
||||
}
|
||||
|
||||
|
||||
/* special group operations */
|
||||
fun getByGroupMembership(groupID: Long): List<LocalContact> {
|
||||
val ids = HashSet<Long>()
|
||||
provider!!.query(syncAdapterURI(ContactsContract.Data.CONTENT_URI),
|
||||
arrayOf(RawContacts.Data.RAW_CONTACT_ID),
|
||||
"(${GroupMembership.MIMETYPE}=? AND ${GroupMembership.GROUP_ROW_ID}=?) OR (${CachedGroupMembership.MIMETYPE}=? AND ${CachedGroupMembership.GROUP_ID}=?)",
|
||||
arrayOf(GroupMembership.CONTENT_ITEM_TYPE, groupID.toString(), CachedGroupMembership.CONTENT_ITEM_TYPE, groupID.toString()),
|
||||
null)?.use { cursor ->
|
||||
while (cursor.moveToNext())
|
||||
ids += cursor.getLong(0)
|
||||
}
|
||||
|
||||
return ids.map { findContactByID(it) }
|
||||
}
|
||||
|
||||
|
||||
/* special group operations */
|
||||
|
||||
/**
|
||||
* Finds the first group with the given title. If there is no group with this
|
||||
* title, a new group is created.
|
||||
* @param title title of the group to look for
|
||||
* @return id of the group with given title
|
||||
* @throws ContactsStorageException on contact provider errors
|
||||
* @param title title of the group to look for
|
||||
* @return id of the group with given title
|
||||
* @throws RemoteException on content provider errors
|
||||
*/
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun findOrCreateGroup(title: String): Long {
|
||||
try {
|
||||
val cursor = provider.query(syncAdapterURI(Groups.CONTENT_URI),
|
||||
arrayOf(Groups._ID),
|
||||
Groups.TITLE + "=?", arrayOf(title), null)
|
||||
try {
|
||||
if (cursor != null && cursor.moveToNext())
|
||||
return cursor.getLong(0)
|
||||
} finally {
|
||||
cursor!!.close()
|
||||
}
|
||||
|
||||
val values = ContentValues()
|
||||
values.put(Groups.TITLE, title)
|
||||
val uri = provider.insert(syncAdapterURI(Groups.CONTENT_URI), values)
|
||||
return ContentUris.parseId(uri)
|
||||
} catch (e: RemoteException) {
|
||||
throw ContactsStorageException("Couldn't find local contact group", e)
|
||||
provider!!.query(syncAdapterURI(Groups.CONTENT_URI), arrayOf(Groups._ID),
|
||||
"${Groups.TITLE}=?", arrayOf(title), null)?.use { cursor ->
|
||||
if (cursor.moveToNext())
|
||||
return cursor.getLong(0)
|
||||
}
|
||||
|
||||
val values = ContentValues(1)
|
||||
values.put(Groups.TITLE, title)
|
||||
val uri = provider.insert(syncAdapterURI(Groups.CONTENT_URI), values)
|
||||
return ContentUris.parseId(uri)
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun removeEmptyGroups() {
|
||||
// find groups without members
|
||||
/** should be done using [Groups.SUMMARY_COUNT], but it's not implemented in Android yet */
|
||||
for (group in queryGroups(null, null) as Array<LocalGroup>)
|
||||
if (group.members.size == 0) {
|
||||
App.log.log(Level.FINE, "Deleting group", group)
|
||||
group.delete()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun removeGroups() {
|
||||
try {
|
||||
provider.delete(syncAdapterURI(Groups.CONTENT_URI), null, null)
|
||||
} catch (e: RemoteException) {
|
||||
throw ContactsStorageException("Couldn't remove all groups", e)
|
||||
/** should be done using {@link Groups.SUMMARY_COUNT}, but it's not implemented in Android yet */
|
||||
queryGroups(null, null).filter { it.getMembers().isEmpty() }.forEach { group ->
|
||||
App.log.log(Level.FINE, "Deleting group", group)
|
||||
group.delete()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/** Fix all of the etags of all of the non-dirty contacts to be non-null.
|
||||
* Currently set to all ones. */
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun fixEtags() {
|
||||
val newEtag = "1111111111111111111111111111111111111111111111111111111111111111"
|
||||
val where = ContactsContract.RawContacts.DIRTY + "=0 AND " + AndroidContact.COLUMN_ETAG + " IS NULL"
|
||||
@ -344,7 +354,7 @@ class LocalAddressBook(protected val context: Context, account: Account, provide
|
||||
val values = ContentValues(1)
|
||||
values.put(AndroidContact.COLUMN_ETAG, newEtag)
|
||||
try {
|
||||
val fixed = provider.update(syncAdapterURI(RawContacts.CONTENT_URI),
|
||||
val fixed = provider?.update(syncAdapterURI(RawContacts.CONTENT_URI),
|
||||
values, where, null)
|
||||
App.log.info("Fixed entries: " + fixed.toString())
|
||||
} catch (e: RemoteException) {
|
||||
@ -352,81 +362,4 @@ class LocalAddressBook(protected val context: Context, account: Account, provide
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
protected val USER_DATA_MAIN_ACCOUNT_TYPE = "real_account_type"
|
||||
protected val USER_DATA_MAIN_ACCOUNT_NAME = "real_account_name"
|
||||
protected val USER_DATA_URL = "url"
|
||||
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun find(context: Context, provider: ContentProviderClient, mainAccount: Account?): Array<LocalAddressBook> {
|
||||
val accountManager = AccountManager.get(context)
|
||||
|
||||
val result = LinkedList<LocalAddressBook>()
|
||||
for (account in accountManager.getAccountsByType(App.addressBookAccountType)) {
|
||||
val addressBook = LocalAddressBook(context, account, provider)
|
||||
if (mainAccount == null || addressBook.mainAccount == mainAccount)
|
||||
result.add(addressBook)
|
||||
}
|
||||
|
||||
return result.toTypedArray()
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun findByUid(context: Context, provider: ContentProviderClient, mainAccount: Account?, uid: String): LocalAddressBook? {
|
||||
val accountManager = AccountManager.get(context)
|
||||
|
||||
for (account in accountManager.getAccountsByType(App.addressBookAccountType)) {
|
||||
val addressBook = LocalAddressBook(context, account, provider)
|
||||
if (addressBook.url == uid && (mainAccount == null || addressBook.mainAccount == mainAccount))
|
||||
return addressBook
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun create(context: Context, provider: ContentProviderClient, mainAccount: Account, journalEntity: JournalEntity): LocalAddressBook {
|
||||
val info = journalEntity.info
|
||||
val accountManager = AccountManager.get(context)
|
||||
|
||||
val account = Account(accountName(mainAccount, info), App.addressBookAccountType)
|
||||
if (!accountManager.addAccountExplicitly(account, null, null))
|
||||
throw ContactsStorageException("Couldn't create address book account")
|
||||
|
||||
setUserData(accountManager, account, mainAccount, info.uid!!)
|
||||
val addressBook = LocalAddressBook(context, account, provider)
|
||||
addressBook.mainAccount = mainAccount
|
||||
addressBook.url = info.uid
|
||||
|
||||
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
|
||||
|
||||
return addressBook
|
||||
}
|
||||
|
||||
|
||||
// SETTINGS
|
||||
|
||||
// XXX: Workaround a bug in Android where passing a bundle to addAccountExplicitly doesn't work.
|
||||
fun setUserData(accountManager: AccountManager, account: Account, mainAccount: Account, url: String) {
|
||||
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_NAME, mainAccount.name)
|
||||
accountManager.setUserData(account, USER_DATA_MAIN_ACCOUNT_TYPE, mainAccount.type)
|
||||
accountManager.setUserData(account, USER_DATA_URL, url)
|
||||
}
|
||||
|
||||
// HELPERS
|
||||
|
||||
fun accountName(mainAccount: Account, info: CollectionInfo): String {
|
||||
val displayName = if (info.displayName != null) info.displayName else info.uid
|
||||
val sb = StringBuilder(displayName)
|
||||
sb.append(" (")
|
||||
.append(mainAccount.name)
|
||||
.append(" ")
|
||||
.append(info.uid!!.substring(0, 4))
|
||||
.append(")")
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,82 +13,114 @@ import android.content.ContentProviderClient
|
||||
import android.content.ContentProviderOperation
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.RemoteException
|
||||
import android.provider.CalendarContract
|
||||
import android.provider.CalendarContract.Calendars
|
||||
import android.provider.CalendarContract.Events
|
||||
import android.provider.CalendarContract.Reminders
|
||||
import android.text.TextUtils
|
||||
|
||||
import android.provider.CalendarContract.*
|
||||
import at.bitfire.ical4android.*
|
||||
import com.etesync.syncadapter.App
|
||||
import com.etesync.syncadapter.model.CollectionInfo
|
||||
import com.etesync.syncadapter.model.JournalEntity
|
||||
|
||||
import net.fortuna.ical4j.model.component.VTimeZone
|
||||
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
|
||||
import java.io.FileNotFoundException
|
||||
import java.util.LinkedList
|
||||
import java.util.*
|
||||
import java.util.logging.Level
|
||||
|
||||
import at.bitfire.ical4android.AndroidCalendar
|
||||
import at.bitfire.ical4android.AndroidCalendarFactory
|
||||
import at.bitfire.ical4android.BatchOperation
|
||||
import at.bitfire.ical4android.CalendarStorageException
|
||||
import at.bitfire.ical4android.DateUtils
|
||||
class LocalCalendar private constructor(
|
||||
account: Account,
|
||||
provider: ContentProviderClient,
|
||||
id: Long
|
||||
): AndroidCalendar<LocalEvent>(account, provider, LocalEvent.Factory, id), LocalCollection<LocalEvent> {
|
||||
|
||||
class LocalCalendar protected constructor(account: Account, provider: ContentProviderClient, id: Long) : AndroidCalendar(account, provider, LocalEvent.Factory.INSTANCE, id), LocalCollection<LocalEvent> {
|
||||
companion object {
|
||||
val defaultColor = -0x743cb6 // light green 500
|
||||
|
||||
override val deleted: Array<LocalEvent>
|
||||
@Throws(CalendarStorageException::class)
|
||||
get() = queryEvents(Events.DELETED + "!=0 AND " + Events.ORIGINAL_ID + " IS NULL", null) as Array<LocalEvent>
|
||||
val COLUMN_CTAG = Calendars.CAL_SYNC1
|
||||
|
||||
override val withoutFileName: Array<LocalEvent>
|
||||
@Throws(CalendarStorageException::class)
|
||||
get() = queryEvents(Events._SYNC_ID + " IS NULL AND " + Events.ORIGINAL_ID + " IS NULL", null) as Array<LocalEvent>
|
||||
fun create(account: Account, provider: ContentProviderClient, journalEntity: JournalEntity): Uri {
|
||||
val values = valuesFromCollectionInfo(journalEntity, true)
|
||||
|
||||
// ACCOUNT_NAME and ACCOUNT_TYPE are required (see docs)! If it's missing, other apps will crash.
|
||||
values.put(Calendars.ACCOUNT_NAME, account.name)
|
||||
values.put(Calendars.ACCOUNT_TYPE, account.type)
|
||||
values.put(Calendars.OWNER_ACCOUNT, account.name)
|
||||
|
||||
val all: Array<LocalEvent>
|
||||
@Throws(CalendarStorageException::class)
|
||||
get() = queryEvents(null, null) as Array<LocalEvent>
|
||||
// flag as visible & synchronizable at creation, might be changed by user at any time
|
||||
values.put(Calendars.VISIBLE, 1)
|
||||
values.put(Calendars.SYNC_EVENTS, 1)
|
||||
|
||||
override// get dirty events which are required to have an increased SEQUENCE value
|
||||
// sequence has not been assigned yet (i.e. this event was just locally created)
|
||||
val dirty: Array<LocalEvent>
|
||||
@Throws(CalendarStorageException::class, FileNotFoundException::class)
|
||||
get() {
|
||||
val dirty = LinkedList<LocalEvent>()
|
||||
for (event in queryEvents(Events.DIRTY + "!=0 AND " + Events.DELETED + "==0 AND " + Events.ORIGINAL_ID + " IS NULL", null) as Array<LocalEvent>) {
|
||||
if (event.event.sequence == null)
|
||||
event.event.sequence = 0
|
||||
else if (event.weAreOrganizer)
|
||||
event.event.sequence++
|
||||
dirty.add(event)
|
||||
}
|
||||
|
||||
return dirty.toTypedArray()
|
||||
return AndroidCalendar.create(account, provider, values)
|
||||
}
|
||||
|
||||
override fun eventBaseInfoColumns(): Array<String> {
|
||||
return BASE_INFO_COLUMNS
|
||||
fun findByName(account: Account, provider: ContentProviderClient, factory: Factory, name: String): LocalCalendar?
|
||||
= AndroidCalendar.find(account, provider, factory, Calendars.NAME + "==?", arrayOf(name)).firstOrNull()
|
||||
|
||||
private fun valuesFromCollectionInfo(journalEntity: JournalEntity, withColor: Boolean): ContentValues {
|
||||
val info = journalEntity.info
|
||||
val values = ContentValues()
|
||||
values.put(Calendars.NAME, info.uid)
|
||||
values.put(Calendars.CALENDAR_DISPLAY_NAME, info.displayName)
|
||||
|
||||
if (withColor)
|
||||
values.put(Calendars.CALENDAR_COLOR, if (info.color != null) info.color else defaultColor)
|
||||
|
||||
if (journalEntity.isReadOnly)
|
||||
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ)
|
||||
else {
|
||||
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER)
|
||||
values.put(Calendars.CAN_MODIFY_TIME_ZONE, 1)
|
||||
values.put(Calendars.CAN_ORGANIZER_RESPOND, 1)
|
||||
}
|
||||
|
||||
info.timeZone?.let { tzData ->
|
||||
try {
|
||||
val timeZone = DateUtils.parseVTimeZone(tzData)
|
||||
timeZone.timeZoneId?.let { tzId ->
|
||||
values.put(Calendars.CALENDAR_TIME_ZONE, DateUtils.findAndroidTimezoneID(tzId.value))
|
||||
}
|
||||
} catch(e: IllegalArgumentException) {
|
||||
App.log.log(Level.WARNING, "Couldn't parse calendar default time zone", e)
|
||||
}
|
||||
}
|
||||
values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT)
|
||||
values.put(Calendars.ALLOWED_AVAILABILITY, StringUtils.join(intArrayOf(Reminders.AVAILABILITY_TENTATIVE, Reminders.AVAILABILITY_FREE, Reminders.AVAILABILITY_BUSY), ","))
|
||||
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, StringUtils.join(intArrayOf(CalendarContract.Attendees.TYPE_OPTIONAL, CalendarContract.Attendees.TYPE_REQUIRED, CalendarContract.Attendees.TYPE_RESOURCE), ", "))
|
||||
return values
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
fun update(journalEntity: JournalEntity, updateColor: Boolean) {
|
||||
update(valuesFromCollectionInfo(journalEntity, updateColor))
|
||||
fun update(journalEntity: JournalEntity, updateColor: Boolean) =
|
||||
update(valuesFromCollectionInfo(journalEntity, updateColor))
|
||||
|
||||
|
||||
override fun findDeleted() =
|
||||
queryEvents("${Events.DELETED}!=0 AND ${Events.ORIGINAL_ID} IS NULL", null)
|
||||
|
||||
override fun findDirty(): List<LocalEvent> {
|
||||
val dirty = LinkedList<LocalEvent>()
|
||||
|
||||
// get dirty events which are required to have an increased SEQUENCE value
|
||||
for (localEvent in queryEvents("${Events.DIRTY}!=0 AND ${Events.ORIGINAL_ID} IS NULL", null)) {
|
||||
val event = localEvent.event!!
|
||||
val sequence = event.sequence
|
||||
if (event.sequence == null) // sequence has not been assigned yet (i.e. this event was just locally created)
|
||||
event.sequence = 0
|
||||
else if (localEvent.weAreOrganizer)
|
||||
event.sequence = sequence!! + 1
|
||||
dirty += localEvent
|
||||
}
|
||||
|
||||
return dirty
|
||||
}
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun getByUid(uid: String): LocalEvent? {
|
||||
val ret = queryEvents(Events._SYNC_ID + " =? ", arrayOf(uid)) as Array<LocalEvent>
|
||||
return if (ret != null && ret.size > 0) {
|
||||
ret[0]
|
||||
} else null
|
||||
}
|
||||
override fun findWithoutFileName(): List<LocalEvent>
|
||||
= queryEvents(Events._SYNC_ID + " IS NULL AND " + Events.ORIGINAL_ID + " IS NULL", null)
|
||||
|
||||
override fun findAll(): List<LocalEvent>
|
||||
= queryEvents(null, null)
|
||||
|
||||
override fun findByUid(uid: String): LocalEvent?
|
||||
= queryEvents(Events._SYNC_ID + " =? ", arrayOf(uid)).firstOrNull()
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
fun processDirtyExceptions() {
|
||||
// process deleted exceptions
|
||||
App.log.info("Processing deleted exceptions")
|
||||
@ -163,19 +195,15 @@ class LocalCalendar protected constructor(account: Account, provider: ContentPro
|
||||
|
||||
}
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun count(): Long {
|
||||
val where = Events.CALENDAR_ID + "=?"
|
||||
val whereArgs = arrayOf(id.toString())
|
||||
|
||||
try {
|
||||
val cursor = provider.query(
|
||||
syncAdapterURI(Events.CONTENT_URI), null,
|
||||
where, whereArgs, null)
|
||||
Events.CALENDAR_ID + "=?", arrayOf(id.toString()), null)
|
||||
try {
|
||||
return cursor.count.toLong()
|
||||
return cursor?.count?.toLong()!!
|
||||
} finally {
|
||||
cursor.close()
|
||||
cursor?.close()
|
||||
}
|
||||
} catch (e: RemoteException) {
|
||||
throw CalendarStorageException("Couldn't query calendar events", e)
|
||||
@ -183,20 +211,6 @@ class LocalCalendar protected constructor(account: Account, provider: ContentPro
|
||||
|
||||
}
|
||||
|
||||
class Factory : AndroidCalendarFactory {
|
||||
|
||||
override fun newInstance(account: Account, provider: ContentProviderClient, id: Long): AndroidCalendar {
|
||||
return LocalCalendar(account, provider, id)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<AndroidCalendar?> {
|
||||
return arrayOfNulls<LocalCalendar>(size) as Array<AndroidCalendar?>
|
||||
}
|
||||
|
||||
companion object {
|
||||
val INSTANCE = Factory()
|
||||
}
|
||||
}
|
||||
|
||||
/** Fix all of the etags of all of the non-dirty events to be non-null.
|
||||
* Currently set to all ones.. */
|
||||
@ -218,67 +232,9 @@ class LocalCalendar protected constructor(account: Account, provider: ContentPro
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
object Factory: AndroidCalendarFactory<LocalCalendar> {
|
||||
|
||||
val defaultColor = -0x743cb6 // light green 500
|
||||
|
||||
val COLUMN_CTAG = Calendars.CAL_SYNC1
|
||||
|
||||
internal var BASE_INFO_COLUMNS = arrayOf(Events._ID, Events._SYNC_ID, LocalEvent.COLUMN_ETAG)
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
fun create(account: Account, provider: ContentProviderClient, journalEntity: JournalEntity): Uri {
|
||||
val values = valuesFromCollectionInfo(journalEntity, true)
|
||||
|
||||
// ACCOUNT_NAME and ACCOUNT_TYPE are required (see docs)! If it's missing, other apps will crash.
|
||||
values.put(Calendars.ACCOUNT_NAME, account.name)
|
||||
values.put(Calendars.ACCOUNT_TYPE, account.type)
|
||||
values.put(Calendars.OWNER_ACCOUNT, account.name)
|
||||
|
||||
// flag as visible & synchronizable at creation, might be changed by user at any time
|
||||
values.put(Calendars.VISIBLE, 1)
|
||||
values.put(Calendars.SYNC_EVENTS, 1)
|
||||
|
||||
return AndroidCalendar.create(account, provider, values)
|
||||
}
|
||||
|
||||
@Throws(FileNotFoundException::class, CalendarStorageException::class)
|
||||
fun findByName(account: Account, provider: ContentProviderClient, factory: AndroidCalendarFactory, name: String): LocalCalendar? {
|
||||
val ret = LocalCalendar.find(account, provider, factory, Calendars.NAME + "==?", arrayOf(name))
|
||||
if (ret.size == 1) {
|
||||
return ret[0]
|
||||
} else {
|
||||
App.log.severe("No calendar found for name $name")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private fun valuesFromCollectionInfo(journalEntity: JournalEntity, withColor: Boolean): ContentValues {
|
||||
val info = journalEntity.info
|
||||
val values = ContentValues()
|
||||
values.put(Calendars.NAME, info.uid)
|
||||
values.put(Calendars.CALENDAR_DISPLAY_NAME, info.displayName)
|
||||
|
||||
if (withColor)
|
||||
values.put(Calendars.CALENDAR_COLOR, if (info.color != null) info.color else defaultColor)
|
||||
|
||||
if (journalEntity.isReadOnly)
|
||||
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ)
|
||||
else {
|
||||
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER)
|
||||
values.put(Calendars.CAN_MODIFY_TIME_ZONE, 1)
|
||||
values.put(Calendars.CAN_ORGANIZER_RESPOND, 1)
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(info.timeZone)) {
|
||||
val timeZone = DateUtils.parseVTimeZone(info.timeZone)
|
||||
if (timeZone != null && timeZone.timeZoneId != null)
|
||||
values.put(Calendars.CALENDAR_TIME_ZONE, DateUtils.findAndroidTimezoneID(timeZone.timeZoneId.value))
|
||||
}
|
||||
values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT)
|
||||
values.put(Calendars.ALLOWED_AVAILABILITY, StringUtils.join(intArrayOf(Reminders.AVAILABILITY_TENTATIVE, Reminders.AVAILABILITY_FREE, Reminders.AVAILABILITY_BUSY), ","))
|
||||
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, StringUtils.join(intArrayOf(CalendarContract.Attendees.TYPE_OPTIONAL, CalendarContract.Attendees.TYPE_REQUIRED, CalendarContract.Attendees.TYPE_RESOURCE), ", "))
|
||||
return values
|
||||
}
|
||||
override fun newInstance(account: Account, provider: ContentProviderClient, id: Long) =
|
||||
LocalCalendar(account, provider, id)
|
||||
}
|
||||
}
|
||||
|
@ -8,21 +8,14 @@
|
||||
|
||||
package com.etesync.syncadapter.resource
|
||||
|
||||
import java.io.FileNotFoundException
|
||||
interface LocalCollection<out T: LocalResource<*>> {
|
||||
fun findDeleted(): List<T>
|
||||
fun findDirty(): List<T>
|
||||
fun findWithoutFileName(): List<T>
|
||||
fun findAll(): List<T>
|
||||
|
||||
import at.bitfire.ical4android.CalendarStorageException
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
fun findByUid(uid: String): T?
|
||||
|
||||
interface LocalCollection<T> {
|
||||
|
||||
val deleted: Array<T>
|
||||
val withoutFileName: Array<T>
|
||||
/** Dirty *non-deleted* entries */
|
||||
val dirty: Array<T>
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class)
|
||||
fun getByUid(uid: String): T?
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class)
|
||||
fun count(): Long
|
||||
}
|
||||
|
@ -42,7 +42,14 @@ import ezvcard.VCardVersion
|
||||
|
||||
import at.bitfire.vcard4android.GroupMethod.GROUP_VCARDS
|
||||
|
||||
class LocalContact : AndroidContact, LocalResource {
|
||||
class LocalContact : AndroidContact, LocalAddress {
|
||||
companion object {
|
||||
init {
|
||||
Contact.productID = Constants.PRODID_BASE + " ez-vcard/" + Ezvcard.VERSION
|
||||
}
|
||||
|
||||
internal const val COLUMN_HASHCODE = ContactsContract.RawContacts.SYNC3
|
||||
}
|
||||
|
||||
private var saveAsDirty = false // When true, the resource will be saved as dirty
|
||||
|
||||
@ -57,7 +64,6 @@ class LocalContact : AndroidContact, LocalResource {
|
||||
get() = TextUtils.isEmpty(eTag)
|
||||
|
||||
override val content: String
|
||||
@Throws(IOException::class, ContactsStorageException::class)
|
||||
get() {
|
||||
val contact: Contact
|
||||
contact = this.contact!!
|
||||
@ -70,97 +76,64 @@ class LocalContact : AndroidContact, LocalResource {
|
||||
return os.toString()
|
||||
}
|
||||
|
||||
val lastHashCode: Int
|
||||
@Throws(ContactsStorageException::class)
|
||||
get() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
|
||||
App.log.severe("getLastHashCode() should not be called on Android <7")
|
||||
constructor(addressBook: AndroidAddressBook<LocalContact,*>, values: ContentValues)
|
||||
: super(addressBook, values) {}
|
||||
|
||||
try {
|
||||
val c = addressBook.provider.query(rawContactSyncURI(), arrayOf(COLUMN_HASHCODE), null, null, null)
|
||||
try {
|
||||
return if (c == null || !c.moveToNext() || c.isNull(0)) 0 else c.getInt(0)
|
||||
} finally {
|
||||
c?.close()
|
||||
}
|
||||
} catch (e: RemoteException) {
|
||||
throw ContactsStorageException("Could't read last hash code", e)
|
||||
}
|
||||
constructor(addressBook: AndroidAddressBook<LocalContact, *>, contact: Contact, uuid: String?, eTag: String?)
|
||||
: super(addressBook, contact, uuid, eTag) {}
|
||||
|
||||
}
|
||||
|
||||
|
||||
constructor(addressBook: AndroidAddressBook, id: Long, uuid: String?, eTag: String?) : super(addressBook, id, uuid, eTag) {}
|
||||
|
||||
constructor(addressBook: AndroidAddressBook, contact: Contact, uuid: String?, eTag: String?) : super(addressBook, contact, uuid, eTag) {}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun resetDirty() {
|
||||
val values = ContentValues(1)
|
||||
values.put(ContactsContract.RawContacts.DIRTY, 0)
|
||||
try {
|
||||
addressBook.provider.update(rawContactSyncURI(), values, null, null)
|
||||
} catch (e: RemoteException) {
|
||||
throw ContactsStorageException("Couldn't clear dirty flag", e)
|
||||
}
|
||||
|
||||
addressBook.provider?.update(rawContactSyncURI(), values, null, null)
|
||||
}
|
||||
|
||||
override fun resetDeleted() {
|
||||
val values = ContentValues(1)
|
||||
values.put(ContactsContract.Groups.DELETED, 0)
|
||||
addressBook.provider?.update(rawContactSyncURI(), values, null, null)
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
override fun clearDirty(eTag: String) {
|
||||
try {
|
||||
val values = ContentValues(3)
|
||||
values.put(AndroidContact.COLUMN_ETAG, eTag)
|
||||
values.put(ContactsContract.RawContacts.DIRTY, 0)
|
||||
val values = ContentValues(3)
|
||||
values.put(AndroidContact.COLUMN_ETAG, eTag)
|
||||
values.put(ContactsContract.RawContacts.DIRTY, 0)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
|
||||
val hashCode = dataHashCode()
|
||||
values.put(COLUMN_HASHCODE, hashCode)
|
||||
App.log.finer("Clearing dirty flag with eTag = $eTag, contact hash = $hashCode")
|
||||
}
|
||||
|
||||
addressBook.provider.update(rawContactSyncURI(), values, null, null)
|
||||
|
||||
this.eTag = eTag
|
||||
} catch (e: FileNotFoundException) {
|
||||
throw ContactsStorageException("Couldn't clear dirty flag", e)
|
||||
} catch (e: RemoteException) {
|
||||
throw ContactsStorageException("Couldn't clear dirty flag", e)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
|
||||
val hashCode = dataHashCode()
|
||||
values.put(COLUMN_HASHCODE, hashCode)
|
||||
App.log.finer("Clearing dirty flag with eTag = $eTag, contact hash = $hashCode")
|
||||
}
|
||||
|
||||
addressBook.provider?.update(rawContactSyncURI(), values, null, null)
|
||||
|
||||
this.eTag = eTag
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
override fun prepareForUpload() {
|
||||
try {
|
||||
val uid = UUID.randomUUID().toString()
|
||||
val uid = UUID.randomUUID().toString()
|
||||
|
||||
val values = ContentValues(2)
|
||||
values.put(AndroidContact.COLUMN_FILENAME, uid)
|
||||
values.put(AndroidContact.COLUMN_UID, uid)
|
||||
addressBook.provider.update(rawContactSyncURI(), values, null, null)
|
||||
|
||||
fileName = uid
|
||||
} catch (e: RemoteException) {
|
||||
throw ContactsStorageException("Couldn't update UID", e)
|
||||
}
|
||||
val values = ContentValues(2)
|
||||
values.put(AndroidContact.COLUMN_FILENAME, uid)
|
||||
values.put(AndroidContact.COLUMN_UID, uid)
|
||||
addressBook.provider?.update(rawContactSyncURI(), values, null, null)
|
||||
|
||||
fileName = uid
|
||||
}
|
||||
|
||||
override fun populateData(mimeType: String, row: ContentValues) {
|
||||
when (mimeType) {
|
||||
CachedGroupMembership.CONTENT_ITEM_TYPE -> cachedGroupMemberships.add(row.getAsLong(CachedGroupMembership.GROUP_ID))
|
||||
GroupMembership.CONTENT_ITEM_TYPE -> groupMemberships.add(row.getAsLong(GroupMembership.GROUP_ROW_ID))
|
||||
UnknownProperties.CONTENT_ITEM_TYPE -> contact.unknownProperties = row.getAsString(UnknownProperties.UNKNOWN_PROPERTIES)
|
||||
UnknownProperties.CONTENT_ITEM_TYPE -> contact?.unknownProperties = row.getAsString(UnknownProperties.UNKNOWN_PROPERTIES)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
override fun insertDataRows(batch: BatchOperation) {
|
||||
super.insertDataRows(batch)
|
||||
|
||||
if (contact.unknownProperties != null) {
|
||||
if (contact?.unknownProperties != null) {
|
||||
val op: BatchOperation.Operation
|
||||
val builder = ContentProviderOperation.newInsert(dataSyncURI())
|
||||
if (id == null) {
|
||||
@ -170,22 +143,20 @@ class LocalContact : AndroidContact, LocalResource {
|
||||
builder.withValue(UnknownProperties.RAW_CONTACT_ID, id)
|
||||
}
|
||||
builder.withValue(UnknownProperties.MIMETYPE, UnknownProperties.CONTENT_ITEM_TYPE)
|
||||
.withValue(UnknownProperties.UNKNOWN_PROPERTIES, contact.unknownProperties)
|
||||
.withValue(UnknownProperties.UNKNOWN_PROPERTIES, contact?.unknownProperties)
|
||||
batch.enqueue(op)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun updateAsDirty(contact: Contact): Int {
|
||||
fun updateAsDirty(contact: Contact): Uri {
|
||||
saveAsDirty = true
|
||||
return this.update(contact)
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun createAsDirty(): Uri {
|
||||
saveAsDirty = true
|
||||
return this.create()
|
||||
return this.add()
|
||||
}
|
||||
|
||||
override fun buildContact(builder: ContentProviderOperation.Builder, update: Boolean) {
|
||||
@ -195,55 +166,56 @@ class LocalContact : AndroidContact, LocalResource {
|
||||
|
||||
/**
|
||||
* Calculates a hash code from the contact's data (VCard) and group memberships.
|
||||
* Attention: re-reads [.contact] from the database, discarding all changes in memory
|
||||
* Attention: re-reads {@link #contact} from the database, discarding all changes in memory
|
||||
* @return hash code of contact data (including group memberships)
|
||||
*/
|
||||
@Throws(FileNotFoundException::class, ContactsStorageException::class)
|
||||
fun dataHashCode(): Int {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
|
||||
App.log.severe("dataHashCode() should not be called on Android <7")
|
||||
internal fun dataHashCode(): Int {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
throw IllegalStateException("dataHashCode() should not be called on Android != 7")
|
||||
|
||||
// reset contact so that getContact() reads from database
|
||||
contact = null
|
||||
|
||||
// groupMemberships is filled by getContact()
|
||||
val dataHash = getContact().hashCode()
|
||||
val dataHash = contact!!.hashCode()
|
||||
val groupHash = groupMemberships.hashCode()
|
||||
App.log.finest("Calculated data hash = $dataHash, group memberships hash = $groupHash")
|
||||
return dataHash xor groupHash
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun updateHashCode(batch: BatchOperation?) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
|
||||
App.log.severe("updateHashCode() should not be called on Android <7")
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
throw IllegalStateException("updateHashCode() should not be called on Android != 7")
|
||||
|
||||
val values = ContentValues(1)
|
||||
try {
|
||||
val hashCode = dataHashCode()
|
||||
App.log.fine("Storing contact hash = $hashCode")
|
||||
values.put(COLUMN_HASHCODE, hashCode)
|
||||
val hashCode = dataHashCode()
|
||||
App.log.fine("Storing contact hash = $hashCode")
|
||||
values.put(COLUMN_HASHCODE, hashCode)
|
||||
|
||||
if (batch == null)
|
||||
addressBook.provider.update(rawContactSyncURI(), values, null, null)
|
||||
else {
|
||||
val builder = ContentProviderOperation
|
||||
.newUpdate(rawContactSyncURI())
|
||||
.withValues(values)
|
||||
batch.enqueue(BatchOperation.Operation(builder))
|
||||
}
|
||||
} catch (e: FileNotFoundException) {
|
||||
throw ContactsStorageException("Couldn't store contact checksum", e)
|
||||
} catch (e: RemoteException) {
|
||||
throw ContactsStorageException("Couldn't store contact checksum", e)
|
||||
if (batch == null)
|
||||
addressBook.provider!!.update(rawContactSyncURI(), values, null, null)
|
||||
else {
|
||||
val builder = ContentProviderOperation
|
||||
.newUpdate(rawContactSyncURI())
|
||||
.withValues(values)
|
||||
batch.enqueue(BatchOperation.Operation(builder))
|
||||
}
|
||||
}
|
||||
|
||||
fun getLastHashCode(): Int {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
throw IllegalStateException("getLastHashCode() should not be called on Android != 7")
|
||||
|
||||
addressBook.provider!!.query(rawContactSyncURI(), arrayOf(COLUMN_HASHCODE), null, null, null)?.use { c ->
|
||||
if (c.moveToNext() && !c.isNull(0))
|
||||
return c.getInt(0)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
fun addToGroup(batch: BatchOperation, groupID: Long) {
|
||||
assertID()
|
||||
|
||||
fun addToGroup(batch: BatchOperation, groupID: Long) {
|
||||
batch.enqueue(BatchOperation.Operation(
|
||||
ContentProviderOperation.newInsert(dataSyncURI())
|
||||
.withValue(GroupMembership.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE)
|
||||
@ -263,7 +235,6 @@ class LocalContact : AndroidContact, LocalResource {
|
||||
}
|
||||
|
||||
fun removeGroupMemberships(batch: BatchOperation) {
|
||||
assertID()
|
||||
batch.enqueue(BatchOperation.Operation(
|
||||
ContentProviderOperation.newDelete(dataSyncURI())
|
||||
.withSelection(
|
||||
@ -284,9 +255,8 @@ class LocalContact : AndroidContact, LocalResource {
|
||||
* @throws ContactsStorageException on contact provider errors
|
||||
* @throws FileNotFoundException if the current contact can't be found
|
||||
*/
|
||||
@Throws(ContactsStorageException::class, FileNotFoundException::class)
|
||||
fun getCachedGroupMemberships(): Set<Long> {
|
||||
getContact()
|
||||
contact
|
||||
return cachedGroupMemberships
|
||||
}
|
||||
|
||||
@ -296,37 +266,16 @@ class LocalContact : AndroidContact, LocalResource {
|
||||
* @throws ContactsStorageException on contact provider errors
|
||||
* @throws FileNotFoundException if the current contact can't be found
|
||||
*/
|
||||
@Throws(ContactsStorageException::class, FileNotFoundException::class)
|
||||
fun getGroupMemberships(): Set<Long> {
|
||||
getContact()
|
||||
contact
|
||||
return groupMemberships
|
||||
}
|
||||
|
||||
|
||||
// factory
|
||||
|
||||
internal class Factory : AndroidContactFactory() {
|
||||
|
||||
override fun newInstance(addressBook: AndroidAddressBook, id: Long, fileName: String, eTag: String): LocalContact {
|
||||
return LocalContact(addressBook, id, fileName, eTag)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<LocalContact?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val INSTANCE = Factory()
|
||||
}
|
||||
|
||||
object Factory: AndroidContactFactory<LocalContact> {
|
||||
override fun fromProvider(addressBook: AndroidAddressBook<LocalContact, *>, values: ContentValues) =
|
||||
LocalContact(addressBook, values)
|
||||
}
|
||||
|
||||
companion object {
|
||||
init {
|
||||
Contact.productID = Constants.PRODID_BASE + " ez-vcard/" + Ezvcard.VERSION
|
||||
}
|
||||
|
||||
val COLUMN_HASHCODE = ContactsContract.RawContacts.SYNC3
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -8,36 +8,34 @@
|
||||
|
||||
package com.etesync.syncadapter.resource
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.ContentProviderOperation
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.RemoteException
|
||||
import android.provider.CalendarContract
|
||||
import android.provider.CalendarContract.Events
|
||||
import android.text.TextUtils
|
||||
|
||||
import at.bitfire.ical4android.*
|
||||
import at.bitfire.ical4android.Constants.ical4jVersion
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
import com.etesync.syncadapter.App
|
||||
import com.etesync.syncadapter.Constants
|
||||
|
||||
import net.fortuna.ical4j.model.property.ProdId
|
||||
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.UUID
|
||||
import java.util.*
|
||||
import java.util.logging.Level
|
||||
|
||||
import at.bitfire.ical4android.AndroidCalendar
|
||||
import at.bitfire.ical4android.AndroidEvent
|
||||
import at.bitfire.ical4android.AndroidEventFactory
|
||||
import at.bitfire.ical4android.CalendarStorageException
|
||||
import at.bitfire.ical4android.Event
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
class LocalEvent : AndroidEvent, LocalResource<Event> {
|
||||
companion object {
|
||||
init {
|
||||
ICalendar.prodId = ProdId(Constants.PRODID_BASE + " ical4j/" + ical4jVersion)
|
||||
}
|
||||
|
||||
@TargetApi(17)
|
||||
class LocalEvent : AndroidEvent, LocalResource {
|
||||
internal const val COLUMN_ETAG = CalendarContract.Events.SYNC_DATA1
|
||||
internal const val COLUMN_UID = Events.UID_2445
|
||||
internal const val COLUMN_SEQUENCE = CalendarContract.Events.SYNC_DATA3
|
||||
}
|
||||
|
||||
private var saveAsDirty = false // When true, the resource will be saved as dirty
|
||||
|
||||
@ -47,12 +45,11 @@ class LocalEvent : AndroidEvent, LocalResource {
|
||||
var weAreOrganizer = true
|
||||
|
||||
override val content: String
|
||||
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class)
|
||||
get() {
|
||||
App.log.log(Level.FINE, "Preparing upload of event " + fileName!!, getEvent())
|
||||
App.log.log(Level.FINE, "Preparing upload of event " + fileName!!, event)
|
||||
|
||||
val os = ByteArrayOutputStream()
|
||||
getEvent().write(os)
|
||||
event?.write(os)
|
||||
|
||||
return os.toString()
|
||||
}
|
||||
@ -64,34 +61,27 @@ class LocalEvent : AndroidEvent, LocalResource {
|
||||
val uuid: String?
|
||||
get() = fileName
|
||||
|
||||
constructor(calendar: AndroidCalendar, event: Event, fileName: String?, eTag: String?) : super(calendar, event) {
|
||||
constructor(calendar: AndroidCalendar<*>, event: Event, fileName: String?, eTag: String?) : super(calendar, event) {
|
||||
this.fileName = fileName
|
||||
this.eTag = eTag
|
||||
}
|
||||
|
||||
protected constructor(calendar: AndroidCalendar, id: Long, baseInfo: ContentValues?) : super(calendar, id, baseInfo) {
|
||||
if (baseInfo != null) {
|
||||
fileName = baseInfo.getAsString(Events._SYNC_ID)
|
||||
eTag = baseInfo.getAsString(COLUMN_ETAG)
|
||||
}
|
||||
protected constructor(calendar: AndroidCalendar<*>, baseInfo: ContentValues) : super(calendar, baseInfo) {
|
||||
fileName = baseInfo.getAsString(Events._SYNC_ID)
|
||||
eTag = baseInfo.getAsString(COLUMN_ETAG)
|
||||
}
|
||||
|
||||
/* process LocalEvent-specific fields */
|
||||
|
||||
override fun populateEvent(values: ContentValues) {
|
||||
super.populateEvent(values)
|
||||
fileName = values.getAsString(Events._SYNC_ID)
|
||||
eTag = values.getAsString(COLUMN_ETAG)
|
||||
event.uid = values.getAsString(COLUMN_UID)
|
||||
override fun populateEvent(row: ContentValues) {
|
||||
super.populateEvent(row)
|
||||
fileName = row.getAsString(Events._SYNC_ID)
|
||||
eTag = row.getAsString(COLUMN_ETAG)
|
||||
event?.uid = row.getAsString(COLUMN_UID)
|
||||
|
||||
event.sequence = values.getAsInteger(COLUMN_SEQUENCE)
|
||||
if (Build.VERSION.SDK_INT >= 17) {
|
||||
val isOrganizer = values.getAsInteger(Events.IS_ORGANIZER)
|
||||
weAreOrganizer = isOrganizer != null && isOrganizer != 0
|
||||
} else {
|
||||
val organizer = values.getAsString(Events.ORGANIZER)
|
||||
weAreOrganizer = organizer == null || organizer == calendar.account.name
|
||||
}
|
||||
event?.sequence = row.getAsInteger(COLUMN_SEQUENCE)
|
||||
val isOrganizer = row.getAsInteger(Events.IS_ORGANIZER)
|
||||
weAreOrganizer = isOrganizer != null && isOrganizer != 0
|
||||
}
|
||||
|
||||
override fun buildEvent(recurrence: Event?, builder: ContentProviderOperation.Builder) {
|
||||
@ -100,7 +90,7 @@ class LocalEvent : AndroidEvent, LocalResource {
|
||||
val buildException = recurrence != null
|
||||
val eventToBuild = if (buildException) recurrence else event
|
||||
|
||||
builder.withValue(COLUMN_UID, event.uid)
|
||||
builder.withValue(COLUMN_UID, event?.uid)
|
||||
.withValue(COLUMN_SEQUENCE, eventToBuild?.sequence)
|
||||
.withValue(CalendarContract.Events.DIRTY, if (saveAsDirty) 1 else 0)
|
||||
.withValue(CalendarContract.Events.DELETED, 0)
|
||||
@ -126,77 +116,42 @@ class LocalEvent : AndroidEvent, LocalResource {
|
||||
|
||||
/* custom queries */
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun prepareForUpload() {
|
||||
try {
|
||||
var uid: String? = null
|
||||
val c = calendar.provider.query(eventSyncURI(), arrayOf(COLUMN_UID), null, null, null)
|
||||
if (c.moveToNext())
|
||||
uid = c.getString(0)
|
||||
if (uid == null)
|
||||
uid = UUID.randomUUID().toString()
|
||||
var uid: String? = null
|
||||
val c = calendar.provider.query(eventSyncURI(), arrayOf(COLUMN_UID), null, null, null)
|
||||
if (c.moveToNext())
|
||||
uid = c.getString(0)
|
||||
if (uid == null)
|
||||
uid = UUID.randomUUID().toString()
|
||||
|
||||
c.close()
|
||||
val newFileName = uid
|
||||
c.close()
|
||||
val newFileName = uid
|
||||
|
||||
val values = ContentValues(2)
|
||||
values.put(Events._SYNC_ID, newFileName)
|
||||
values.put(COLUMN_UID, uid)
|
||||
calendar.provider.update(eventSyncURI(), values, null, null)
|
||||
val values = ContentValues(2)
|
||||
values.put(Events._SYNC_ID, newFileName)
|
||||
values.put(COLUMN_UID, uid)
|
||||
calendar.provider.update(eventSyncURI(), values, null, null)
|
||||
|
||||
fileName = newFileName
|
||||
if (event != null)
|
||||
event.uid = uid
|
||||
|
||||
} catch (e: RemoteException) {
|
||||
throw CalendarStorageException("Couldn't update UID", e)
|
||||
}
|
||||
fileName = newFileName
|
||||
|
||||
val event = this.event
|
||||
if (event != null)
|
||||
event.uid = uid
|
||||
}
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun clearDirty(eTag: String) {
|
||||
try {
|
||||
val values = ContentValues(2)
|
||||
values.put(CalendarContract.Events.DIRTY, 0)
|
||||
values.put(COLUMN_ETAG, eTag)
|
||||
if (event != null)
|
||||
values.put(COLUMN_SEQUENCE, event.sequence)
|
||||
calendar.provider.update(eventSyncURI(), values, null, null)
|
||||
|
||||
this.eTag = eTag
|
||||
} catch (e: RemoteException) {
|
||||
throw CalendarStorageException("Couldn't update UID", e)
|
||||
}
|
||||
val values = ContentValues(2)
|
||||
values.put(CalendarContract.Events.DIRTY, 0)
|
||||
values.put(COLUMN_ETAG, eTag)
|
||||
if (event != null)
|
||||
values.put(COLUMN_SEQUENCE, event?.sequence)
|
||||
calendar.provider.update(eventSyncURI(), values, null, null)
|
||||
|
||||
this.eTag = eTag
|
||||
}
|
||||
|
||||
internal class Factory : AndroidEventFactory {
|
||||
|
||||
override fun newInstance(calendar: AndroidCalendar, id: Long, baseInfo: ContentValues): AndroidEvent {
|
||||
return LocalEvent(calendar, id, baseInfo)
|
||||
}
|
||||
|
||||
override fun newInstance(calendar: AndroidCalendar, event: Event): AndroidEvent {
|
||||
return LocalEvent(calendar, event, null, null)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<AndroidEvent?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val INSTANCE = Factory()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
init {
|
||||
Event.prodId = ProdId(Constants.PRODID_BASE + " ical4j/2.x")
|
||||
}
|
||||
|
||||
internal val COLUMN_ETAG = CalendarContract.Events.SYNC_DATA1
|
||||
internal val COLUMN_UID = if (Build.VERSION.SDK_INT >= 17) Events.UID_2445 else Events.SYNC_DATA2
|
||||
internal val COLUMN_SEQUENCE = CalendarContract.Events.SYNC_DATA3
|
||||
object Factory: AndroidEventFactory<LocalEvent> {
|
||||
override fun fromProvider(calendar: AndroidCalendar<AndroidEvent>, values: ContentValues): LocalEvent =
|
||||
LocalEvent(calendar, values)
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import android.content.ContentProviderOperation
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Parcel
|
||||
import android.os.RemoteException
|
||||
@ -21,6 +22,7 @@ import android.provider.ContactsContract.Groups
|
||||
import android.provider.ContactsContract.RawContacts
|
||||
import android.provider.ContactsContract.RawContacts.Data
|
||||
import android.text.TextUtils
|
||||
import at.bitfire.vcard4android.*
|
||||
|
||||
import com.etesync.syncadapter.App
|
||||
|
||||
@ -34,27 +36,88 @@ import java.util.LinkedList
|
||||
import java.util.UUID
|
||||
import java.util.logging.Level
|
||||
|
||||
import at.bitfire.vcard4android.AndroidAddressBook
|
||||
import at.bitfire.vcard4android.AndroidGroup
|
||||
import at.bitfire.vcard4android.AndroidGroupFactory
|
||||
import at.bitfire.vcard4android.BatchOperation
|
||||
import at.bitfire.vcard4android.CachedGroupMembership
|
||||
import at.bitfire.vcard4android.Contact
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
import ezvcard.VCardVersion
|
||||
|
||||
import at.bitfire.vcard4android.GroupMethod.GROUP_VCARDS
|
||||
|
||||
class LocalGroup : AndroidGroup, LocalResource {
|
||||
class LocalGroup : AndroidGroup, LocalAddress {
|
||||
companion object {
|
||||
/** marshalled list of member UIDs, as sent by server */
|
||||
val COLUMN_PENDING_MEMBERS = Groups.SYNC3
|
||||
|
||||
override val uuid: String
|
||||
get() = getFileName()
|
||||
/**
|
||||
* Processes all groups with non-null {@link #COLUMN_PENDING_MEMBERS}: the pending memberships
|
||||
* are (if possible) applied, keeping cached memberships in sync.
|
||||
* @param addressBook address book to take groups from
|
||||
*/
|
||||
fun applyPendingMemberships(addressBook: LocalAddressBook) {
|
||||
addressBook.provider!!.query(
|
||||
addressBook.groupsSyncUri(),
|
||||
arrayOf(Groups._ID, COLUMN_PENDING_MEMBERS),
|
||||
"$COLUMN_PENDING_MEMBERS IS NOT NULL", null,
|
||||
null
|
||||
)?.use { cursor ->
|
||||
val batch = BatchOperation(addressBook.provider)
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(0)
|
||||
Constants.log.fine("Assigning members to group $id")
|
||||
|
||||
// required for workaround for Android 7 which sets DIRTY flag when only meta-data is changed
|
||||
val changeContactIDs = HashSet<Long>()
|
||||
|
||||
// delete all memberships and cached memberships for this group
|
||||
for (contact in addressBook.getByGroupMembership(id)) {
|
||||
contact.removeGroupMemberships(batch)
|
||||
changeContactIDs += contact.id!!
|
||||
}
|
||||
|
||||
// extract list of member UIDs
|
||||
val members = LinkedList<String>()
|
||||
val raw = cursor.getBlob(1)
|
||||
val parcel = Parcel.obtain()
|
||||
try {
|
||||
parcel.unmarshall(raw, 0, raw.size)
|
||||
parcel.setDataPosition(0)
|
||||
parcel.readStringList(members)
|
||||
} finally {
|
||||
parcel.recycle()
|
||||
}
|
||||
|
||||
// insert memberships
|
||||
for (uid in members) {
|
||||
Constants.log.fine("Assigning member: $uid")
|
||||
addressBook.findContactByUID(uid)?.let { member ->
|
||||
member.addToGroup(batch, id)
|
||||
changeContactIDs += member.id!!
|
||||
} ?: Constants.log.warning("Group member not found: $uid")
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
|
||||
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
|
||||
changeContactIDs
|
||||
.map { addressBook.findContactByID(it) }
|
||||
.forEach { it.updateHashCode(batch) }
|
||||
|
||||
// remove pending memberships
|
||||
batch.enqueue(BatchOperation.Operation(
|
||||
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, id)))
|
||||
.withValue(COLUMN_PENDING_MEMBERS, null)
|
||||
.withYieldAllowed(true)
|
||||
))
|
||||
|
||||
batch.commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override val uuid: String?
|
||||
get() = fileName
|
||||
|
||||
override val content: String
|
||||
@Throws(IOException::class, ContactsStorageException::class)
|
||||
get() {
|
||||
val contact: Contact
|
||||
contact = getContact()
|
||||
contact = this.contact!!
|
||||
|
||||
App.log.log(Level.FINE, "Preparing upload of VCard $uuid", contact)
|
||||
|
||||
@ -65,42 +128,29 @@ class LocalGroup : AndroidGroup, LocalResource {
|
||||
}
|
||||
|
||||
override val isLocalOnly: Boolean
|
||||
get() = TextUtils.isEmpty(getETag())
|
||||
get() = TextUtils.isEmpty(eTag)
|
||||
|
||||
/**
|
||||
* Lists all members of this group.
|
||||
* @return list of all members' raw contact IDs
|
||||
* @throws ContactsStorageException on contact provider errors
|
||||
*/
|
||||
val members: LongArray
|
||||
@Throws(ContactsStorageException::class)
|
||||
get() {
|
||||
assertID()
|
||||
val members = LinkedList<Long>()
|
||||
try {
|
||||
val cursor = addressBook.provider.query(
|
||||
addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI),
|
||||
arrayOf(Data.RAW_CONTACT_ID),
|
||||
GroupMembership.MIMETYPE + "=? AND " + GroupMembership.GROUP_ROW_ID + "=?",
|
||||
arrayOf(GroupMembership.CONTENT_ITEM_TYPE, id.toString()), null
|
||||
)
|
||||
while (cursor != null && cursor.moveToNext())
|
||||
members.add(cursor.getLong(0))
|
||||
cursor!!.close()
|
||||
} catch (e: RemoteException) {
|
||||
throw ContactsStorageException("Couldn't list group members", e)
|
||||
}
|
||||
constructor(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, values: ContentValues)
|
||||
: super(addressBook, values) {}
|
||||
|
||||
return ArrayUtils.toPrimitive(members.toTypedArray())
|
||||
constructor(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, contact: Contact, fileName: String?, eTag: String?)
|
||||
: super(addressBook, contact, fileName, eTag) {}
|
||||
|
||||
override fun contentValues(): ContentValues {
|
||||
val values = super.contentValues()
|
||||
|
||||
val members = Parcel.obtain()
|
||||
try {
|
||||
members.writeStringList(contact?.members)
|
||||
values.put(COLUMN_PENDING_MEMBERS, members.marshall())
|
||||
} finally {
|
||||
members.recycle()
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
constructor(addressBook: AndroidAddressBook, id: Long, fileName: String?, eTag: String?) : super(addressBook, id, fileName, eTag) {}
|
||||
|
||||
constructor(addressBook: AndroidAddressBook, contact: Contact, fileName: String?, eTag: String?) : super(addressBook, contact, fileName, eTag) {}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
override fun clearDirty(eTag: String) {
|
||||
assertID()
|
||||
val id = requireNotNull(id)
|
||||
|
||||
val values = ContentValues(2)
|
||||
values.put(Groups.DIRTY, 0)
|
||||
@ -109,7 +159,7 @@ class LocalGroup : AndroidGroup, LocalResource {
|
||||
update(values)
|
||||
|
||||
// update cached group memberships
|
||||
val batch = BatchOperation(addressBook.provider)
|
||||
val batch = BatchOperation(addressBook.provider!!)
|
||||
|
||||
// delete cached group memberships
|
||||
batch.enqueue(BatchOperation.Operation(
|
||||
@ -121,7 +171,7 @@ class LocalGroup : AndroidGroup, LocalResource {
|
||||
))
|
||||
|
||||
// insert updated cached group memberships
|
||||
for (member in members)
|
||||
for (member in getMembers())
|
||||
batch.enqueue(BatchOperation.Operation(
|
||||
ContentProviderOperation.newInsert(addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI))
|
||||
.withValue(CachedGroupMembership.MIMETYPE, CachedGroupMembership.CONTENT_ITEM_TYPE)
|
||||
@ -133,7 +183,6 @@ class LocalGroup : AndroidGroup, LocalResource {
|
||||
batch.commit()
|
||||
}
|
||||
|
||||
@Throws(ContactsStorageException::class)
|
||||
override fun prepareForUpload() {
|
||||
val uid = UUID.randomUUID().toString()
|
||||
|
||||
@ -145,147 +194,46 @@ class LocalGroup : AndroidGroup, LocalResource {
|
||||
fileName = uid
|
||||
}
|
||||
|
||||
override fun contentValues(): ContentValues {
|
||||
val values = super.contentValues()
|
||||
|
||||
val members = Parcel.obtain()
|
||||
members.writeStringList(contact.members)
|
||||
values.put(COLUMN_PENDING_MEMBERS, members.marshall())
|
||||
|
||||
members.recycle()
|
||||
return values
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Marks all members of the current group as dirty.
|
||||
*/
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun markMembersDirty() {
|
||||
assertID()
|
||||
val batch = BatchOperation(addressBook.provider)
|
||||
|
||||
for (member in members)
|
||||
batch.enqueue(BatchOperation.Operation(
|
||||
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(RawContacts.CONTENT_URI, member)))
|
||||
.withValue(RawContacts.DIRTY, 1)
|
||||
.withYieldAllowed(true)
|
||||
))
|
||||
|
||||
batch.commit()
|
||||
override fun resetDeleted() {
|
||||
val values = ContentValues(1)
|
||||
values.put(Groups.DELETED, 0)
|
||||
addressBook.provider!!.update(groupSyncUri(), values, null, null)
|
||||
}
|
||||
|
||||
|
||||
// helpers
|
||||
|
||||
private fun assertID() {
|
||||
if (id == null)
|
||||
throw IllegalStateException("Group has not been saved yet")
|
||||
private fun groupSyncUri(): Uri {
|
||||
val id = requireNotNull(id)
|
||||
return ContentUris.withAppendedId(addressBook.groupsSyncUri(), id)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "LocalGroup(super=" + super.toString() + ", uuid=" + this.uuid + ")"
|
||||
/**
|
||||
* Lists all members of this group.
|
||||
* @return list of all members' raw contact IDs
|
||||
* @throws RemoteException on contact provider errors
|
||||
*/
|
||||
internal fun getMembers(): List<Long> {
|
||||
val id = requireNotNull(id)
|
||||
val members = LinkedList<Long>()
|
||||
addressBook.provider!!.query(
|
||||
addressBook.syncAdapterURI(ContactsContract.Data.CONTENT_URI),
|
||||
arrayOf(Data.RAW_CONTACT_ID),
|
||||
"${GroupMembership.MIMETYPE}=? AND ${GroupMembership.GROUP_ROW_ID}=?",
|
||||
arrayOf(GroupMembership.CONTENT_ITEM_TYPE, id.toString()),
|
||||
null
|
||||
)?.use { cursor ->
|
||||
while (cursor.moveToNext())
|
||||
members += cursor.getLong(0)
|
||||
}
|
||||
return members
|
||||
}
|
||||
|
||||
|
||||
// factory
|
||||
|
||||
internal class Factory : AndroidGroupFactory() {
|
||||
|
||||
override fun newInstance(addressBook: AndroidAddressBook, id: Long, fileName: String, eTag: String): LocalGroup {
|
||||
return LocalGroup(addressBook, id, fileName, eTag)
|
||||
}
|
||||
|
||||
override fun newInstance(addressBook: AndroidAddressBook, contact: Contact, fileName: String, eTag: String): LocalGroup {
|
||||
return LocalGroup(addressBook, contact, fileName, eTag)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<LocalGroup> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val INSTANCE = Factory()
|
||||
}
|
||||
|
||||
object Factory: AndroidGroupFactory<LocalGroup> {
|
||||
override fun fromProvider(addressBook: AndroidAddressBook<out AndroidContact, LocalGroup>, values: ContentValues) =
|
||||
LocalGroup(addressBook, values)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** marshalled list of member UIDs, as sent by server */
|
||||
val COLUMN_PENDING_MEMBERS = Groups.SYNC3
|
||||
|
||||
/**
|
||||
* Processes all groups with non-null [.COLUMN_PENDING_MEMBERS]: the pending memberships
|
||||
* are (if possible) applied, keeping cached memberships in sync.
|
||||
* @param addressBook address book to take groups from
|
||||
* @throws ContactsStorageException on contact provider errors
|
||||
*/
|
||||
@Throws(ContactsStorageException::class)
|
||||
fun applyPendingMemberships(addressBook: LocalAddressBook) {
|
||||
try {
|
||||
val cursor = addressBook.provider.query(
|
||||
addressBook.syncAdapterURI(Groups.CONTENT_URI),
|
||||
arrayOf(Groups._ID, COLUMN_PENDING_MEMBERS),
|
||||
"$COLUMN_PENDING_MEMBERS IS NOT NULL", arrayOf(), null
|
||||
)
|
||||
|
||||
val batch = BatchOperation(addressBook.provider)
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
val id = cursor.getLong(0)
|
||||
App.log.fine("Assigning members to group $id")
|
||||
|
||||
// required for workaround for Android 7 which sets DIRTY flag when only meta-data is changed
|
||||
val changeContactIDs = HashSet<Long>()
|
||||
|
||||
// delete all memberships and cached memberships for this group
|
||||
for (contact in addressBook.getByGroupMembership(id)) {
|
||||
contact.removeGroupMemberships(batch)
|
||||
changeContactIDs.add(contact.id)
|
||||
}
|
||||
|
||||
// extract list of member UIDs
|
||||
val members = LinkedList<String>()
|
||||
val raw = cursor.getBlob(1)
|
||||
val parcel = Parcel.obtain()
|
||||
parcel.unmarshall(raw, 0, raw.size)
|
||||
parcel.setDataPosition(0)
|
||||
parcel.readStringList(members)
|
||||
parcel.recycle()
|
||||
|
||||
// insert memberships
|
||||
for (uid in members) {
|
||||
App.log.fine("Assigning member: $uid")
|
||||
try {
|
||||
val member = addressBook.findContactByUID(uid)
|
||||
member.addToGroup(batch, id)
|
||||
changeContactIDs.add(member.id)
|
||||
} catch (e: FileNotFoundException) {
|
||||
App.log.log(Level.WARNING, "Group member not found: $uid", e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
|
||||
for (contactID in changeContactIDs) {
|
||||
val contact = LocalContact(addressBook, contactID, null, null)
|
||||
contact.updateHashCode(batch)
|
||||
}
|
||||
|
||||
// remove pending memberships
|
||||
batch.enqueue(BatchOperation.Operation(
|
||||
ContentProviderOperation.newUpdate(addressBook.syncAdapterURI(ContentUris.withAppendedId(Groups.CONTENT_URI, id)))
|
||||
.withValue(COLUMN_PENDING_MEMBERS, null)
|
||||
.withYieldAllowed(true)
|
||||
))
|
||||
|
||||
batch.commit()
|
||||
}
|
||||
cursor!!.close()
|
||||
} catch (e: RemoteException) {
|
||||
throw ContactsStorageException("Couldn't get pending memberships", e)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -8,12 +8,10 @@
|
||||
|
||||
package com.etesync.syncadapter.resource
|
||||
|
||||
import java.io.IOException
|
||||
|
||||
import at.bitfire.ical4android.CalendarStorageException
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
|
||||
interface LocalResource {
|
||||
interface LocalResource<in TData: Any> {
|
||||
val uuid: String?
|
||||
|
||||
/** True if doesn't exist on server yet, false otherwise. */
|
||||
@ -22,13 +20,9 @@ interface LocalResource {
|
||||
/** Returns a string of how this should be represented for example: vCard. */
|
||||
val content: String
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class)
|
||||
fun delete(): Int
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class)
|
||||
fun prepareForUpload()
|
||||
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class)
|
||||
fun clearDirty(eTag: String)
|
||||
|
||||
}
|
||||
|
@ -10,143 +10,98 @@ package com.etesync.syncadapter.resource
|
||||
|
||||
import android.content.ContentProviderOperation
|
||||
import android.content.ContentValues
|
||||
import android.os.RemoteException
|
||||
import android.provider.CalendarContract.Events
|
||||
|
||||
import com.etesync.syncadapter.Constants
|
||||
|
||||
import net.fortuna.ical4j.model.property.ProdId
|
||||
|
||||
import org.dmfs.provider.tasks.TaskContract.Tasks
|
||||
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.text.ParseException
|
||||
import java.util.UUID
|
||||
|
||||
import android.text.TextUtils
|
||||
import at.bitfire.ical4android.AndroidTask
|
||||
import at.bitfire.ical4android.AndroidTaskFactory
|
||||
import at.bitfire.ical4android.AndroidTaskList
|
||||
import at.bitfire.ical4android.CalendarStorageException
|
||||
import at.bitfire.ical4android.Task
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
import com.etesync.syncadapter.App
|
||||
import org.dmfs.tasks.contract.TaskContract
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.*
|
||||
import java.util.logging.Level
|
||||
|
||||
class LocalTask : AndroidTask, LocalResource<Task> {
|
||||
companion object {
|
||||
internal const val COLUMN_ETAG = TaskContract.Tasks.SYNC1
|
||||
internal const val COLUMN_UID = TaskContract.Tasks.SYNC2
|
||||
internal const val COLUMN_SEQUENCE = TaskContract.Tasks.SYNC3
|
||||
}
|
||||
|
||||
class LocalTask : AndroidTask, LocalResource {
|
||||
private var fileName: String? = null
|
||||
var eTag: String? = null
|
||||
|
||||
override val content: String
|
||||
@Throws(IOException::class, ContactsStorageException::class)
|
||||
get() = ""
|
||||
get() {
|
||||
App.log.log(Level.FINE, "Preparing upload of task " + fileName!!, task)
|
||||
|
||||
val os = ByteArrayOutputStream()
|
||||
task?.write(os)
|
||||
|
||||
return os.toString()
|
||||
}
|
||||
|
||||
override val isLocalOnly: Boolean
|
||||
get() = false
|
||||
get() = TextUtils.isEmpty(eTag)
|
||||
|
||||
override// Now the same
|
||||
val uuid: String?
|
||||
get() = fileName
|
||||
|
||||
constructor(taskList: AndroidTaskList, task: Task, fileName: String?, eTag: String?) : super(taskList, task) {
|
||||
constructor(taskList: AndroidTaskList<*>, task: Task, fileName: String?, eTag: String?, flags: Int)
|
||||
: super(taskList, task) {
|
||||
this.fileName = fileName
|
||||
this.eTag = eTag
|
||||
}
|
||||
|
||||
protected constructor(taskList: AndroidTaskList, id: Long, baseInfo: ContentValues?) : super(taskList, id) {
|
||||
if (baseInfo != null) {
|
||||
fileName = baseInfo.getAsString(Events._SYNC_ID)
|
||||
eTag = baseInfo.getAsString(COLUMN_ETAG)
|
||||
}
|
||||
private constructor(taskList: AndroidTaskList<*>, values: ContentValues): super(taskList) {
|
||||
id = values.getAsLong(TaskContract.Tasks._ID)
|
||||
fileName = values.getAsString(TaskContract.Tasks._SYNC_ID)
|
||||
eTag = values.getAsString(COLUMN_ETAG)
|
||||
}
|
||||
|
||||
|
||||
/* process LocalTask-specific fields */
|
||||
|
||||
@Throws(FileNotFoundException::class, RemoteException::class, ParseException::class)
|
||||
override fun populateTask(values: ContentValues) {
|
||||
super.populateTask(values)
|
||||
|
||||
fileName = values.getAsString(Events._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(Tasks._SYNC_ID, fileName)
|
||||
.withValue(COLUMN_UID, task.uid)
|
||||
.withValue(COLUMN_SEQUENCE, task.sequence)
|
||||
builder.withValue(TaskContract.Tasks._SYNC_ID, fileName)
|
||||
.withValue(COLUMN_UID, task?.uid)
|
||||
.withValue(COLUMN_SEQUENCE, task?.sequence)
|
||||
.withValue(COLUMN_ETAG, eTag)
|
||||
}
|
||||
|
||||
|
||||
/* custom queries */
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun prepareForUpload() {
|
||||
try {
|
||||
val uid = UUID.randomUUID().toString()
|
||||
val newFileName = "$uid.ics"
|
||||
val uid = UUID.randomUUID().toString()
|
||||
|
||||
val values = ContentValues(2)
|
||||
values.put(Tasks._SYNC_ID, newFileName)
|
||||
values.put(COLUMN_UID, uid)
|
||||
taskList.provider.client.update(taskSyncURI(), values, null, null)
|
||||
|
||||
fileName = newFileName
|
||||
if (task != null)
|
||||
task.uid = uid
|
||||
|
||||
} catch (e: RemoteException) {
|
||||
throw CalendarStorageException("Couldn't update UID", e)
|
||||
}
|
||||
val values = ContentValues(2)
|
||||
values.put(TaskContract.Tasks._SYNC_ID, uid)
|
||||
values.put(COLUMN_UID, uid)
|
||||
taskList.provider.client.update(taskSyncURI(), values, null, null)
|
||||
|
||||
fileName = uid
|
||||
val task = this.task
|
||||
if (task != null)
|
||||
task.uid = uid
|
||||
}
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun clearDirty(eTag: String) {
|
||||
try {
|
||||
val values = ContentValues(2)
|
||||
values.put(Tasks._DIRTY, 0)
|
||||
values.put(COLUMN_ETAG, eTag)
|
||||
if (task != null)
|
||||
values.put(COLUMN_SEQUENCE, task.sequence)
|
||||
taskList.provider.client.update(taskSyncURI(), values, null, null)
|
||||
|
||||
this.eTag = eTag
|
||||
} catch (e: RemoteException) {
|
||||
throw CalendarStorageException("Couldn't update _DIRTY/ETag/SEQUENCE", e)
|
||||
}
|
||||
val values = ContentValues(2)
|
||||
values.put(TaskContract.Tasks._DIRTY, 0)
|
||||
values.put(COLUMN_ETAG, eTag)
|
||||
if (task != null)
|
||||
values.put(COLUMN_SEQUENCE, task?.sequence)
|
||||
taskList.provider.client.update(taskSyncURI(), values, null, null)
|
||||
|
||||
this.eTag = eTag
|
||||
}
|
||||
|
||||
|
||||
internal class Factory : AndroidTaskFactory {
|
||||
|
||||
override fun newInstance(taskList: AndroidTaskList, id: Long, baseInfo: ContentValues): LocalTask {
|
||||
return LocalTask(taskList, id, baseInfo)
|
||||
}
|
||||
|
||||
override fun newInstance(taskList: AndroidTaskList, task: Task): LocalTask {
|
||||
return LocalTask(taskList, task, null, null)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<LocalTask?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val INSTANCE = Factory()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
init {
|
||||
Task.prodId = ProdId(Constants.PRODID_BASE + " ical4j/2.x")
|
||||
}
|
||||
|
||||
internal val COLUMN_ETAG = Tasks.SYNC1
|
||||
internal val COLUMN_UID = Tasks.SYNC2
|
||||
internal val COLUMN_SEQUENCE = Tasks.SYNC3
|
||||
object Factory: AndroidTaskFactory<LocalTask> {
|
||||
override fun fromProvider(taskList: AndroidTaskList<*>, values: ContentValues) =
|
||||
LocalTask(taskList, values)
|
||||
}
|
||||
}
|
||||
|
@ -9,82 +9,95 @@
|
||||
package com.etesync.syncadapter.resource
|
||||
|
||||
import android.accounts.Account
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.RemoteException
|
||||
|
||||
import com.etesync.syncadapter.model.CollectionInfo
|
||||
|
||||
import org.dmfs.provider.tasks.TaskContract.TaskLists
|
||||
import org.dmfs.provider.tasks.TaskContract.Tasks
|
||||
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
import at.bitfire.ical4android.AndroidTaskList
|
||||
import at.bitfire.ical4android.AndroidTaskListFactory
|
||||
import at.bitfire.ical4android.CalendarStorageException
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import com.etesync.syncadapter.model.JournalEntity
|
||||
import org.dmfs.tasks.contract.TaskContract.TaskLists
|
||||
import org.dmfs.tasks.contract.TaskContract.Tasks
|
||||
|
||||
class LocalTaskList protected constructor(account: Account, provider: TaskProvider, id: Long) : AndroidTaskList(account, provider, LocalTask.Factory.INSTANCE, id), LocalCollection<LocalTask> {
|
||||
class LocalTaskList private constructor(
|
||||
account: Account,
|
||||
provider: TaskProvider,
|
||||
id: Long
|
||||
): AndroidTaskList<LocalTask>(account, provider, LocalTask.Factory, id), LocalCollection<LocalTask> {
|
||||
companion object {
|
||||
val defaultColor = -0x3c1592 // "DAVdroid green"
|
||||
|
||||
override val deleted: Array<LocalTask>
|
||||
@Throws(CalendarStorageException::class)
|
||||
get() = queryTasks(Tasks._DELETED + "!=0", null) as Array<LocalTask>
|
||||
|
||||
override val withoutFileName: Array<LocalTask>
|
||||
@Throws(CalendarStorageException::class)
|
||||
get() = queryTasks(Tasks._SYNC_ID + " IS NULL", null) as Array<LocalTask>
|
||||
|
||||
override// sequence has not been assigned yet (i.e. this task was just locally created)
|
||||
val dirty: Array<LocalTask>
|
||||
@Throws(CalendarStorageException::class, FileNotFoundException::class)
|
||||
get() {
|
||||
val tasks = queryTasks(Tasks._DIRTY + "!=0 AND " + Tasks._DELETED + "== 0", null) as Array<LocalTask>
|
||||
for (task in tasks) {
|
||||
if (task.task.sequence == null)
|
||||
task.task.sequence = 0
|
||||
else
|
||||
task.task.sequence++
|
||||
fun tasksProviderAvailable(context: Context): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
||||
return context.packageManager.resolveContentProvider(TaskProvider.ProviderName.OpenTasks.authority, 0) != null
|
||||
else {
|
||||
val provider = TaskProvider.acquire(context, TaskProvider.ProviderName.OpenTasks)
|
||||
provider?.use { return true }
|
||||
return false
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
|
||||
fun create(account: Account, provider: TaskProvider, journalEntity: JournalEntity): Uri {
|
||||
val values = valuesFromCollectionInfo(journalEntity, true)
|
||||
values.put(TaskLists.OWNER, account.name)
|
||||
values.put(TaskLists.SYNC_ENABLED, 1)
|
||||
values.put(TaskLists.VISIBLE, 1)
|
||||
return create(account, provider, values)
|
||||
}
|
||||
|
||||
private fun valuesFromCollectionInfo(journalEntity: JournalEntity, withColor: Boolean): ContentValues {
|
||||
val info = journalEntity.info
|
||||
val values = ContentValues(3)
|
||||
values.put(TaskLists._SYNC_ID, info.uid)
|
||||
values.put(TaskLists.LIST_NAME, if (info.displayName.isNullOrBlank()) info.uid else info.displayName)
|
||||
|
||||
if (withColor)
|
||||
values.put(TaskLists.LIST_COLOR, info.color ?: defaultColor)
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
override fun taskBaseInfoColumns(): Array<String> {
|
||||
return BASE_INFO_COLUMNS
|
||||
}
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
fun update(info: CollectionInfo, updateColor: Boolean) {
|
||||
update(valuesFromCollectionInfo(info, updateColor))
|
||||
fun update(journalEntity: JournalEntity, updateColor: Boolean) =
|
||||
update(valuesFromCollectionInfo(journalEntity, updateColor))
|
||||
|
||||
override fun findDeleted() = queryTasks("${Tasks._DELETED}!=0", null)
|
||||
|
||||
override fun findDirty(): List<LocalTask> {
|
||||
val tasks = queryTasks("${Tasks._DIRTY}!=0", null)
|
||||
for (localTask in tasks) {
|
||||
val task = requireNotNull(localTask.task)
|
||||
val sequence = task.sequence
|
||||
if (sequence == null) // sequence has not been assigned yet (i.e. this task was just locally created)
|
||||
task.sequence = 0
|
||||
else
|
||||
task.sequence = sequence + 1
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun getByUid(uid: String): LocalTask? {
|
||||
val ret = queryTasks(Tasks._SYNC_ID + " =? ", arrayOf(uid)) as Array<LocalTask>
|
||||
return if (ret != null && ret.size > 0) {
|
||||
ret[0]
|
||||
} else null
|
||||
}
|
||||
override fun findAll(): List<LocalTask>
|
||||
= queryTasks(null, null)
|
||||
|
||||
override fun findWithoutFileName(): List<LocalTask>
|
||||
= queryTasks(Tasks._SYNC_ID + " IS NULL", null)
|
||||
|
||||
override fun findByUid(uid: String): LocalTask?
|
||||
= queryTasks(Tasks._SYNC_ID + " =? ", arrayOf(uid)).firstOrNull()
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
override fun count(): Long {
|
||||
val where = Tasks.LIST_ID + "=?"
|
||||
val whereArgs = arrayOf(id.toString())
|
||||
|
||||
try {
|
||||
val cursor = provider.client.query(
|
||||
syncAdapterURI(provider.tasksUri()), null,
|
||||
where, whereArgs, null)
|
||||
TaskProvider.syncAdapterUri(provider.tasksUri(), account), null,
|
||||
Tasks.LIST_ID + "=?", arrayOf(id.toString()), null)
|
||||
try {
|
||||
return cursor.count.toLong()
|
||||
return cursor?.count?.toLong()!!
|
||||
} finally {
|
||||
cursor.close()
|
||||
cursor?.close()
|
||||
}
|
||||
} catch (e: RemoteException) {
|
||||
throw CalendarStorageException("Couldn't query calendar events", e)
|
||||
@ -92,78 +105,10 @@ class LocalTaskList protected constructor(account: Account, provider: TaskProvid
|
||||
|
||||
}
|
||||
|
||||
object Factory: AndroidTaskListFactory<LocalTaskList> {
|
||||
|
||||
class Factory : AndroidTaskListFactory {
|
||||
override fun newInstance(account: Account, provider: TaskProvider, id: Long) =
|
||||
LocalTaskList(account, provider, id)
|
||||
|
||||
override fun newInstance(account: Account, provider: TaskProvider, id: Long): AndroidTaskList {
|
||||
return LocalTaskList(account, provider, id)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<AndroidTaskList?> {
|
||||
return arrayOfNulls<LocalTaskList>(size) as Array<AndroidTaskList?>
|
||||
}
|
||||
|
||||
companion object {
|
||||
val INSTANCE = Factory()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val defaultColor = -0x3c1592 // "DAVdroid green"
|
||||
|
||||
val COLUMN_CTAG = TaskLists.SYNC_VERSION
|
||||
|
||||
internal var BASE_INFO_COLUMNS = arrayOf(Tasks._ID, Tasks._SYNC_ID, LocalTask.COLUMN_ETAG)
|
||||
|
||||
@Throws(CalendarStorageException::class)
|
||||
fun create(account: Account, provider: TaskProvider, info: CollectionInfo): Uri {
|
||||
val values = valuesFromCollectionInfo(info, true)
|
||||
values.put(TaskLists.OWNER, account.name)
|
||||
values.put(TaskLists.SYNC_ENABLED, 1)
|
||||
values.put(TaskLists.VISIBLE, 1)
|
||||
return AndroidTaskList.create(account, provider, values)
|
||||
}
|
||||
|
||||
private fun valuesFromCollectionInfo(info: CollectionInfo, withColor: Boolean): ContentValues {
|
||||
val values = ContentValues()
|
||||
values.put(TaskLists._SYNC_ID, info.uid)
|
||||
values.put(TaskLists.LIST_NAME, info.displayName)
|
||||
|
||||
if (withColor)
|
||||
values.put(TaskLists.LIST_COLOR, if (info.color != null) info.color else defaultColor)
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
// helpers
|
||||
|
||||
fun tasksProviderAvailable(context: Context): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
||||
return context.packageManager.resolveContentProvider(TaskProvider.ProviderName.OpenTasks.authority, 0) != null
|
||||
else {
|
||||
val provider = TaskProvider.acquire(context.contentResolver, TaskProvider.ProviderName.OpenTasks)
|
||||
try {
|
||||
return provider != null
|
||||
} finally {
|
||||
provider?.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// HELPERS
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
fun onRenameAccount(resolver: ContentResolver, oldName: String, newName: String) {
|
||||
val client = resolver.acquireContentProviderClient(TaskProvider.ProviderName.OpenTasks.authority)
|
||||
if (client != null) {
|
||||
val values = ContentValues(1)
|
||||
values.put(Tasks.ACCOUNT_NAME, newName)
|
||||
client.update(Tasks.getContentUri(TaskProvider.ProviderName.OpenTasks.authority), values, Tasks.ACCOUNT_NAME + "=?", arrayOf(oldName))
|
||||
client.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -108,10 +108,6 @@ class AddressBooksSyncAdapterService : SyncAdapterService() {
|
||||
|
||||
notificationManager.notify(title, context.getString(syncPhase))
|
||||
} catch (e: OutOfMemoryError) {
|
||||
if (e is ContactsStorageException || e is SQLiteException) {
|
||||
App.log.log(Level.SEVERE, "Couldn't prepare local address books", e)
|
||||
syncResult.databaseError = true
|
||||
}
|
||||
val syncPhase = R.string.sync_phase_journals
|
||||
val title = context.getString(R.string.sync_error_contacts, account.name)
|
||||
notificationManager.setThrowable(e)
|
||||
|
@ -38,12 +38,9 @@ import org.apache.commons.codec.Charsets
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.ArrayList
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
@ -51,25 +48,25 @@ import at.bitfire.ical4android.CalendarStorageException
|
||||
import at.bitfire.ical4android.Event
|
||||
import at.bitfire.ical4android.InvalidCalendarException
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
import com.etesync.syncadapter.resource.LocalCollection
|
||||
import okhttp3.HttpUrl
|
||||
import java.io.StringReader
|
||||
|
||||
/**
|
||||
*
|
||||
* Synchronization manager for CardDAV collections; handles contacts and groups.
|
||||
*/
|
||||
class CalendarSyncManager @Throws(Exceptions.IntegrityException::class, Exceptions.GenericCryptoException::class)
|
||||
constructor(context: Context, account: Account, settings: AccountSettings, extras: Bundle, authority: String, result: SyncResult, calendar: LocalCalendar, private val remote: HttpUrl) : SyncManager(context, account, settings, extras, authority, result, calendar.name, CollectionInfo.Type.CALENDAR, account.name) {
|
||||
constructor(context: Context, account: Account, settings: AccountSettings, extras: Bundle, authority: String, result: SyncResult, calendar: LocalCalendar, private val remote: HttpUrl) : SyncManager<LocalEvent>(context, account, settings, extras, authority, result, calendar.name!!, CollectionInfo.Type.CALENDAR, account.name) {
|
||||
|
||||
protected override val syncErrorTitle: String
|
||||
override val syncErrorTitle: String
|
||||
get() = context.getString(R.string.sync_error_calendar, account.name)
|
||||
|
||||
protected override val syncSuccessfullyTitle: String
|
||||
override val syncSuccessfullyTitle: String
|
||||
get() = context.getString(R.string.sync_successfully_calendar, info.displayName,
|
||||
account.name)
|
||||
|
||||
init {
|
||||
localCollection = calendar as LocalCollection<LocalResource>
|
||||
localCollection = calendar
|
||||
}
|
||||
|
||||
override fun notificationId(): Int {
|
||||
@ -81,7 +78,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
if (!super.prepare())
|
||||
return false
|
||||
|
||||
journal = JournalEntryManager(httpClient, remote, localCalendar().name)
|
||||
journal = JournalEntryManager(httpClient, remote, localCalendar().name!!)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -101,9 +98,9 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
|
||||
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class, InvalidCalendarException::class)
|
||||
override fun processSyncEntry(cEntry: SyncEntry) {
|
||||
val `is` = ByteArrayInputStream(cEntry.content.toByteArray(Charsets.UTF_8))
|
||||
val inputReader = StringReader(cEntry.content)
|
||||
|
||||
val events = Event.fromStream(`is`, Charsets.UTF_8)
|
||||
val events = Event.fromReader(inputReader)
|
||||
if (events.size == 0) {
|
||||
App.log.warning("Received VCard without data, ignoring")
|
||||
return
|
||||
@ -112,7 +109,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
}
|
||||
|
||||
val event = events[0]
|
||||
val local = localCollection!!.getByUid(event.uid) as LocalEvent?
|
||||
val local = localCollection!!.findByUid(event.uid!!)
|
||||
|
||||
if (cEntry.isAction(SyncEntry.Actions.ADD) || cEntry.isAction(SyncEntry.Actions.CHANGE)) {
|
||||
processEvent(event, local)
|
||||
@ -140,7 +137,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
for (local in localDirty) {
|
||||
val event = (local as LocalEvent).event
|
||||
|
||||
if (event.attendees.isEmpty()) {
|
||||
if (event?.attendees?.isEmpty()!!) {
|
||||
return
|
||||
}
|
||||
createInviteAttendeesNotification(event, local.content)
|
||||
@ -148,7 +145,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
}
|
||||
|
||||
private fun createInviteAttendeesNotification(event: Event, icsContent: String) {
|
||||
val notificationHelper = NotificationHelper(context, event.uid, event.uid.hashCode())
|
||||
val notificationHelper = NotificationHelper(context, event.uid!!, event.uid!!.hashCode())
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.type = "text/plain"
|
||||
intent.putExtra(Intent.EXTRA_EMAIL, getEmailAddresses(event.attendees, false))
|
||||
@ -156,14 +153,14 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT,
|
||||
context.getString(R.string.sync_calendar_attendees_email_subject,
|
||||
event.summary,
|
||||
dateFormatDate.format(event.dtStart.date)))
|
||||
dateFormatDate.format(event.dtStart?.date)))
|
||||
intent.putExtra(Intent.EXTRA_TEXT,
|
||||
context.getString(R.string.sync_calendar_attendees_email_content,
|
||||
event.summary,
|
||||
formatEventDates(event),
|
||||
if (event.location != null) event.location else "",
|
||||
formatAttendees(event.attendees)))
|
||||
val uri = createAttachmentFromString(context, event.uid, icsContent)
|
||||
val uri = createAttachmentFromString(context, event.uid!!, icsContent)
|
||||
if (uri == null) {
|
||||
App.log.severe("Unable to create attachment from calendar event")
|
||||
return
|
||||
@ -179,7 +176,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
}
|
||||
|
||||
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class)
|
||||
private fun processEvent(newData: Event, localEvent: LocalEvent?): LocalResource {
|
||||
private fun processEvent(newData: Event, localEvent: LocalEvent?): LocalEvent {
|
||||
var localEvent = localEvent
|
||||
// delete local event, if it exists
|
||||
if (localEvent != null) {
|
||||
@ -221,23 +218,23 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
|
||||
private fun formatEventDates(event: Event): String {
|
||||
val locale = Locale.getDefault()
|
||||
val timezone = if (event.dtStart.timeZone != null) event.dtStart.timeZone else TimeZone.getTimeZone("UTC")
|
||||
val dateFormatString = if (event.isAllDay) "EEEE, MMM dd" else "EEEE, MMM dd @ hh:mm a"
|
||||
val timezone = if (event.dtStart?.timeZone != null) event.dtStart?.timeZone else TimeZone.getTimeZone("UTC")
|
||||
val dateFormatString = if (event.isAllDay()) "EEEE, MMM dd" else "EEEE, MMM dd @ hh:mm a"
|
||||
val longDateFormat = SimpleDateFormat(dateFormatString, locale)
|
||||
longDateFormat.timeZone = timezone
|
||||
val shortDateFormat = SimpleDateFormat("hh:mm a", locale)
|
||||
shortDateFormat.timeZone = timezone
|
||||
|
||||
val startDate = event.dtStart.date
|
||||
val startDate = event.dtStart?.date
|
||||
val endDate = event.getEndDate(true)!!.date
|
||||
val tzName = timezone.getDisplayName(timezone.inDaylightTime(startDate), TimeZone.SHORT)
|
||||
val tzName = timezone?.getDisplayName(timezone?.inDaylightTime(startDate)!!, TimeZone.SHORT)
|
||||
|
||||
val cal1 = Calendar.getInstance()
|
||||
val cal2 = Calendar.getInstance()
|
||||
cal1.time = startDate
|
||||
cal2.time = endDate
|
||||
val sameDay = cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) && cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR)
|
||||
if (sameDay && event.isAllDay) {
|
||||
if (sameDay && event.isAllDay()) {
|
||||
return longDateFormat.format(startDate)
|
||||
}
|
||||
return if (sameDay)
|
||||
|
@ -17,6 +17,7 @@ import android.content.SyncResult
|
||||
import android.database.sqlite.SQLiteException
|
||||
import android.os.Bundle
|
||||
import android.provider.CalendarContract
|
||||
import at.bitfire.ical4android.AndroidCalendar
|
||||
|
||||
import com.etesync.syncadapter.AccountSettings
|
||||
import com.etesync.syncadapter.App
|
||||
@ -67,7 +68,7 @@ class CalendarsSyncAdapterService : SyncAdapterService() {
|
||||
|
||||
val principal = HttpUrl.get(settings.uri!!)!!
|
||||
|
||||
for (calendar in LocalCalendar.find(account, provider, LocalCalendar.Factory.INSTANCE, CalendarContract.Calendars.SYNC_EVENTS + "!=0", null) as Array<LocalCalendar>) {
|
||||
for (calendar in AndroidCalendar.find(account, provider, LocalCalendar.Factory, CalendarContract.Calendars.SYNC_EVENTS + "!=0", null)) {
|
||||
App.log.info("Synchronizing calendar #" + calendar.id + ", URL: " + calendar.name)
|
||||
val syncManager = CalendarSyncManager(context, account, settings, extras, authority, syncResult, calendar, principal)
|
||||
syncManager.performSync()
|
||||
@ -95,10 +96,6 @@ class CalendarsSyncAdapterService : SyncAdapterService() {
|
||||
|
||||
notificationManager.notify(title, context.getString(syncPhase))
|
||||
} catch (e: OutOfMemoryError) {
|
||||
if (e is CalendarStorageException || e is SQLiteException) {
|
||||
App.log.log(Level.SEVERE, "Couldn't prepare local calendars", e)
|
||||
syncResult.databaseError = true
|
||||
}
|
||||
val syncPhase = R.string.sync_phase_journals
|
||||
val title = context.getString(R.string.sync_error_calendar, account.name)
|
||||
notificationManager.setThrowable(e)
|
||||
@ -121,7 +118,7 @@ class CalendarsSyncAdapterService : SyncAdapterService() {
|
||||
remote[journalEntity.uid] = journalEntity
|
||||
}
|
||||
|
||||
val local = LocalCalendar.find(account, provider, LocalCalendar.Factory.INSTANCE, null, null) as Array<LocalCalendar>
|
||||
val local = AndroidCalendar.find(account, provider, LocalCalendar.Factory, null, null)
|
||||
|
||||
val updateColors = settings.manageCalendarColors
|
||||
|
||||
|
@ -29,37 +29,30 @@ import com.etesync.syncadapter.journalmanager.Exceptions
|
||||
import com.etesync.syncadapter.journalmanager.JournalEntryManager
|
||||
import com.etesync.syncadapter.model.CollectionInfo
|
||||
import com.etesync.syncadapter.model.SyncEntry
|
||||
import com.etesync.syncadapter.resource.LocalAddressBook
|
||||
import com.etesync.syncadapter.resource.LocalContact
|
||||
import com.etesync.syncadapter.resource.LocalGroup
|
||||
import com.etesync.syncadapter.resource.LocalResource
|
||||
|
||||
import org.apache.commons.codec.Charsets
|
||||
import org.apache.commons.collections4.SetUtils
|
||||
import org.apache.commons.io.IOUtils
|
||||
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.logging.Level
|
||||
|
||||
import at.bitfire.ical4android.CalendarStorageException
|
||||
import at.bitfire.vcard4android.BatchOperation
|
||||
import at.bitfire.vcard4android.Contact
|
||||
import at.bitfire.vcard4android.ContactsStorageException
|
||||
import com.etesync.syncadapter.resource.*
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody
|
||||
import java.io.StringReader
|
||||
|
||||
/**
|
||||
*
|
||||
* Synchronization manager for CardDAV collections; handles contacts and groups.
|
||||
*/
|
||||
class ContactsSyncManager @Throws(Exceptions.IntegrityException::class, Exceptions.GenericCryptoException::class, ContactsStorageException::class)
|
||||
constructor(context: Context, account: Account, settings: AccountSettings, extras: Bundle, authority: String, private val provider: ContentProviderClient, result: SyncResult, localAddressBook: LocalAddressBook, private val remote: HttpUrl) : SyncManager(context, account, settings, extras, authority, result, localAddressBook.url!!, CollectionInfo.Type.ADDRESS_BOOK, localAddressBook.mainAccount.name) {
|
||||
constructor(context: Context, account: Account, settings: AccountSettings, extras: Bundle, authority: String, private val provider: ContentProviderClient, result: SyncResult, localAddressBook: LocalAddressBook, private val remote: HttpUrl) : SyncManager<LocalAddress>(context, account, settings, extras, authority, result, localAddressBook.url!!, CollectionInfo.Type.ADDRESS_BOOK, localAddressBook.mainAccount.name) {
|
||||
|
||||
protected override val syncErrorTitle: String
|
||||
get() = context.getString(R.string.sync_error_contacts, account.name)
|
||||
@ -85,7 +78,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
|
||||
val reallyDirty = localAddressBook.verifyDirty()
|
||||
val deleted = localAddressBook.deleted.size
|
||||
val deleted = localAddressBook.findDeleted().size
|
||||
if (extras.containsKey(ContentResolver.SYNC_EXTRAS_UPLOAD) && reallyDirty == 0 && deleted == 0) {
|
||||
App.log.info("This sync was called to up-sync dirty/deleted contacts, but no contacts have been changed")
|
||||
return false
|
||||
@ -96,7 +89,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
val values = ContentValues(2)
|
||||
values.put(ContactsContract.Settings.SHOULD_SYNC, 1)
|
||||
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1)
|
||||
localAddressBook.updateSettings(values)
|
||||
localAddressBook.settings.putAll(values)
|
||||
|
||||
journal = JournalEntryManager(httpClient, remote, localAddressBook.url!!)
|
||||
|
||||
@ -114,12 +107,12 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
/* groups as separate VCards: there are group contacts and individual contacts */
|
||||
|
||||
// mark groups with changed members as dirty
|
||||
val batch = BatchOperation(addressBook.provider)
|
||||
for (contact in addressBook.dirtyContacts) {
|
||||
val batch = BatchOperation(addressBook.provider!!)
|
||||
for (contact in addressBook.findDirtyContacts()) {
|
||||
try {
|
||||
App.log.fine("Looking for changed group memberships of contact " + contact.fileName)
|
||||
val cachedGroups = contact.cachedGroupMemberships
|
||||
val currentGroups = contact.groupMemberships
|
||||
val cachedGroups = contact.getCachedGroupMemberships()
|
||||
val currentGroups = contact.getGroupMemberships()
|
||||
for (groupID in SetUtils.disjunction(cachedGroups, currentGroups)) {
|
||||
App.log.fine("Marking group as dirty: " + groupID!!)
|
||||
batch.enqueue(BatchOperation.Operation(
|
||||
@ -152,10 +145,10 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
|
||||
@Throws(IOException::class, ContactsStorageException::class, CalendarStorageException::class)
|
||||
override fun processSyncEntry(cEntry: SyncEntry) {
|
||||
val `is` = ByteArrayInputStream(cEntry.content.toByteArray(Charsets.UTF_8))
|
||||
val inputReader = StringReader(cEntry.content)
|
||||
val downloader = ResourceDownloader(context)
|
||||
|
||||
val contacts = Contact.fromStream(`is`, Charsets.UTF_8, downloader)
|
||||
val contacts = Contact.fromReader(inputReader, downloader)
|
||||
if (contacts.size == 0) {
|
||||
App.log.warning("Received VCard without data, ignoring")
|
||||
return
|
||||
@ -163,14 +156,13 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
App.log.warning("Received multiple VCards, using first one")
|
||||
|
||||
val contact = contacts[0]
|
||||
val local = localCollection!!.getByUid(contact.uid) as LocalResource?
|
||||
|
||||
val local = localCollection!!.findByUid(contact.uid!!)
|
||||
|
||||
if (cEntry.isAction(SyncEntry.Actions.ADD) || cEntry.isAction(SyncEntry.Actions.CHANGE)) {
|
||||
processContact(contact, local)
|
||||
} else {
|
||||
if (local != null) {
|
||||
App.log.info("Removing local record #" + local.id + " which has been deleted on the server")
|
||||
App.log.info("Removing local record which has been deleted on the server")
|
||||
local.delete()
|
||||
} else {
|
||||
App.log.warning("Tried deleting a non-existent record: " + contact.uid)
|
||||
@ -179,7 +171,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
}
|
||||
|
||||
@Throws(IOException::class, ContactsStorageException::class)
|
||||
private fun processContact(newData: Contact, local: LocalResource?): LocalResource {
|
||||
private fun processContact(newData: Contact, local: LocalAddress?): LocalAddress {
|
||||
var local = local
|
||||
val uuid = newData.uid
|
||||
// update local contact, if it exists
|
||||
@ -188,14 +180,14 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
|
||||
if (local is LocalGroup && newData.group) {
|
||||
// update group
|
||||
val group = local as LocalGroup?
|
||||
val group: LocalGroup = local
|
||||
group!!.eTag = uuid
|
||||
group.updateFromServer(newData)
|
||||
group.update(newData)
|
||||
syncResult.stats.numUpdates++
|
||||
|
||||
} else if (local is LocalContact && !newData.group) {
|
||||
// update contact
|
||||
val contact = local as LocalContact?
|
||||
val contact: LocalContact = local
|
||||
contact!!.eTag = uuid
|
||||
contact.update(newData)
|
||||
syncResult.stats.numUpdates++
|
||||
@ -216,13 +208,13 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
if (newData.group) {
|
||||
App.log.log(Level.INFO, "Creating local group", newData.uid)
|
||||
val group = LocalGroup(localAddressBook(), newData, uuid, uuid)
|
||||
group.create()
|
||||
group.add()
|
||||
|
||||
local = group
|
||||
} else {
|
||||
App.log.log(Level.INFO, "Creating local contact", newData.uid)
|
||||
val contact = LocalContact(localAddressBook(), newData, uuid, uuid)
|
||||
contact.create()
|
||||
contact.add()
|
||||
|
||||
local = contact
|
||||
}
|
||||
@ -272,15 +264,7 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
|
||||
|
||||
val body = response.body()
|
||||
if (body != null) {
|
||||
val stream = body.byteStream()
|
||||
try {
|
||||
if (response.isSuccessful && stream != null) {
|
||||
return IOUtils.toByteArray(stream)
|
||||
} else
|
||||
App.log.severe("Couldn't download external resource")
|
||||
} finally {
|
||||
stream?.close()
|
||||
}
|
||||
return body.bytes()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
App.log.log(Level.SEVERE, "Couldn't download external resource", e)
|
||||
|
@ -55,12 +55,12 @@ import okhttp3.OkHttpClient
|
||||
import com.etesync.syncadapter.Constants.KEY_ACCOUNT
|
||||
import com.etesync.syncadapter.model.SyncEntry.Actions.ADD
|
||||
|
||||
abstract class SyncManager @Throws(Exceptions.IntegrityException::class, Exceptions.GenericCryptoException::class)
|
||||
abstract class SyncManager<T: LocalResource<*>> @Throws(Exceptions.IntegrityException::class, Exceptions.GenericCryptoException::class)
|
||||
constructor(protected val context: Context, protected val account: Account, protected val settings: AccountSettings, protected val extras: Bundle, protected val authority: String, protected val syncResult: SyncResult, journalUid: String, protected val serviceType: CollectionInfo.Type, accountName: String) {
|
||||
|
||||
protected val notificationManager: NotificationHelper
|
||||
protected val info: CollectionInfo
|
||||
protected var localCollection: LocalCollection<LocalResource>? = null
|
||||
protected var localCollection: LocalCollection<T>? = null
|
||||
|
||||
protected var httpClient: OkHttpClient
|
||||
|
||||
@ -89,8 +89,8 @@ constructor(protected val context: Context, protected val account: Account, prot
|
||||
/**
|
||||
* Dirty and deleted resources. We need to save them so we safely ignore ones that were added after we started.
|
||||
*/
|
||||
private var localDeleted: List<LocalResource>? = null
|
||||
protected var localDirty: Array<LocalResource> = arrayOf()
|
||||
private var localDeleted: List<T>? = null
|
||||
protected var localDirty: List<T> = LinkedList()
|
||||
|
||||
protected abstract val syncErrorTitle: String
|
||||
|
||||
@ -227,8 +227,6 @@ constructor(protected val context: Context, protected val account: Account, prot
|
||||
} catch (e: OutOfMemoryError) {
|
||||
if (e is Exceptions.HttpException) {
|
||||
syncResult.stats.numParseExceptions++
|
||||
} else if (e is CalendarStorageException || e is ContactsStorageException) {
|
||||
syncResult.databaseError = true
|
||||
} else {
|
||||
syncResult.stats.numParseExceptions++
|
||||
}
|
||||
@ -400,7 +398,7 @@ constructor(protected val context: Context, protected val account: Account, prot
|
||||
local.delete()
|
||||
}
|
||||
if (left > 0) {
|
||||
localDeleted?.drop(left)
|
||||
localDeleted = localDeleted?.drop(left)
|
||||
}
|
||||
|
||||
left = pushed
|
||||
@ -412,7 +410,7 @@ constructor(protected val context: Context, protected val account: Account, prot
|
||||
local.clearDirty(local.uuid!!)
|
||||
}
|
||||
if (left > 0) {
|
||||
localDirty = Arrays.copyOfRange(localDirty, left, localDirty.size)
|
||||
localDirty = localDirty.drop(left)
|
||||
}
|
||||
|
||||
if (pushed > 0) {
|
||||
@ -450,7 +448,7 @@ constructor(protected val context: Context, protected val account: Account, prot
|
||||
|
||||
val entry = SyncEntry(local.content, action)
|
||||
val tmp = JournalEntryManager.Entry()
|
||||
tmp.update(crypto, entry.toJson(), previousEntry!!)
|
||||
tmp.update(crypto, entry.toJson(), previousEntry)
|
||||
previousEntry = tmp
|
||||
localEntries!!.add(previousEntry)
|
||||
|
||||
@ -467,7 +465,7 @@ constructor(protected val context: Context, protected val account: Account, prot
|
||||
remoteCTag = journalEntity.getLastUid(data)
|
||||
|
||||
localDeleted = processLocallyDeleted()
|
||||
localDirty = localCollection!!.dirty
|
||||
localDirty = localCollection!!.findDirty()
|
||||
// This is done after fetching the local dirty so all the ones we are using will be prepared
|
||||
prepareDirty()
|
||||
}
|
||||
@ -478,9 +476,9 @@ constructor(protected val context: Context, protected val account: Account, prot
|
||||
* Checks Thread.interrupted() before each request to allow quick sync cancellation.
|
||||
*/
|
||||
@Throws(CalendarStorageException::class, ContactsStorageException::class)
|
||||
private fun processLocallyDeleted(): List<LocalResource> {
|
||||
val localList = localCollection!!.deleted
|
||||
val ret = ArrayList<LocalResource>(localList.size)
|
||||
private fun processLocallyDeleted(): List<T> {
|
||||
val localList = localCollection!!.findDeleted()
|
||||
val ret = ArrayList<T>(localList.size)
|
||||
|
||||
for (local in localList) {
|
||||
if (Thread.interrupted())
|
||||
@ -504,7 +502,7 @@ constructor(protected val context: Context, protected val account: Account, prot
|
||||
continue
|
||||
}
|
||||
|
||||
App.log.fine("Found local record #" + local.id + " without file name; generating file name/UID if necessary")
|
||||
App.log.fine("Found local record without file name; generating file name/UID if necessary")
|
||||
local.prepareForUpload()
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,6 @@ import com.etesync.syncadapter.BuildConfig
|
||||
import com.etesync.syncadapter.Constants
|
||||
import com.etesync.syncadapter.R
|
||||
import ezvcard.Ezvcard
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.time.DateFormatUtils
|
||||
import java.io.IOException
|
||||
import java.util.logging.Level
|
||||
@ -143,9 +142,9 @@ class AboutActivity : BaseActivity() {
|
||||
override fun loadInBackground(): Spanned? {
|
||||
App.log.fine("Loading license file $fileName")
|
||||
try {
|
||||
val `is` = context.resources.assets.open(fileName)
|
||||
val raw = IOUtils.toByteArray(`is`)
|
||||
`is`.close()
|
||||
val inputStream = context.resources.assets.open(fileName)
|
||||
val raw = inputStream.readBytes()
|
||||
inputStream.close()
|
||||
content = Html.fromHtml(String(raw))
|
||||
return content
|
||||
} catch (e: IOException) {
|
||||
|
@ -359,11 +359,7 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
|
||||
try {
|
||||
if (future.result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT))
|
||||
finish()
|
||||
} catch (e: OperationCanceledException) {
|
||||
App.log.log(Level.SEVERE, "Couldn't remove account", e)
|
||||
} catch (e: IOException) {
|
||||
App.log.log(Level.SEVERE, "Couldn't remove account", e)
|
||||
} catch (e: AuthenticatorException) {
|
||||
} catch(e: Exception) {
|
||||
App.log.log(Level.SEVERE, "Couldn't remove account", e)
|
||||
}
|
||||
}, null)
|
||||
@ -372,16 +368,13 @@ class AccountActivity : BaseActivity(), Toolbar.OnMenuItemClickListener, PopupMe
|
||||
try {
|
||||
if (future.result)
|
||||
finish()
|
||||
} catch (e: OperationCanceledException) {
|
||||
App.log.log(Level.SEVERE, "Couldn't remove account", e)
|
||||
} catch (e: IOException) {
|
||||
App.log.log(Level.SEVERE, "Couldn't remove account", e)
|
||||
} catch (e: AuthenticatorException) {
|
||||
} catch (e: Exception) {
|
||||
App.log.log(Level.SEVERE, "Couldn't remove account", e)
|
||||
}
|
||||
}, null)
|
||||
}
|
||||
|
||||
|
||||
private fun requestSync() {
|
||||
requestSync(account)
|
||||
Snackbar.make(findViewById(R.id.parent), R.string.account_synchronizing_now, Snackbar.LENGTH_LONG).show()
|
||||
|
@ -149,7 +149,7 @@ class DebugInfoActivity : BaseActivity(), LoaderManager.LoaderCallbacks<String>
|
||||
|
||||
report.append("CONFIGURATION\n")
|
||||
// power saving
|
||||
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager?
|
||||
if (powerManager != null && Build.VERSION.SDK_INT >= 23)
|
||||
report.append("Power saving disabled: ")
|
||||
.append(if (powerManager.isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID)) "yes" else "no")
|
||||
|
@ -27,9 +27,8 @@ import com.etesync.syncadapter.model.JournalEntity
|
||||
import com.etesync.syncadapter.model.SyncEntry
|
||||
import com.etesync.syncadapter.ui.journalviewer.ListEntriesFragment.Companion.setJournalEntryView
|
||||
import ezvcard.util.PartialDate
|
||||
import org.apache.commons.codec.Charsets
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.io.StringReader
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@ -154,10 +153,10 @@ class JournalItemActivity : BaseActivity(), Refreshable {
|
||||
|
||||
private inner class LoadEventTask internal constructor(internal var view: View) : AsyncTask<Void, Void, Event>() {
|
||||
override fun doInBackground(vararg aVoids: Void): Event? {
|
||||
val `is` = ByteArrayInputStream(syncEntry.content.toByteArray(Charsets.UTF_8))
|
||||
val inputReader = StringReader(syncEntry.content)
|
||||
|
||||
try {
|
||||
return Event.fromStream(`is`, Charsets.UTF_8, null)[0]
|
||||
return Event.fromReader(inputReader, null)[0]
|
||||
} catch (e: InvalidCalendarException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: IOException) {
|
||||
@ -175,16 +174,17 @@ class JournalItemActivity : BaseActivity(), Refreshable {
|
||||
|
||||
setTextViewText(view, R.id.title, event.summary)
|
||||
|
||||
setTextViewText(view, R.id.when_datetime, getDisplayedDatetime(event.dtStart.date.time, event.dtEnd.date.time, event.isAllDay, context))
|
||||
setTextViewText(view, R.id.when_datetime, getDisplayedDatetime(event.dtStart?.date?.time!!, event.dtEnd?.date!!.time, event.isAllDay(), context))
|
||||
|
||||
setTextViewText(view, R.id.where, event.location)
|
||||
|
||||
if (event.organizer != null) {
|
||||
val organizer = event.organizer
|
||||
if (organizer != null) {
|
||||
val tv = view.findViewById<View>(R.id.organizer) as TextView
|
||||
tv.text = event.organizer.calAddress.toString().replaceFirst("mailto:".toRegex(), "")
|
||||
tv.text = organizer.calAddress.toString().replaceFirst("mailto:".toRegex(), "")
|
||||
} else {
|
||||
val organizer = view.findViewById<View>(R.id.organizer_container)
|
||||
organizer.visibility = View.GONE
|
||||
val organizerView = view.findViewById<View>(R.id.organizer_container)
|
||||
organizerView.visibility = View.GONE
|
||||
}
|
||||
|
||||
setTextViewText(view, R.id.description, event.description)
|
||||
@ -220,10 +220,10 @@ class JournalItemActivity : BaseActivity(), Refreshable {
|
||||
private inner class LoadContactTask internal constructor(internal var view: View) : AsyncTask<Void, Void, Contact>() {
|
||||
|
||||
override fun doInBackground(vararg aVoids: Void): Contact? {
|
||||
val `is` = ByteArrayInputStream(syncEntry.content.toByteArray(Charsets.UTF_8))
|
||||
val reader = StringReader(syncEntry.content)
|
||||
|
||||
try {
|
||||
return Contact.fromStream(`is`, Charsets.UTF_8, null)[0]
|
||||
return Contact.fromReader(reader, null)[0]
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
@ -279,7 +279,7 @@ class JournalItemActivity : BaseActivity(), Refreshable {
|
||||
|
||||
// ORG, TITLE, ROLE
|
||||
if (contact.organization != null) {
|
||||
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_organization), contact.jobTitle, contact.organization.values[0])
|
||||
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_organization), contact.jobTitle, contact.organization?.values!![0])
|
||||
}
|
||||
if (contact.jobDescription != null) {
|
||||
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_job_description), null, contact.jobTitle)
|
||||
@ -291,8 +291,8 @@ class JournalItemActivity : BaseActivity(), Refreshable {
|
||||
}
|
||||
|
||||
// NICKNAME
|
||||
if (contact.nickName != null && contact.nickName.values.size > 0) {
|
||||
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_nickname), null, contact.nickName.values[0])
|
||||
if (contact.nickName != null && !contact.nickName?.values?.isEmpty()!!) {
|
||||
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_nickname), null, contact.nickName?.values!![0])
|
||||
}
|
||||
|
||||
// ADR
|
||||
@ -314,11 +314,11 @@ class JournalItemActivity : BaseActivity(), Refreshable {
|
||||
|
||||
// ANNIVERSARY
|
||||
if (contact.anniversary != null) {
|
||||
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_anniversary), null, getDisplayedDate(contact.anniversary.date, contact.anniversary.partialDate))
|
||||
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_anniversary), null, getDisplayedDate(contact.anniversary?.date, contact.anniversary?.partialDate))
|
||||
}
|
||||
// BDAY
|
||||
if (contact.birthDay != null) {
|
||||
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_birthday), null, getDisplayedDate(contact.birthDay.date, contact.birthDay.partialDate))
|
||||
addInfoItem(view.context, aboutCard, getString(R.string.journal_item_birthday), null, getDisplayedDate(contact.birthDay?.date, contact.birthDay?.partialDate))
|
||||
}
|
||||
|
||||
// RELATED
|
||||
@ -333,17 +333,19 @@ class JournalItemActivity : BaseActivity(), Refreshable {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDisplayedDate(date: Date?, partialDate: PartialDate): String? {
|
||||
private fun getDisplayedDate(date: Date?, partialDate: PartialDate?): String? {
|
||||
if (date != null) {
|
||||
val epochDate = date.time
|
||||
return getDisplayedDatetime(epochDate, epochDate, true, context)
|
||||
} else {
|
||||
} else if (partialDate != null){
|
||||
val formatter = SimpleDateFormat("d MMMM", Locale.getDefault())
|
||||
val calendar = GregorianCalendar()
|
||||
calendar.set(Calendar.DAY_OF_MONTH, partialDate.date!!)
|
||||
calendar.set(Calendar.MONTH, partialDate.month!! - 1)
|
||||
return formatter.format(calendar.time)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -171,7 +171,7 @@ class ViewCollectionActivity : BaseActivity(), Refreshable {
|
||||
if (info.type == CollectionInfo.Type.CALENDAR) {
|
||||
try {
|
||||
val providerClient = contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)
|
||||
val resource = LocalCalendar.findByName(account, providerClient, LocalCalendar.Factory.INSTANCE, info.uid!!)
|
||||
val resource = LocalCalendar.findByName(account, providerClient, LocalCalendar.Factory, info.uid!!)
|
||||
providerClient!!.release()
|
||||
if (resource == null) {
|
||||
return null
|
||||
|
@ -77,7 +77,7 @@ class CalendarAccount protected constructor(val account: Account) {
|
||||
try {
|
||||
val localCalendar = LocalCalendar.findByName(calendarAccount.account,
|
||||
contentProviderClient,
|
||||
LocalCalendar.Factory.INSTANCE, getString(cur, Calendars.NAME)!!)
|
||||
LocalCalendar.Factory, getString(cur, Calendars.NAME)!!)
|
||||
if (localCalendar != null) calendarAccount.calendars.add(localCalendar)
|
||||
} catch (ex: Exception) {
|
||||
ex.printStackTrace()
|
||||
|
@ -32,10 +32,7 @@ import com.etesync.syncadapter.syncadapter.ContactsSyncManager
|
||||
import com.etesync.syncadapter.ui.Refreshable
|
||||
import com.etesync.syncadapter.ui.importlocal.ResultFragment.ImportResult
|
||||
import org.apache.commons.codec.Charsets
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.*
|
||||
|
||||
class ImportFragment : DialogFragment() {
|
||||
|
||||
@ -204,11 +201,11 @@ class ImportFragment : DialogFragment() {
|
||||
val result = ImportResult()
|
||||
|
||||
try {
|
||||
val importStream = FileInputStream(importFile!!)
|
||||
val importReader = FileReader(importFile!!)
|
||||
|
||||
if (info!!.type == CollectionInfo.Type.CALENDAR) {
|
||||
val events = Event.fromStream(importStream, Charsets.UTF_8)
|
||||
importStream.close()
|
||||
val events = Event.fromReader(importReader, null)
|
||||
importReader.close()
|
||||
|
||||
if (events.size == 0) {
|
||||
App.log.warning("Empty/invalid file.")
|
||||
@ -223,7 +220,7 @@ class ImportFragment : DialogFragment() {
|
||||
val provider = context!!.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)
|
||||
val localCalendar: LocalCalendar?
|
||||
try {
|
||||
localCalendar = LocalCalendar.findByName(account, provider, LocalCalendar.Factory.INSTANCE, info!!.uid!!)
|
||||
localCalendar = LocalCalendar.findByName(account, provider, LocalCalendar.Factory, info!!.uid!!)
|
||||
if (localCalendar == null) {
|
||||
throw FileNotFoundException("Failed to load local resource.")
|
||||
}
|
||||
@ -251,7 +248,7 @@ class ImportFragment : DialogFragment() {
|
||||
} else if (info!!.type == CollectionInfo.Type.ADDRESS_BOOK) {
|
||||
// FIXME: Handle groups and download icon?
|
||||
val downloader = ContactsSyncManager.ResourceDownloader(context!!)
|
||||
val contacts = Contact.fromStream(importStream, Charsets.UTF_8, downloader)
|
||||
val contacts = Contact.fromReader(importReader, downloader)
|
||||
|
||||
if (contacts.size == 0) {
|
||||
App.log.warning("Empty/invalid file.")
|
||||
|
@ -78,7 +78,7 @@ class LocalCalendarImportFragment : ListFragment() {
|
||||
}
|
||||
|
||||
override fun getChild(groupPosition: Int, childPosititon: Int): Any {
|
||||
return calendarAccounts[groupPosition].getCalendars()[childPosititon].displayName
|
||||
return calendarAccounts[groupPosition].getCalendars()[childPosititon].displayName!!
|
||||
}
|
||||
|
||||
override fun getChildId(groupPosition: Int, childPosition: Int): Long {
|
||||
@ -198,9 +198,9 @@ class LocalCalendarImportFragment : ListFragment() {
|
||||
val result = ResultFragment.ImportResult()
|
||||
try {
|
||||
val localCalendar = LocalCalendar.findByName(account,
|
||||
context!!.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI),
|
||||
LocalCalendar.Factory.INSTANCE, info!!.uid!!)
|
||||
val localEvents = fromCalendar.all
|
||||
context!!.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)!!,
|
||||
LocalCalendar.Factory, info!!.uid!!)
|
||||
val localEvents = fromCalendar.findAll()
|
||||
val total = localEvents.size
|
||||
progressDialog!!.max = total
|
||||
result.total = total.toLong()
|
||||
@ -208,7 +208,7 @@ class LocalCalendarImportFragment : ListFragment() {
|
||||
for (currentLocalEvent in localEvents) {
|
||||
val event = currentLocalEvent.event
|
||||
try {
|
||||
val localEvent = LocalEvent(localCalendar!!, event, null, null)
|
||||
val localEvent = LocalEvent(localCalendar!!, event!!, null, null)
|
||||
localEvent.addAsDirty()
|
||||
result.added++
|
||||
} catch (e: CalendarStorageException) {
|
||||
|
@ -133,7 +133,7 @@ class LocalContactImportFragment : Fragment() {
|
||||
val addressBook = LocalAddressBook.findByUid(context!!,
|
||||
context!!.contentResolver.acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI)!!,
|
||||
account, info!!.uid!!)
|
||||
val localContacts = localAddressBook.all
|
||||
val localContacts = localAddressBook.findAll()
|
||||
val total = localContacts.size
|
||||
progressDialog!!.max = total
|
||||
result.total = total.toLong()
|
||||
@ -142,7 +142,7 @@ class LocalContactImportFragment : Fragment() {
|
||||
val contact = currentLocalContact.contact
|
||||
|
||||
try {
|
||||
val localContact = LocalContact(addressBook!!, contact, null, null)
|
||||
val localContact = LocalContact(addressBook!!, contact!!, null, null)
|
||||
localContact.createAsDirty()
|
||||
result.added++
|
||||
} catch (e: ContactsStorageException) {
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 268473341cb761a0676f1746ff4467e48973f972
|
||||
Subproject commit 2437b0b7aedf4fa1907a88c72781cff4c8291e40
|
@ -1 +1 @@
|
||||
Subproject commit 3974799d7790f47987f7ae95fe444ab4442e7786
|
||||
Subproject commit 42d5cc3f8b16c628fa13a5a3b0f211e6660fb084
|
Loading…
Reference in New Issue
Block a user