From 6afa2cff3d7c65a88cebd16d7edd62ff9349de4c Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Sat, 9 Nov 2024 11:58:48 +0100 Subject: [PATCH] [WIP] Move local collection management from companion objects to LocalDataStore --- .../davdroid/resource/LocalAddressBook.kt | 110 +----------- .../resource/LocalAddressBookStore.kt | 156 ++++++++++++++++++ .../davdroid/resource/LocalCalendar.kt | 25 +-- .../davdroid/resource/LocalCalendarStore.kt | 72 ++++++++ .../davdroid/resource/LocalCollection.kt | 8 - .../davdroid/resource/LocalDataStore.kt | 42 +++++ .../davdroid/resource/LocalJtxCollection.kt | 2 +- .../resource/LocalJtxCollectionStore.kt | 24 +++ .../davdroid/resource/LocalTaskList.kt | 2 - .../davdroid/resource/LocalTaskListStore.kt | 24 +++ .../settings/AccountSettingsMigrations.kt | 6 +- .../davdroid/sync/AddressBookSyncer.kt | 36 ++-- .../bitfire/davdroid/sync/CalendarSyncer.kt | 19 +-- .../at/bitfire/davdroid/sync/JtxSyncer.kt | 16 +- .../kotlin/at/bitfire/davdroid/sync/Syncer.kt | 30 ++-- .../at/bitfire/davdroid/sync/TaskSyncer.kt | 16 +- .../at/bitfire/davdroid/util/CompatUtils.kt | 5 +- gradle.properties | 5 +- gradle/libs.versions.toml | 2 +- 19 files changed, 377 insertions(+), 223 deletions(-) create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBookStore.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendarStore.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/LocalDataStore.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxCollectionStore.kt create mode 100644 app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTaskListStore.kt diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBook.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBook.kt index e1ca08239..c0827d7e2 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBook.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBook.kt @@ -149,59 +149,6 @@ open class LocalAddressBook @AssistedInject constructor( return number } - /** - * Updates the address book settings. - * - * @param info collection where to take the settings from - * @param forceReadOnly `true`: set the address book to "force read-only"; - * `false`: determine read-only flag from [info]; - */ - fun update(info: Collection, forceReadOnly: Boolean) { - logger.log(Level.INFO, "Updating local address book $addressBookAccount with collection $info") - val accountManager = AccountManager.get(context) - - // Update the account name - val newAccountName = accountName(context, info) - if (addressBookAccount.name != newAccountName) - // rename, move contacts/groups and update [AndroidAddressBook.]account - renameAccount(newAccountName) - - // Update the account user data - accountManager.setAndVerifyUserData(addressBookAccount, USER_DATA_COLLECTION_ID, info.id.toString()) - accountManager.setAndVerifyUserData(addressBookAccount, USER_DATA_URL, info.url.toString()) - - // Set contacts provider settings - settings = contactsProviderSettings - - // Update force read only - val nowReadOnly = shouldBeReadOnly(info, forceReadOnly) - if (nowReadOnly != readOnly) { - logger.info("Address book now read-only = $nowReadOnly, updating contacts") - - // update address book itself - readOnly = nowReadOnly - - // update raw contacts - val rawContactValues = ContentValues(1) - rawContactValues.put(RawContacts.RAW_CONTACT_IS_READ_ONLY, if (nowReadOnly) 1 else 0) - provider!!.update(rawContactsSyncUri(), rawContactValues, null, null) - - // update data rows - val dataValues = ContentValues(1) - dataValues.put(ContactsContract.Data.IS_READ_ONLY, if (nowReadOnly) 1 else 0) - provider!!.update(syncAdapterURI(ContactsContract.Data.CONTENT_URI), dataValues, null, null) - - // update group rows - val groupValues = ContentValues(1) - groupValues.put(Groups.GROUP_IS_READ_ONLY, if (nowReadOnly) 1 else 0) - provider!!.update(groupsSyncUri(), groupValues, null, null) - } - - - // make sure it will still be synchronized when contacts are updated - updateSyncFrameworkSettings() - } - /** * Renames an address book account and moves the contacts and groups (without making them dirty). * Does not keep user data of the old account, so these have to be set again. @@ -215,7 +162,6 @@ open class LocalAddressBook @AssistedInject constructor( * * @return whether the account was renamed successfully */ - @VisibleForTesting internal fun renameAccount(newName: String): Boolean { val oldAccount = addressBookAccount logger.info("Renaming address book from \"${oldAccount.name}\" to \"$newName\"") @@ -249,11 +195,6 @@ open class LocalAddressBook @AssistedInject constructor( return true } - override fun deleteCollection(): Boolean { - val accountManager = AccountManager.get(context) - return accountManager.removeAccountExplicitly(addressBookAccount) - } - /** * Updates the sync framework settings for this address book: @@ -393,58 +334,9 @@ open class LocalAddressBook @AssistedInject constructor( const val USER_DATA_COLLECTION_ID = "collection_id" const val USER_DATA_READ_ONLY = "read_only" - /** - * Contacts Provider Settings (equal for every address book) - */ - val contactsProviderSettings = ContentValues(2).apply { - // SHOULD_SYNC is just a hint that an account's contacts (the contacts of this local - // address book) are syncable. - put(ContactsContract.Settings.SHOULD_SYNC, 1) - // UNGROUPED_VISIBLE is required for making contacts work over Bluetooth (especially - // with some car systems). - put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1) - } // create/query/delete - /** - * Creates a new local address book. - * - * @param context app context to resolve string resources - * @param provider contacts provider client - * @param info collection where to take the name and settings from - * @param forceReadOnly `true`: set the address book to "force read-only"; `false`: determine read-only flag from [info] - */ - fun create(context: Context, provider: ContentProviderClient, info: Collection, forceReadOnly: Boolean): LocalAddressBook { - val entryPoint = EntryPointAccessors.fromApplication(context) - val logger = entryPoint.logger() - - val account = Account(accountName(context, info), context.getString(R.string.account_type_address_book)) - val userData = initialUserData(info.url.toString(), info.id.toString()) - logger.log(Level.INFO, "Creating local address book $account", userData) - if (!SystemAccountUtils.createAccount(context, account, userData)) - throw IllegalStateException("Couldn't create address book account") - - val factory = entryPoint.localAddressBookFactory() - val addressBook = factory.create(account, provider) - - addressBook.updateSyncFrameworkSettings() - addressBook.settings = contactsProviderSettings - addressBook.readOnly = shouldBeReadOnly(info, forceReadOnly) - - return addressBook - } - - /** - * Determines whether the address book should be set to read-only. - * - * @param forceReadOnly Whether (usually managed, app-wide) setting should overwrite local read-only information - * @param info Collection data to determine read-only status from (either user-set read-only flag or missing write privilege) - */ - @VisibleForTesting - internal fun shouldBeReadOnly(info: Collection, forceReadOnly: Boolean): Boolean = - info.readOnly() || forceReadOnly - /** * Finds a [LocalAddressBook] based on its corresponding collection. * @@ -512,7 +404,7 @@ open class LocalAddressBook @AssistedInject constructor( return sb.toString() } - private fun initialUserData(url: String, collectionId: String): Bundle { + internal fun initialUserData(url: String, collectionId: String): Bundle { val bundle = Bundle(3) bundle.putString(USER_DATA_COLLECTION_ID, collectionId) bundle.putString(USER_DATA_URL, url) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBookStore.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBookStore.kt new file mode 100644 index 000000000..afbaeae35 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBookStore.kt @@ -0,0 +1,156 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.ContentProviderClient +import android.content.ContentValues +import android.content.Context +import android.provider.ContactsContract +import android.provider.ContactsContract.Groups +import android.provider.ContactsContract.RawContacts +import androidx.annotation.VisibleForTesting +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.resource.LocalAddressBook.Companion.USER_DATA_COLLECTION_ID +import at.bitfire.davdroid.resource.LocalAddressBook.Companion.USER_DATA_URL +import at.bitfire.davdroid.resource.LocalAddressBook.Companion.accountName +import at.bitfire.davdroid.settings.Settings +import at.bitfire.davdroid.settings.SettingsManager +import at.bitfire.davdroid.sync.account.SystemAccountUtils +import at.bitfire.davdroid.util.setAndVerifyUserData +import dagger.hilt.android.qualifiers.ApplicationContext +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject + +class LocalAddressBookStore @Inject constructor( + val addressBookFactory: LocalAddressBook.Factory, + @ApplicationContext val context: Context, + val logger: Logger, + val settings: SettingsManager +): LocalDataStore { + + /** whether a (usually managed) setting wants all address-books to be read-only **/ + val forceAllReadOnly: Boolean + get() = settings.getBoolean(Settings.FORCE_READ_ONLY_ADDRESSBOOKS) + + + override fun create(provider: ContentProviderClient, fromCollection: Collection): LocalAddressBook? { + val name = LocalAddressBook.accountName(context, fromCollection) + val account = createAccount( + name = name, + id = fromCollection.id, + url = fromCollection.url.toString() + ) ?: return null + + val addressBook = addressBookFactory.create(account, provider) + + // update settings + addressBook.updateSyncFrameworkSettings() + addressBook.settings = contactsProviderSettings + addressBook.readOnly = forceAllReadOnly || fromCollection.readOnly() + + return addressBook + } + + fun createAccount(name: String, id: Long, url: String): Account? { + // create account + val account = Account(name, context.getString(R.string.account_type_address_book)) + val userData = LocalAddressBook.initialUserData( + url = url, + collectionId = id.toString() + ) + if (!SystemAccountUtils.createAccount(context, account, userData)) { + logger.warning("Couldn't create address book account: $account") + return null + } + + return account + } + + override fun update(provider: ContentProviderClient, localCollection: LocalAddressBook, fromCollection: Collection) { + var currentAccount = localCollection.addressBookAccount + logger.log(Level.INFO, "Updating local address book $currentAccount from collection $fromCollection") + + // Update the account name + val newAccountName = accountName(context, fromCollection) + if (currentAccount.name != newAccountName) { + // rename, move contacts/groups and update [AndroidAddressBook.]account + localCollection.renameAccount(newAccountName) + currentAccount.name = newAccountName + } + + // Update the account user data + val accountManager = AccountManager.get(context) + accountManager.setAndVerifyUserData(currentAccount, USER_DATA_COLLECTION_ID, fromCollection.id.toString()) + accountManager.setAndVerifyUserData(currentAccount, USER_DATA_URL, fromCollection.url.toString()) + + // Set contacts provider settings + localCollection.settings = contactsProviderSettings + + // Update force read only + val nowReadOnly = shouldBeReadOnly(fromCollection, forceAllReadOnly) + if (nowReadOnly != localCollection.readOnly) { + logger.info("Address book now read-only = $nowReadOnly, updating contacts") + + // update address book itself + localCollection.readOnly = nowReadOnly + + // update raw contacts + val rawContactValues = ContentValues(1) + rawContactValues.put(RawContacts.RAW_CONTACT_IS_READ_ONLY, if (nowReadOnly) 1 else 0) + provider.update(localCollection.rawContactsSyncUri(), rawContactValues, null, null) + + // update data rows + val dataValues = ContentValues(1) + dataValues.put(ContactsContract.Data.IS_READ_ONLY, if (nowReadOnly) 1 else 0) + provider.update(localCollection.syncAdapterURI(ContactsContract.Data.CONTENT_URI), dataValues, null, null) + + // update group rows + val groupValues = ContentValues(1) + groupValues.put(Groups.GROUP_IS_READ_ONLY, if (nowReadOnly) 1 else 0) + provider.update(localCollection.groupsSyncUri(), groupValues, null, null) + } + + + // make sure it will still be synchronized when contacts are updated + localCollection.updateSyncFrameworkSettings() + } + + + override fun delete(localCollection: LocalAddressBook) { + val accountManager = AccountManager.get(context) + accountManager.removeAccountExplicitly(localCollection.addressBookAccount) + } + + + companion object { + + /** + * Contacts Provider Settings (equal for every address book) + */ + val contactsProviderSettings = ContentValues(2).apply { + // SHOULD_SYNC is just a hint that an account's contacts (the contacts of this local address book) are syncable. + put(ContactsContract.Settings.SHOULD_SYNC, 1) + + // UNGROUPED_VISIBLE is required for making contacts work over Bluetooth (especially with some car systems). + put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1) + } + + /** + * Determines whether the address book should be set to read-only. + * + * @param forceAllReadOnly Whether (usually managed, app-wide) setting should overwrite local read-only information + * @param info Collection data to determine read-only status from (either user-set read-only flag or missing write privilege) + */ + @VisibleForTesting + internal fun shouldBeReadOnly(info: Collection, forceAllReadOnly: Boolean): Boolean = + info.readOnly() || forceAllReadOnly + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendar.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendar.kt index 535c683c9..b6ab4cfc5 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendar.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendar.kt @@ -42,28 +42,7 @@ class LocalCalendar private constructor( private val logger: Logger get() = Logger.getGlobal() - fun create(account: Account, provider: ContentProviderClient, info: Collection): Uri { - // If the collection doesn't have a color, use a default color. - if (info.color != null) - info.color = Constants.DAVDROID_GREEN_RGBA - - val values = valuesFromCollectionInfo(info, withColor = 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) - - // Email address for scheduling. Used by the calendar provider to determine whether the - // user is ORGANIZER/ATTENDEE for a certain event. - 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 create(account, provider, values) - } - - private fun valuesFromCollectionInfo(info: Collection, withColor: Boolean): ContentValues { + fun valuesFromCollectionInfo(info: Collection, withColor: Boolean): ContentValues { val values = ContentValues() values.put(Calendars.NAME, info.url.toString()) values.put(Calendars.CALENDAR_DISPLAY_NAME, @@ -111,8 +90,6 @@ class LocalCalendar private constructor( override val readOnly get() = accessLevel <= Calendars.CAL_ACCESS_READ - override fun deleteCollection(): Boolean = delete() - override var lastSyncState: SyncState? get() = provider.query(calendarSyncURI(), arrayOf(COLUMN_SYNC_STATE), null, null, null)?.use { cursor -> if (cursor.moveToNext()) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendarStore.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendarStore.kt new file mode 100644 index 000000000..87643c370 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCalendarStore.kt @@ -0,0 +1,72 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.accounts.Account +import android.content.ContentProviderClient +import android.content.ContentUris +import android.content.Context +import android.provider.CalendarContract.Calendars +import at.bitfire.davdroid.Constants +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.AppDatabase +import at.bitfire.davdroid.db.Collection +import at.bitfire.davdroid.resource.LocalCalendar.Companion.valuesFromCollectionInfo +import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.ical4android.AndroidCalendar +import dagger.hilt.android.qualifiers.ApplicationContext +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject + +class LocalCalendarStore @Inject constructor( + @ApplicationContext val context: Context, + val accountSettingsFactory: AccountSettings.Factory, + db: AppDatabase, + val logger: Logger +): LocalDataStore { + + private val serviceDao = db.serviceDao() + + + override fun create(provider: ContentProviderClient, fromCollection: Collection): LocalCalendar? { + val service = serviceDao.get(fromCollection.serviceId) ?: throw IllegalArgumentException("Couldn't fetch DB service from collection") + val account = Account(service.accountName, context.getString(R.string.account_type)) + + // If the collection doesn't have a color, use a default color. + if (fromCollection.color != null) + fromCollection.color = Constants.DAVDROID_GREEN_RGBA + + val values = valuesFromCollectionInfo(fromCollection, withColor = 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) + + // Email address for scheduling. Used by the calendar provider to determine whether the + // user is ORGANIZER/ATTENDEE for a certain event. + values.put(Calendars.OWNER_ACCOUNT, account.name) + + // flag as visible & syncable at creation, might be changed by user at any time + values.put(Calendars.VISIBLE, 1) + values.put(Calendars.SYNC_EVENTS, 1) + + logger.log(Level.INFO, "Adding local calendar", values) + val uri = AndroidCalendar.create(account, provider, values) + return AndroidCalendar.findByID(account, provider, LocalCalendar.Factory, ContentUris.parseId(uri)) + } + + override fun update(provider: ContentProviderClient, localCollection: LocalCalendar, fromCollection: Collection) { + logger.log(Level.FINE, "Updating local calendar ${fromCollection.url}", fromCollection) + val accountSettings = accountSettingsFactory.create(localCollection.account) + localCollection.update(fromCollection, accountSettings.getManageCalendarColors()) + } + + override fun delete(localCollection: LocalCalendar) { + logger.log(Level.INFO, "Deleting local calendar", localCollection) + localCollection.delete() + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCollection.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCollection.kt index a021fe252..2028866e7 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCollection.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalCollection.kt @@ -4,7 +4,6 @@ package at.bitfire.davdroid.resource -import android.provider.CalendarContract.Events import at.bitfire.davdroid.db.SyncState interface LocalCollection> { @@ -27,13 +26,6 @@ interface LocalCollection> { */ val readOnly: Boolean - /** - * Deletes the local collection. - * - * @return true if the collection was deleted, false otherwise - */ - fun deleteCollection(): Boolean - /** * Finds local resources of this collection which have been marked as *deleted* by the user * or an app acting on their behalf. diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalDataStore.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalDataStore.kt new file mode 100644 index 000000000..96ba73ab2 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalDataStore.kt @@ -0,0 +1,42 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.content.ContentProviderClient +import at.bitfire.davdroid.db.Collection + +/** + * Represents a local data store for a specific collection type. + * Manages creation, update, and deletion of collections of the given type. + */ +interface LocalDataStore> { + + /** + * Creates a new local collection from the given (remote) collection info. + * + * @param provider the content provider client + * @param fromCollection collection info + * + * @return the new local collection, or `null` if creation failed + */ + fun create(provider: ContentProviderClient, fromCollection: Collection): T? + + /** + * Updates the local collection with the data from the given (remote) collection info. + * + * @param provider the content provider client + * @param localCollection the local collection to update + * @param fromCollection collection info + */ + fun update(provider: ContentProviderClient, localCollection: T, fromCollection: Collection) + + /** + * Deletes the local collection. + * + * @param localCollection the local collection to delete + */ + fun delete(localCollection: T) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxCollection.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxCollection.kt index 6e0eb082c..b80287077 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxCollection.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxCollection.kt @@ -62,7 +62,7 @@ class LocalJtxCollection(account: Account, client: ContentProviderClient, id: Lo override val readOnly: Boolean get() = throw NotImplementedError() - override fun deleteCollection(): Boolean = delete() + //override fun deleteCollection(): Boolean = delete() override val tag: String get() = "jtx-${account.name}-$id" diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxCollectionStore.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxCollectionStore.kt new file mode 100644 index 000000000..e56d78934 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalJtxCollectionStore.kt @@ -0,0 +1,24 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.content.ContentProviderClient +import at.bitfire.davdroid.db.Collection + +class LocalJtxCollectionStore: LocalDataStore { + + override fun create(provider: ContentProviderClient, fromCollection: Collection): LocalJtxCollection? { + TODO("Not yet implemented") + } + + override fun update(provider: ContentProviderClient, localCollection: LocalJtxCollection, fromCollection: Collection) { + TODO("Not yet implemented") + } + + override fun delete(localCollection: LocalJtxCollection) { + TODO("Not yet implemented") + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTaskList.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTaskList.kt index ee0733b74..d6f6be758 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTaskList.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTaskList.kt @@ -88,8 +88,6 @@ class LocalTaskList private constructor( accessLevel != TaskListColumns.ACCESS_LEVEL_UNDEFINED && accessLevel <= TaskListColumns.ACCESS_LEVEL_READ - override fun deleteCollection(): Boolean = delete() - override val collectionUrl: String? get() = syncId diff --git a/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTaskListStore.kt b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTaskListStore.kt new file mode 100644 index 000000000..f6f91055f --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/resource/LocalTaskListStore.kt @@ -0,0 +1,24 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.resource + +import android.content.ContentProviderClient +import at.bitfire.davdroid.db.Collection + +class LocalTaskListStore: LocalDataStore { + + override fun create(provider: ContentProviderClient, fromCollection: Collection): LocalTaskList? { + TODO("Not yet implemented") + } + + override fun update(provider: ContentProviderClient, localCollection: LocalTaskList, fromCollection: Collection) { + TODO("Not yet implemented") + } + + override fun delete(localCollection: LocalTaskList) { + TODO("Not yet implemented") + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettingsMigrations.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettingsMigrations.kt index 91558b94f..a15f65051 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettingsMigrations.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettingsMigrations.kt @@ -24,6 +24,7 @@ import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.repository.DavCollectionRepository import at.bitfire.davdroid.repository.DavServiceRepository import at.bitfire.davdroid.resource.LocalAddressBook +import at.bitfire.davdroid.resource.LocalAddressBookStore import at.bitfire.davdroid.resource.LocalTask import at.bitfire.davdroid.sync.SyncUtils import at.bitfire.davdroid.sync.TasksAppManager @@ -53,6 +54,7 @@ class AccountSettingsMigrations @AssistedInject constructor( @ApplicationContext val context: Context, private val collectionRepository: DavCollectionRepository, private val db: AppDatabase, + private val localAddressBookStore: LocalAddressBookStore, private val localAddressBookFactory: LocalAddressBook.Factory, private val logger: Logger, private val serviceRepository: DavServiceRepository, @@ -97,11 +99,11 @@ class AccountSettingsMigrations @AssistedInject constructor( for (oldAddressBookAccount in oldAddressBookAccounts) { // Old address books only have a URL, so use it to determine the collection ID logger.info("Migrating address book ${oldAddressBookAccount.name}") + val oldAddressBook = localAddressBookFactory.create(oldAddressBookAccount, provider) val url = accountManager.getUserData(oldAddressBookAccount, LocalAddressBook.USER_DATA_URL) collectionRepository.getByServiceAndUrl(service.id, url)?.let { collection -> // Set collection ID and rename the account - val localAddressBook = localAddressBookFactory.create(oldAddressBookAccount, provider) - localAddressBook.update(collection, /* read-only flag will be updated at next sync */ forceReadOnly = false) + localAddressBookStore.update(provider, oldAddressBook, collection) } } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/AddressBookSyncer.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/AddressBookSyncer.kt index 9d256cf2d..6f5d256e6 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/AddressBookSyncer.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/AddressBookSyncer.kt @@ -11,8 +11,7 @@ import at.bitfire.davdroid.db.Collection import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.network.HttpClient import at.bitfire.davdroid.resource.LocalAddressBook -import at.bitfire.davdroid.settings.Settings -import at.bitfire.davdroid.settings.SettingsManager +import at.bitfire.davdroid.resource.LocalAddressBookStore import at.bitfire.davdroid.util.setAndVerifyUserData import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -26,20 +25,16 @@ class AddressBookSyncer @AssistedInject constructor( @Assisted account: Account, @Assisted extras: Array, @Assisted syncResult: SyncResult, - private val contactsSyncManagerFactory: ContactsSyncManager.Factory, - settingsManager: SettingsManager -): Syncer(account, extras, syncResult) { + addressBookStore: LocalAddressBookStore, + private val contactsSyncManagerFactory: ContactsSyncManager.Factory +): Syncer(account, extras, syncResult) { @AssistedFactory interface Factory { fun create(account: Account, extras: Array, syncResult: SyncResult): AddressBookSyncer } - companion object { - const val PREVIOUS_GROUP_METHOD = "previous_group_method" - } - - private val forceAllReadOnly = settingsManager.getBoolean(Settings.FORCE_READ_ONLY_ADDRESSBOOKS) + override val dataStore = addressBookStore override val serviceType: String get() = Service.TYPE_CARDDAV @@ -58,20 +53,6 @@ class AddressBookSyncer @AssistedInject constructor( override fun getDbSyncCollections(serviceId: Long): List = collectionRepository.getByServiceAndSync(serviceId) - override fun update(localCollection: LocalAddressBook, remoteCollection: Collection) { - try { - logger.log(Level.FINE, "Updating local address book ${remoteCollection.url}", remoteCollection) - localCollection.update(remoteCollection, forceAllReadOnly) - } catch (e: Exception) { - logger.log(Level.WARNING, "Couldn't rename address book account", e) - } - } - - override fun create(provider: ContentProviderClient, remoteCollection: Collection): LocalAddressBook { - logger.log(Level.INFO, "Adding local address book", remoteCollection) - return LocalAddressBook.create(context, provider, remoteCollection, forceAllReadOnly) - } - override fun syncCollection(provider: ContentProviderClient, localCollection: LocalAddressBook, remoteCollection: Collection) { logger.info("Synchronizing address book: ${localCollection.addressBookAccount.name}") syncAddressBook( @@ -133,4 +114,11 @@ class AddressBookSyncer @AssistedInject constructor( logger.info("Contacts sync complete") } + + companion object { + + const val PREVIOUS_GROUP_METHOD = "previous_group_method" + + } + } \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/CalendarSyncer.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/CalendarSyncer.kt index 7aa9d69a3..d90059ac7 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/CalendarSyncer.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/CalendarSyncer.kt @@ -6,16 +6,15 @@ package at.bitfire.davdroid.sync import android.accounts.Account import android.content.ContentProviderClient -import android.content.ContentUris import android.provider.CalendarContract import at.bitfire.davdroid.db.Collection import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.resource.LocalCalendar +import at.bitfire.davdroid.resource.LocalCalendarStore import at.bitfire.ical4android.AndroidCalendar import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import java.util.logging.Level /** * Sync logic for calendars @@ -24,14 +23,17 @@ class CalendarSyncer @AssistedInject constructor( @Assisted account: Account, @Assisted extras: Array, @Assisted syncResult: SyncResult, + calendarStore: LocalCalendarStore, private val calendarSyncManagerFactory: CalendarSyncManager.Factory -): Syncer(account, extras, syncResult) { +): Syncer(account, extras, syncResult) { @AssistedFactory interface Factory { fun create(account: Account, extras: Array, syncResult: SyncResult): CalendarSyncer } + override val dataStore = calendarStore + override val serviceType: String get() = Service.TYPE_CALDAV override val authority: String @@ -69,15 +71,4 @@ class CalendarSyncer @AssistedInject constructor( syncManager.performSync() } - override fun update(localCollection: LocalCalendar, remoteCollection: Collection) { - logger.log(Level.FINE, "Updating local calendar ${remoteCollection.url}", remoteCollection) - localCollection.update(remoteCollection, accountSettings.getManageCalendarColors()) - } - - override fun create(provider: ContentProviderClient, remoteCollection: Collection): LocalCalendar { - logger.log(Level.INFO, "Adding local calendar", remoteCollection) - val uri = LocalCalendar.create(account, provider, remoteCollection) - return AndroidCalendar.findByID(account, provider, LocalCalendar.Factory, ContentUris.parseId(uri)) - } - } \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/JtxSyncer.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/JtxSyncer.kt index 1624b06e5..6601543e6 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/JtxSyncer.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/JtxSyncer.kt @@ -7,15 +7,14 @@ package at.bitfire.davdroid.sync import android.accounts.Account import android.accounts.AccountManager import android.content.ContentProviderClient -import android.content.ContentUris import android.os.Build import at.bitfire.davdroid.db.Collection import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.repository.PrincipalRepository import at.bitfire.davdroid.resource.LocalJtxCollection +import at.bitfire.davdroid.resource.LocalJtxCollectionStore import at.bitfire.ical4android.JtxCollection import at.bitfire.ical4android.TaskProvider -import at.techbee.jtx.JtxContract import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -31,13 +30,16 @@ class JtxSyncer @AssistedInject constructor( private val jtxSyncManagerFactory: JtxSyncManager.Factory, private val principalRepository: PrincipalRepository, private val tasksAppManager: dagger.Lazy -): Syncer(account, extras, syncResult) { +): Syncer(account, extras, syncResult) { @AssistedFactory interface Factory { fun create(account: Account, extras: Array, syncResult: SyncResult): JtxSyncer } + override val dataStore: LocalJtxCollectionStore + get() = TODO("Not yet implemented") + override val serviceType: String get() = Service.TYPE_CALDAV override val authority: String @@ -72,13 +74,13 @@ class JtxSyncer @AssistedInject constructor( override fun getDbSyncCollections(serviceId: Long): List = collectionRepository.getSyncJtxCollections(serviceId) - override fun update(localCollection: LocalJtxCollection, remoteCollection: Collection) { + /*override fun update(localCollection: LocalJtxCollection, remoteCollection: Collection) { logger.log(Level.FINE, "Updating local jtx collection ${remoteCollection.url}", remoteCollection) val owner = remoteCollection.ownerId?.let { principalRepository.get(it) } localCollection.updateCollection(remoteCollection, owner, accountSettings.getManageCalendarColors()) - } + }*/ - override fun create(provider: ContentProviderClient, remoteCollection: Collection): LocalJtxCollection { + /*override fun create(provider: ContentProviderClient, remoteCollection: Collection): LocalJtxCollection { logger.log(Level.INFO, "Adding local jtx collection", remoteCollection) val owner = remoteCollection.ownerId?.let { principalRepository.get(it) } val uri = LocalJtxCollection.create(account, provider, remoteCollection, owner) @@ -90,7 +92,7 @@ class JtxSyncer @AssistedInject constructor( "${JtxContract.JtxCollection.ID} = ?", arrayOf("${ContentUris.parseId(uri)}") ).first() - } + }*/ override fun syncCollection(provider: ContentProviderClient, localCollection: LocalJtxCollection, remoteCollection: Collection) { logger.info("Synchronizing jtx collection $localCollection") diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/Syncer.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/Syncer.kt index e83bbbd88..6bb32dbb9 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/Syncer.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/Syncer.kt @@ -15,6 +15,7 @@ import at.bitfire.davdroid.network.HttpClient import at.bitfire.davdroid.repository.DavCollectionRepository import at.bitfire.davdroid.repository.DavServiceRepository import at.bitfire.davdroid.resource.LocalCollection +import at.bitfire.davdroid.resource.LocalDataStore import at.bitfire.davdroid.settings.AccountSettings import dagger.hilt.android.qualifiers.ApplicationContext import okhttp3.HttpUrl @@ -29,7 +30,7 @@ import javax.inject.Inject * * Contains generic sync code, equal for all sync authorities. */ -abstract class Syncer>( +abstract class Syncer, CollectionType: LocalCollection<*>>( protected val account: Account, protected val extras: Array, protected val syncResult: SyncResult @@ -58,6 +59,8 @@ abstract class Syncer>( } + abstract val dataStore: StoreType + @Inject lateinit var accountSettingsFactory: AccountSettings.Factory @@ -148,12 +151,12 @@ abstract class Syncer>( if (dbCollection == null) { // Collection not available in db = on server (anymore), delete and remove from the updated list logger.fine("Deleting local collection ${localCollection.title}") - localCollection.deleteCollection() + dataStore.delete(localCollection) updatedLocalCollections -= localCollection } else { // Collection exists locally, update local collection and remove it from "to be created" map logger.fine("Updating local collection ${localCollection.title} with $dbCollection") - update(localCollection, dbCollection) + dataStore.update(provider, localCollection, dbCollection) newDbCollections -= dbCollection.url } } @@ -183,7 +186,10 @@ abstract class Syncer>( provider: ContentProviderClient, dbCollections: List ): List = - dbCollections.map { collection -> create(provider, collection) } + dbCollections.map { collection -> + dataStore.create(provider, collection) + ?: throw IllegalStateException("Couldn't create local collection for $collection") + } /** * Synchronize the actual collection contents. @@ -233,22 +239,6 @@ abstract class Syncer>( */ abstract fun getDbSyncCollections(serviceId: Long): List - /** - * Updates an existing local collection (in the content provider) with remote collection information (from the DB). - * - * @param localCollection The local collection to be updated - * @param remoteCollection The new remote collection information - */ - abstract fun update(localCollection: CollectionType, remoteCollection: Collection) - - /** - * Creates a new local collection (in the content provider) from remote collection information (from the DB). - * - * @param provider The content provider client to create the local collection - * @param remoteCollection The remote collection to be created locally - */ - abstract fun create(provider: ContentProviderClient, remoteCollection: Collection): CollectionType - /** * Synchronizes local with remote collection contents. * diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/TaskSyncer.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/TaskSyncer.kt index ef5625b3a..95b24d853 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/TaskSyncer.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/TaskSyncer.kt @@ -7,18 +7,17 @@ package at.bitfire.davdroid.sync import android.accounts.Account import android.accounts.AccountManager import android.content.ContentProviderClient -import android.content.ContentUris import android.os.Build import at.bitfire.davdroid.db.Collection import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.resource.LocalTaskList +import at.bitfire.davdroid.resource.LocalTaskListStore import at.bitfire.ical4android.DmfsTaskList import at.bitfire.ical4android.TaskProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import org.dmfs.tasks.contract.TaskContract.TaskLists -import java.util.logging.Level /** * Sync logic for tasks in CalDAV collections ({@code VTODO}). @@ -30,7 +29,7 @@ class TaskSyncer @AssistedInject constructor( @Assisted syncResult: SyncResult, private val tasksAppManager: dagger.Lazy, private val tasksSyncManagerFactory: TasksSyncManager.Factory, -): Syncer(account, extras, syncResult) { +): Syncer(account, extras, syncResult) { @AssistedFactory interface Factory { @@ -39,6 +38,9 @@ class TaskSyncer @AssistedInject constructor( private val providerName = TaskProvider.ProviderName.fromAuthority(authority) + override val dataStore: LocalTaskListStore + get() = TODO("Not yet implemented") + override val serviceType: String get() = Service.TYPE_CALDAV @@ -70,16 +72,16 @@ class TaskSyncer @AssistedInject constructor( override fun getDbSyncCollections(serviceId: Long): List = collectionRepository.getSyncTaskLists(serviceId) - override fun update(localCollection: LocalTaskList, remoteCollection: Collection) { + /*override fun update(localCollection: LocalTaskList, remoteCollection: Collection) { logger.log(Level.FINE, "Updating local task list ${remoteCollection.url}", remoteCollection) localCollection.update(remoteCollection, accountSettings.getManageCalendarColors()) - } + }*/ - override fun create(provider: ContentProviderClient, remoteCollection: Collection): LocalTaskList { + /*override fun create(provider: ContentProviderClient, remoteCollection: Collection): LocalTaskList { logger.log(Level.INFO, "Adding local task list", remoteCollection) val uri = LocalTaskList.create(account, provider, providerName, remoteCollection) return DmfsTaskList.findByID(account, provider, providerName, LocalTaskList.Factory, ContentUris.parseId(uri)) - } + }*/ override fun syncCollection(provider: ContentProviderClient, localCollection: LocalTaskList, remoteCollection: Collection) { logger.info("Synchronizing task list #${localCollection.id} [${localCollection.syncId}]") diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/CompatUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/CompatUtils.kt index 1afcc310b..43eb97114 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/util/CompatUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/CompatUtils.kt @@ -17,9 +17,8 @@ import java.util.logging.Logger */ fun AccountManager.setAndVerifyUserData(account: Account, key: String, value: String?) { for (i in 1..10) { - setUserData(account, key, value) - if (getUserData(account, key) == value) - return /* success */ + if (getUserData(account, key) != value) + setUserData(account, key, value) Thread.sleep(100) } diff --git a/gradle.properties b/gradle.properties index 4755d3130..37b6618e6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,4 +13,7 @@ android.enableR8.fullMode=false # org.gradle.configuration-cache.problems=warn # https://docs.gradle.org/current/userguide/build_cache.html -# org.gradle.caching=true \ No newline at end of file +# org.gradle.caching=true + +# temporary fix for https://github.com/google/ksp/issues/2072 +ksp.incremental=false \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8c64de057..3768907db 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,7 +32,7 @@ hilt = "2.52" kotlin = "2.0.21" kotlinx-coroutines = "1.9.0" # see https://github.com/google/ksp/releases for version numbers -ksp = "2.0.21-1.0.25" +ksp = "2.0.21-1.0.26" mikepenz-aboutLibraries = "11.2.3" nsk90-kstatemachine = "0.31.1" mockk = "1.13.13"