Skip to content

Commit

Permalink
[WIP] Move local collection management from companion objects to Loca…
Browse files Browse the repository at this point in the history
…lDataStore
  • Loading branch information
rfc2822 committed Nov 9, 2024
1 parent 32925dc commit 6afa2cf
Show file tree
Hide file tree
Showing 19 changed files with 377 additions and 223 deletions.
110 changes: 1 addition & 109 deletions app/src/main/kotlin/at/bitfire/davdroid/resource/LocalAddressBook.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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\"")
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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<LocalAddressBookCompanionEntryPoint>(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.
*
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<LocalAddressBook> {

/** 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

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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())
Expand Down
Loading

0 comments on commit 6afa2cf

Please sign in to comment.