From 5b804ef84c0928fcf3ea569a5028b85620cbc7f3 Mon Sep 17 00:00:00 2001 From: Vandolf Estrellado Date: Thu, 30 Dec 2021 10:18:53 -1000 Subject: [PATCH] Closes #154 Standardize interface for CRUD APIs --- .../src/main/java/contacts/core/BroadQuery.kt | 29 ++-- core/src/main/java/contacts/core/Contacts.kt | 49 +++++-- core/src/main/java/contacts/core/CrudApi.kt | 138 ++++++++++++++++++ core/src/main/java/contacts/core/Delete.kt | 40 ++--- core/src/main/java/contacts/core/Include.kt | 5 +- core/src/main/java/contacts/core/Insert.kt | 30 ++-- core/src/main/java/contacts/core/Query.kt | 29 ++-- core/src/main/java/contacts/core/Update.kt | 62 ++++---- .../java/contacts/core/accounts/Accounts.kt | 30 ++-- .../AccountsLocalRawContactsUpdate.kt | 60 ++++---- .../contacts/core/accounts/AccountsQuery.kt | 28 ++-- .../core/accounts/AccountsRawContactsQuery.kt | 29 ++-- core/src/main/java/contacts/core/data/Data.kt | 29 ++-- .../java/contacts/core/data/DataDelete.kt | 29 ++-- .../main/java/contacts/core/data/DataQuery.kt | 32 ++-- .../java/contacts/core/data/DataUpdate.kt | 27 ++-- .../entities/custom/CustomDataRegistry.kt | 7 +- .../main/java/contacts/core/groups/Groups.kt | 31 ++-- .../java/contacts/core/groups/GroupsDelete.kt | 31 ++-- .../java/contacts/core/groups/GroupsInsert.kt | 31 ++-- .../java/contacts/core/groups/GroupsQuery.kt | 22 ++- .../java/contacts/core/groups/GroupsUpdate.kt | 31 ++-- .../java/contacts/core/profile/Profile.kt | 33 ++--- .../contacts/core/profile/ProfileDelete.kt | 30 ++-- .../contacts/core/profile/ProfileInsert.kt | 40 +++-- .../contacts/core/profile/ProfileQuery.kt | 28 ++-- .../contacts/core/profile/ProfileUpdate.kt | 29 ++-- .../java/contacts/core/util/ContactLinks.kt | 87 ++++++----- .../java/contacts/core/util/ContactOptions.kt | 4 +- .../java/contacts/core/util/ContactPhoto.kt | 29 ++-- .../contacts/core/util/DefaultContactData.kt | 4 +- .../contacts/core/util/RawContactOptions.kt | 6 +- .../contacts/core/util/RawContactPhoto.kt | 12 +- .../accounts/AccountsPermissionsRequest.kt | 12 +- .../data/DataPermissionsRequest.kt | 12 +- .../groups/GroupsPermissionsRequest.kt | 18 +-- .../profile/ProfilePermissionsRequest.kt | 18 +-- .../main/java/contacts/sample/SampleApp.kt | 10 +- .../main/java/contacts/test/TestContacts.kt | 14 +- test/src/main/java/contacts/test/TestQuery.kt | 3 +- 40 files changed, 646 insertions(+), 542 deletions(-) create mode 100644 core/src/main/java/contacts/core/CrudApi.kt diff --git a/core/src/main/java/contacts/core/BroadQuery.kt b/core/src/main/java/contacts/core/BroadQuery.kt index bfb40b84..32777d52 100644 --- a/core/src/main/java/contacts/core/BroadQuery.kt +++ b/core/src/main/java/contacts/core/BroadQuery.kt @@ -145,7 +145,7 @@ import contacts.core.util.unsafeLazy * * Matching is **case-insensitive** (case is ignored). */ -interface BroadQuery : Redactable { +interface BroadQuery : CrudApi { /** * If [includeBlanks] is set to true, then queries may include blank RawContacts. Otherwise, @@ -444,7 +444,7 @@ interface BroadQuery : Redactable { // I know that this interface also exist in Query but I want each API to have its own // interface for the results in case we need to deviate implementation. Besides, this is the // only pair of APIs in the library that have the same name for its results interface. - interface Result : List, Redactable { + interface Result : List, CrudApi.Result { // We have to cast the return type because we are not using recursive generic types. override fun redactedCopy(): Result @@ -452,21 +452,15 @@ interface BroadQuery : Redactable { } @Suppress("FunctionName") -internal fun BroadQuery(contacts: Contacts): BroadQuery = BroadQueryImpl( - contacts.applicationContext.contentResolver, - contacts.permissions, - contacts.customDataRegistry -) +internal fun BroadQuery(contacts: Contacts): BroadQuery = BroadQueryImpl(contacts) private class BroadQueryImpl( - private val contentResolver: ContentResolver, - private val permissions: ContactsPermissions, - private val customDataRegistry: CustomDataRegistry, + override val contactsApi: Contacts, private var includeBlanks: Boolean = DEFAULT_INCLUDE_BLANKS, private var rawContactsWhere: Where? = DEFAULT_RAW_CONTACTS_WHERE, private var groupMembershipWhere: Where? = DEFAULT_GROUP_MEMBERSHIP_WHERE, - private var include: Include = allDataFields(customDataRegistry), + private var include: Include = contactsApi.includeAllFields(), private var searchString: String? = DEFAULT_SEARCH_STRING, private var orderBy: CompoundOrderBy = DEFAULT_ORDER_BY, private var limit: Int = DEFAULT_LIMIT, @@ -491,10 +485,9 @@ private class BroadQueryImpl( """.trimIndent() override fun redactedCopy(): BroadQuery = BroadQueryImpl( - contentResolver, permissions, customDataRegistry, + contactsApi, includeBlanks, - // Redact Account information. rawContactsWhere?.redactedCopy(), groupMembershipWhere, @@ -539,7 +532,7 @@ private class BroadQueryImpl( override fun include(fields: Sequence): BroadQuery = apply { include = if (fields.isEmpty()) { - allDataFields(customDataRegistry) + contactsApi.includeAllFields() } else { Include(fields + REQUIRED_INCLUDE_FIELDS) } @@ -588,7 +581,8 @@ private class BroadQueryImpl( override fun find(): BroadQuery.Result = find { false } override fun find(cancel: () -> Boolean): BroadQuery.Result { - // TODO issue #144 log this + onPreExecute() + val contacts = if (!permissions.canQuery()) { emptyList() } else { @@ -599,8 +593,9 @@ private class BroadQueryImpl( ) } - return BroadQueryResult(contacts).redactedCopyOrThis(isRedacted) - // TODO issue #144 log result + return BroadQueryResult(contacts) + .redactedCopyOrThis(isRedacted) + .apply { onPostExecute(contactsApi) } } private companion object { diff --git a/core/src/main/java/contacts/core/Contacts.kt b/core/src/main/java/contacts/core/Contacts.kt index 429e3713..643c4dbb 100644 --- a/core/src/main/java/contacts/core/Contacts.kt +++ b/core/src/main/java/contacts/core/Contacts.kt @@ -1,9 +1,11 @@ package contacts.core +import android.content.ContentResolver import android.content.Context +import android.content.res.Resources import contacts.core.accounts.Accounts +import contacts.core.accounts.AccountsPermissions import contacts.core.data.Data -import contacts.core.entities.MimeType import contacts.core.entities.custom.CustomDataRegistry import contacts.core.groups.Groups import contacts.core.profile.Profile @@ -95,10 +97,16 @@ interface Contacts { /** * Returns a [ContactsPermissions] instance, which provides functions for checking required - * permissions. + * permissions for Contacts Provider operations. */ val permissions: ContactsPermissions + /** + * Returns a [AccountsPermissions] instance, which provides functions for checking required + * permissions for Account operations. + */ + val accountsPermissions: AccountsPermissions + /** * Reference to the Application's Context for use in extension functions and external library * modules. This is safe to hold on to. Not meant for consumer use. @@ -120,9 +128,15 @@ interface Contacts { val applicationContext: Context /** - * Provides functions required to support custom data, which have [MimeType.Custom]. + * Registry of custom data components, enabling queries, inserts, updates, and deletes for + * custom data. */ val customDataRegistry: CustomDataRegistry + + /** + * Registry for all [CrudApi.Listener]s. + */ + val apiListenerRegistry: CrudApiListenerRegistry } /** @@ -132,17 +146,21 @@ interface Contacts { @Suppress("FunctionName") fun Contacts( context: Context, - customDataRegistry: CustomDataRegistry = CustomDataRegistry() + customDataRegistry: CustomDataRegistry = CustomDataRegistry(), + apiListenerRegistry: CrudApiListenerRegistry = CrudApiListenerRegistry() ): Contacts = ContactsImpl( context.applicationContext, ContactsPermissions(context.applicationContext), - customDataRegistry + AccountsPermissions(context.applicationContext), + customDataRegistry, + apiListenerRegistry ) /** * Creates a new [Contacts] instance. * - * This is mainly for Java convenience. Kotlin users should use [Contacts] function instead. + * This is mainly exist for traditional Java conventions. Kotlin users should use the [Contacts] + * function instead. */ object ContactsFactory { @@ -150,14 +168,17 @@ object ContactsFactory { @JvmOverloads fun create( context: Context, - customDataRegistry: CustomDataRegistry = CustomDataRegistry() - ): Contacts = Contacts(context, customDataRegistry) + customDataRegistry: CustomDataRegistry = CustomDataRegistry(), + apiListenerRegistry: CrudApiListenerRegistry = CrudApiListenerRegistry() + ): Contacts = Contacts(context, customDataRegistry, apiListenerRegistry) } private class ContactsImpl( override val applicationContext: Context, override val permissions: ContactsPermissions, - override val customDataRegistry: CustomDataRegistry + override val accountsPermissions: AccountsPermissions, + override val customDataRegistry: CustomDataRegistry, + override val apiListenerRegistry: CrudApiListenerRegistry ) : Contacts { override fun query() = Query(this) @@ -180,3 +201,13 @@ private class ContactsImpl( override fun accounts(isProfile: Boolean) = Accounts(this, isProfile) } + +// region Shortcuts + +internal val Contacts.contentResolver: ContentResolver + get() = applicationContext.contentResolver + +internal val Contacts.resources: Resources + get() = applicationContext.resources + +// endregion \ No newline at end of file diff --git a/core/src/main/java/contacts/core/CrudApi.kt b/core/src/main/java/contacts/core/CrudApi.kt new file mode 100644 index 00000000..490ebf44 --- /dev/null +++ b/core/src/main/java/contacts/core/CrudApi.kt @@ -0,0 +1,138 @@ +package contacts.core + +import android.content.ContentResolver +import contacts.core.accounts.AccountsPermissions +import contacts.core.entities.custom.CustomDataRegistry + +/** + * All CRUD (Insert*, Query*, Update*, Delete*) APIs, accessible via a [Contacts], must implement + * this interface. + * + * ## Developer notes + * + * This should actually be an internal interface. It is public because public APIs implement this. + * + * Anyways, this enforces some level of uniformity for all CRUD APIs. However, we should try to + * keep the contract loose to allow for the most flexibility. + * + * Who knows? Consumers might find some use for this. There is no howto page for this until we can + * find an official use for it. + */ +interface CrudApi : Redactable { + + /** + * A reference to the [Contacts] instance that constructed this. This is mostly used internally + * to shorten internal code. + * + * Don't worry, [Contacts] does not keep references to instances of this. There are no circular + * references that could cause leaks =). [Contacts] is just a factory. + */ + val contactsApi: Contacts + + /** + * The API core function result. + */ + interface Result : Redactable + + /** + * Get notified about events within [CrudApi] instances. + */ + interface Listener { + /** + * Invoked by the [api] before executing its core function (e.g. "find" or "commit"). + * + * ## Thread Safety + * + * This is invoked on the same thread as the thread the core function is invoked, which is + * determined by the consumer. + */ + fun onPreExecute(api: CrudApi) + + /** + * Invoked by the API after executing its core function (e.g. "find" or "commit") right + * before returning the [result] to the caller. + * + * ## Thread Safety + * + * This is invoked on the same thread as the thread the core function is invoked, which is + * determined by the consumer. + */ + fun onPostExecute(result: Result) + } +} + +/** + * Registry for all [CrudApi.Listener]s. + * + * This single instance of [CrudApi.Listener] per [Contacts] instance that will be used by all + * [CrudApi] instances. + */ +class CrudApiListenerRegistry { + + private val listeners = mutableSetOf() + + /** + * Register a [listener] that will be notified about events on all CRUD APIs accessible via a + * [Contacts] instance. + * + * ## A short lesson about memory leaks <3 + * + * Make sure to [unregister] the [listener] to prevent leaks! You only need to do this if the + * [listener] lifecycle is less than the lifecycle of the [Contacts] instance. + * + * For example, if your application is holding one instance of [Contacts] throughout the entire + * lifecycle of your [android.app.Application] (a singleton) and the [listener] passed here is + * (or contains a reference to) an `Activity`, `Fragment`, or `View`, then you must [unregister] + * the [listener] before your Activity, Fragment, or View is destroyed. Otherwise, the + * reference to the Activity, Fragment, or View will remain in memory for as long as your + * `Application` is alive because there is a reference to it via this registry! + * + * However, if the [listener] does not have a reference to a non-Application `Context` or it + * is meant to have the same lifecycle as your [android.app.Application], then there is no need + * to [unregister]. + */ + fun register(listener: CrudApi.Listener): CrudApiListenerRegistry = apply { + listeners.add(listener) + } + + /** + * Removes the [listener] from the registry. + * + * This is important for preventing memory leaks! Read more about it in the [register] function! + */ + fun unregister(listener: CrudApi.Listener): CrudApiListenerRegistry = apply { + listeners.remove(listener) + } + + internal fun onPreExecute(api: CrudApi) { + listeners.forEach { it.onPreExecute(api) } + } + + internal fun onPostExecute(result: CrudApi.Result) { + listeners.forEach { it.onPostExecute(result) } + } +} + +// region Shortcuts + +internal fun CrudApi.onPreExecute() { + contactsApi.apiListenerRegistry.onPreExecute(this) +} + +internal fun CrudApi.Result.onPostExecute(contactsApi: Contacts) { + contactsApi.apiListenerRegistry.onPostExecute(this) +} + +internal val CrudApi.permissions: ContactsPermissions + get() = contactsApi.permissions + +internal val CrudApi.accountsPermissions: AccountsPermissions + get() = contactsApi.accountsPermissions + +internal val CrudApi.contentResolver: ContentResolver + get() = contactsApi.contentResolver + +internal val CrudApi.customDataRegistry: CustomDataRegistry + get() = contactsApi.customDataRegistry + +// endregion \ No newline at end of file diff --git a/core/src/main/java/contacts/core/Delete.kt b/core/src/main/java/contacts/core/Delete.kt index 572cd947..7790d36b 100644 --- a/core/src/main/java/contacts/core/Delete.kt +++ b/core/src/main/java/contacts/core/Delete.kt @@ -36,7 +36,7 @@ import contacts.core.util.unsafeLazy * .commit() * ``` */ -interface Delete : Redactable { +interface Delete : CrudApi { /** * Adds the given [rawContacts] to the delete queue, which will be deleted on [commit]. @@ -123,7 +123,7 @@ interface Delete : Redactable { // We have to cast the return type because we are not using recursive generic types. override fun redactedCopy(): Delete - interface Result : Redactable { + interface Result : CrudApi.Result { /** * True if all Contacts and RawContacts have successfully been deleted. False if even one @@ -156,14 +156,10 @@ interface Delete : Redactable { } @Suppress("FunctionName") -internal fun Delete(contacts: Contacts): Delete = DeleteImpl( - contacts.applicationContext.contentResolver, - contacts.permissions -) +internal fun Delete(contacts: Contacts): Delete = DeleteImpl(contacts) private class DeleteImpl( - private val contentResolver: ContentResolver, - private val permissions: ContactsPermissions, + override val contactsApi: Contacts, private val rawContactIds: MutableSet = mutableSetOf(), private val contactIds: MutableSet = mutableSetOf(), @@ -183,9 +179,10 @@ private class DeleteImpl( // There isn't really anything to redact =) override fun redactedCopy(): Delete = DeleteImpl( - contentResolver, permissions, + contactsApi, - rawContactIds, contactIds, + rawContactIds, + contactIds, isRedacted = true ) @@ -210,7 +207,8 @@ private class DeleteImpl( } override fun commit(): Delete.Result { - // TODO issue #144 log this + onPreExecute() + return if ((contactIds.isEmpty() && rawContactIds.isEmpty()) || !permissions.canUpdateDelete()) { DeleteAllResult(isSuccessful = false) } else { @@ -240,12 +238,14 @@ private class DeleteImpl( } DeleteResult(rawContactsResult, contactsResults) - }.redactedCopyOrThis(isRedacted) - // TODO issue #144 log result + } + .redactedCopyOrThis(isRedacted) + .apply { onPostExecute(contactsApi) } } override fun commitInOneTransaction(): Delete.Result { - // TODO issue #144 log this + onPreExecute() + return if ((rawContactIds.isEmpty() && contactIds.isEmpty()) || !permissions.canUpdateDelete()) { DeleteAllResult(isSuccessful = false) } else { @@ -274,15 +274,15 @@ private class DeleteImpl( DeleteAllResult(isSuccessful = contentResolver.applyBatch(operations) != null) } - }.redactedCopyOrThis(isRedacted) - // TODO issue #144 log result + } + .redactedCopyOrThis(isRedacted) + .apply { onPostExecute(contactsApi) } } } -internal fun ContentResolver.deleteRawContactWithId(rawContactId: Long): Boolean = - applyBatch( - RawContactsOperation(rawContactId.isProfileId).deleteRawContact(rawContactId) - ) != null +internal fun ContentResolver.deleteRawContactWithId(rawContactId: Long): Boolean = applyBatch( + RawContactsOperation(rawContactId.isProfileId).deleteRawContact(rawContactId) +) != null private fun ContentResolver.deleteContactWithId(contactId: Long): Boolean = applyBatch( diff --git a/core/src/main/java/contacts/core/Include.kt b/core/src/main/java/contacts/core/Include.kt index 97bc5346..f001cf1d 100644 --- a/core/src/main/java/contacts/core/Include.kt +++ b/core/src/main/java/contacts/core/Include.kt @@ -59,9 +59,8 @@ internal class Include( override fun toString(): String = columnNames.joinToString(", ") } -internal fun allDataFields( - customDataRegistry: CustomDataRegistry -): Include = Include(Fields.all + customDataRegistry.allFields()) +internal fun Contacts.includeAllFields(): Include = + Include(Fields.all + customDataRegistry.allFields()) /** * Returns a new instance of [Include] where only [ContactsFields] in [this] are included. diff --git a/core/src/main/java/contacts/core/Insert.kt b/core/src/main/java/contacts/core/Insert.kt index 7f3c6500..ad5fee09 100644 --- a/core/src/main/java/contacts/core/Insert.kt +++ b/core/src/main/java/contacts/core/Insert.kt @@ -81,7 +81,7 @@ import contacts.core.util.* * .commit(); * ``` */ -interface Insert : Redactable { +interface Insert : CrudApi { /** * If [allowBlanks] is set to true, then blank RawContacts ([NewRawContact.isBlank]) will @@ -242,7 +242,7 @@ interface Insert : Redactable { // We have to cast the return type because we are not using recursive generic types. override fun redactedCopy(): Insert - interface Result : Redactable { + interface Result : CrudApi.Result { /** * The list of IDs of successfully created RawContacts. @@ -279,10 +279,10 @@ interface Insert : Redactable { internal fun Insert(contacts: Contacts): Insert = InsertImpl(contacts) private class InsertImpl( - private val contacts: Contacts, + override val contactsApi: Contacts, private var allowBlanks: Boolean = false, - private var include: Include = allDataFields(contacts.customDataRegistry), + private var include: Include = contactsApi.includeAllFields(), private var account: Account? = null, private val rawContacts: MutableSet = mutableSetOf(), @@ -296,13 +296,13 @@ private class InsertImpl( include: $include account: $account rawContacts: $rawContacts - hasPermission: ${contacts.permissions.canInsert()} + hasPermission: ${permissions.canInsert()} isRedacted: $isRedacted } """.trimIndent() override fun redactedCopy(): Insert = InsertImpl( - contacts, + contactsApi, allowBlanks, include, @@ -328,7 +328,7 @@ private class InsertImpl( override fun include(fields: Sequence): Insert = apply { include = if (fields.isEmpty()) { - allDataFields(contacts.customDataRegistry) + contactsApi.includeAllFields() } else { Include(fields + Fields.Required.all.asSequence()) } @@ -353,12 +353,13 @@ private class InsertImpl( override fun commit(): Insert.Result = commit { false } override fun commit(cancel: () -> Boolean): Insert.Result { - // TODO issue #144 log this - return if (rawContacts.isEmpty() || !contacts.permissions.canInsert() || cancel()) { + onPreExecute() + + return if (rawContacts.isEmpty() || !permissions.canInsert() || cancel()) { InsertFailed() } else { // This ensures that a valid account is used. Otherwise, null is used. - account = account?.nullIfNotInSystem(contacts.accounts()) + account = account?.nullIfNotInSystem(contactsApi.accounts()) val results = mutableMapOf() for (rawContact in rawContacts) { @@ -371,7 +372,7 @@ private class InsertImpl( } else { // No need to propagate the cancel function to within insertRawContactForAccount // as that operation should be fast and CPU time should be trivial. - contacts.insertRawContactForAccount( + contactsApi.insertRawContactForAccount( account, include.fields, rawContact, @@ -380,8 +381,9 @@ private class InsertImpl( } } InsertResult(results) - }.redactedCopyOrThis(isRedacted) - // TODO issue #144 log result + } + .redactedCopyOrThis(isRedacted) + .apply { onPostExecute(contactsApi) } } private companion object { @@ -514,7 +516,7 @@ internal fun Contacts.insertRawContactForAccount( * Atomically create the RawContact row and all of the associated Data rows. All of the * above operations will either succeed or fail. */ - val results = applicationContext.contentResolver.applyBatch(operations) + val results = contentResolver.applyBatch(operations) /* * The ContentProviderResult[0] contains the first result of the batch, which is the diff --git a/core/src/main/java/contacts/core/Query.kt b/core/src/main/java/contacts/core/Query.kt index e0ab8b7e..035cf356 100644 --- a/core/src/main/java/contacts/core/Query.kt +++ b/core/src/main/java/contacts/core/Query.kt @@ -78,7 +78,7 @@ import contacts.core.util.unsafeLazy * Unlike [BroadQuery.groups], this does not have a groups function. You may still match groups * (in a much flexible way) by using [Fields.GroupMembership] with [where]. */ -interface Query : Redactable { +interface Query : CrudApi { /** * If [includeBlanks] is set to true, then queries may include blank RawContacts or blank @@ -407,7 +407,7 @@ interface Query : Redactable { // I know that this interface also exist in BroadQuery but I want each API to have its own // interface for the results in case we need to deviate implementation. Besides, this is the // only pair of APIs in the library that have the same name for its results interface. - interface Result : List, Redactable { + interface Result : List, CrudApi.Result { // We have to cast the return type because we are not using recursive generic types. override fun redactedCopy(): Result @@ -415,21 +415,14 @@ interface Query : Redactable { } @Suppress("FunctionName") -internal fun Query(contacts: Contacts): Query = - QueryImpl( - contacts.applicationContext.contentResolver, - contacts.permissions, - contacts.customDataRegistry - ) +internal fun Query(contacts: Contacts): Query = QueryImpl(contacts) private class QueryImpl( - private val contentResolver: ContentResolver, - private val permissions: ContactsPermissions, - private val customDataRegistry: CustomDataRegistry, + override val contactsApi: Contacts, private var includeBlanks: Boolean = DEFAULT_INCLUDE_BLANKS, private var rawContactsWhere: Where? = DEFAULT_RAW_CONTACTS_WHERE, - private var include: Include = allDataFields(customDataRegistry), + private var include: Include = contactsApi.includeAllFields(), private var where: Where? = DEFAULT_WHERE, private var orderBy: CompoundOrderBy = DEFAULT_ORDER_BY, private var limit: Int = DEFAULT_LIMIT, @@ -454,7 +447,7 @@ private class QueryImpl( """.trimIndent() override fun redactedCopy(): Query = QueryImpl( - contentResolver, permissions, customDataRegistry, + contactsApi, includeBlanks, // Redact Account information. @@ -487,7 +480,7 @@ private class QueryImpl( override fun include(fields: Sequence): Query = apply { include = if (fields.isEmpty()) { - allDataFields(customDataRegistry) + contactsApi.includeAllFields() } else { Include(fields + REQUIRED_INCLUDE_FIELDS) } @@ -538,7 +531,8 @@ private class QueryImpl( override fun find(): Query.Result = find { false } override fun find(cancel: () -> Boolean): Query.Result { - // TODO issue #144 log this + onPreExecute() + val contacts = if (!permissions.canQuery() || cancel()) { emptyList() } else { @@ -555,8 +549,9 @@ private class QueryImpl( ) } - return QueryResult(contacts).redactedCopyOrThis(isRedacted) - // TODO issue #144 log result + return QueryResult(contacts) + .redactedCopyOrThis(isRedacted) + .apply { onPostExecute(contactsApi) } } private companion object { diff --git a/core/src/main/java/contacts/core/Update.kt b/core/src/main/java/contacts/core/Update.kt index e14464e4..08174e80 100644 --- a/core/src/main/java/contacts/core/Update.kt +++ b/core/src/main/java/contacts/core/Update.kt @@ -3,7 +3,8 @@ package contacts.core import android.content.ContentProviderOperation import android.content.ContentResolver import contacts.core.accounts.accountForRawContactWithId -import contacts.core.entities.* +import contacts.core.entities.ExistingContactEntity +import contacts.core.entities.ExistingRawContactEntity import contacts.core.entities.custom.CustomDataCountRestriction import contacts.core.entities.custom.CustomDataRegistry import contacts.core.entities.operation.* @@ -80,7 +81,7 @@ import contacts.core.util.unsafeLazy * .commit(); * ``` */ -interface Update : Redactable { +interface Update : CrudApi { /** * If [deleteBlanks] is set to true, then updating blank RawContacts @@ -244,7 +245,7 @@ interface Update : Redactable { // We have to cast the return type because we are not using recursive generic types. override fun redactedCopy(): Update - interface Result : Redactable { + interface Result : CrudApi.Result { /** * True if all Contacts and RawContacts have successfully been updated. False if even one @@ -279,10 +280,10 @@ interface Update : Redactable { internal fun Update(contacts: Contacts): Update = UpdateImpl(contacts) private class UpdateImpl( - private val contacts: Contacts, + override val contactsApi: Contacts, private var deleteBlanks: Boolean = true, - private var include: Include = allDataFields(contacts.customDataRegistry), + private var include: Include = contactsApi.includeAllFields(), private val rawContacts: MutableSet = mutableSetOf(), override val isRedacted: Boolean = false @@ -294,13 +295,13 @@ private class UpdateImpl( deleteBlanks: $deleteBlanks include: $include rawContacts: $rawContacts - hasPermission: ${contacts.permissions.canUpdateDelete()} + hasPermission: ${permissions.canUpdateDelete()} isRedacted: $isRedacted } """.trimIndent() override fun redactedCopy(): Update = UpdateImpl( - contacts, + contactsApi, deleteBlanks, include, @@ -320,7 +321,7 @@ private class UpdateImpl( override fun include(fields: Sequence): Update = apply { include = if (fields.isEmpty()) { - allDataFields(contacts.customDataRegistry) + contactsApi.includeAllFields() } else { Include(fields + Fields.Required.all.asSequence()) } @@ -351,8 +352,9 @@ private class UpdateImpl( override fun commit(): Update.Result = commit { false } override fun commit(cancel: () -> Boolean): Update.Result { - // TODO issue #144 log this - return if (rawContacts.isEmpty() || !contacts.permissions.canUpdateDelete() || cancel()) { + onPreExecute() + + return if (rawContacts.isEmpty() || !permissions.canUpdateDelete() || cancel()) { UpdateFailed() } else { val results = mutableMapOf() @@ -367,15 +369,15 @@ private class UpdateImpl( // enforce API design. false } else if (rawContact.isBlank && deleteBlanks) { - contacts.applicationContext.contentResolver - .deleteRawContactWithId(rawContact.id) + contentResolver.deleteRawContactWithId(rawContact.id) } else { - contacts.updateRawContact(include.fields, rawContact) + contactsApi.updateRawContact(include.fields, rawContact) } } UpdateResult(results) - }.redactedCopyOrThis(isRedacted) - // TODO issue #144 log result + } + .redactedCopyOrThis(isRedacted) + .apply { onPostExecute(contactsApi) } } } @@ -393,20 +395,20 @@ internal fun Contacts.updateRawContact( rawContact: ExistingRawContactEntity ): Boolean { val isProfile = rawContact.isProfile - val account = applicationContext.contentResolver.accountForRawContactWithId(rawContact.id) + val account = contentResolver.accountForRawContactWithId(rawContact.id) val hasAccount = account != null val operations = arrayListOf() operations.addAll( AddressOperation(isProfile, Fields.Address.intersect(includeFields)).updateInsertOrDelete( - rawContact.addresses, rawContact.id, applicationContext.contentResolver + rawContact.addresses, rawContact.id, contentResolver ) ) operations.addAll( EmailOperation(isProfile, Fields.Email.intersect(includeFields)).updateInsertOrDelete( - rawContact.emails, rawContact.id, applicationContext.contentResolver + rawContact.emails, rawContact.id, contentResolver ) ) @@ -416,7 +418,7 @@ internal fun Contacts.updateRawContact( // follow in the footsteps of the native Contacts app... operations.addAll( EventOperation(isProfile, Fields.Event.intersect(includeFields)).updateInsertOrDelete( - rawContact.events, rawContact.id, applicationContext.contentResolver + rawContact.events, rawContact.id, contentResolver ) ) } @@ -438,30 +440,30 @@ internal fun Contacts.updateRawContact( operations.addAll( ImOperation(isProfile, Fields.Im.intersect(includeFields)).updateInsertOrDelete( - rawContact.ims, rawContact.id, applicationContext.contentResolver + rawContact.ims, rawContact.id, contentResolver ) ) NameOperation(isProfile, Fields.Name.intersect(includeFields)).updateInsertOrDelete( - rawContact.name, rawContact.id, applicationContext.contentResolver + rawContact.name, rawContact.id, contentResolver )?.let(operations::add) NicknameOperation(isProfile, Fields.Nickname.intersect(includeFields)).updateInsertOrDelete( - rawContact.nickname, rawContact.id, applicationContext.contentResolver + rawContact.nickname, rawContact.id, contentResolver )?.let(operations::add) NoteOperation(isProfile, Fields.Note.intersect(includeFields)).updateInsertOrDelete( - rawContact.note, rawContact.id, applicationContext.contentResolver + rawContact.note, rawContact.id, contentResolver )?.let(operations::add) OrganizationOperation(isProfile, Fields.Organization.intersect(includeFields)) .updateInsertOrDelete( - rawContact.organization, rawContact.id, applicationContext.contentResolver + rawContact.organization, rawContact.id, contentResolver )?.let(operations::add) operations.addAll( PhoneOperation(isProfile, Fields.Phone.intersect(includeFields)).updateInsertOrDelete( - rawContact.phones, rawContact.id, applicationContext.contentResolver + rawContact.phones, rawContact.id, contentResolver ) ) @@ -475,26 +477,26 @@ internal fun Contacts.updateRawContact( operations.addAll( RelationOperation(isProfile, Fields.Relation.intersect(includeFields)) .updateInsertOrDelete( - rawContact.relations, rawContact.id, applicationContext.contentResolver + rawContact.relations, rawContact.id, contentResolver ) ) } SipAddressOperation(isProfile, Fields.SipAddress.intersect(includeFields)) .updateInsertOrDelete( - rawContact.sipAddress, rawContact.id, applicationContext.contentResolver + rawContact.sipAddress, rawContact.id, contentResolver )?.let(operations::add) operations.addAll( WebsiteOperation(isProfile, Fields.Website.intersect(includeFields)).updateInsertOrDelete( - rawContact.websites, rawContact.id, applicationContext.contentResolver + rawContact.websites, rawContact.id, contentResolver ) ) // Process custom data operations.addAll( rawContact.customDataUpdateInsertOrDeleteOperations( - applicationContext.contentResolver, includeFields, customDataRegistry + contentResolver, includeFields, customDataRegistry ) ) @@ -502,7 +504,7 @@ internal fun Contacts.updateRawContact( * Atomically update all of the associated Data rows. All of the above operations will * either succeed or fail. */ - return applicationContext.contentResolver.applyBatch(operations) != null + return contentResolver.applyBatch(operations) != null } private fun ExistingRawContactEntity.customDataUpdateInsertOrDeleteOperations( diff --git a/core/src/main/java/contacts/core/accounts/Accounts.kt b/core/src/main/java/contacts/core/accounts/Accounts.kt index aae2d8d1..fa3dc2cc 100644 --- a/core/src/main/java/contacts/core/accounts/Accounts.kt +++ b/core/src/main/java/contacts/core/accounts/Accounts.kt @@ -1,6 +1,5 @@ package contacts.core.accounts -import android.content.Context import contacts.core.Contacts /** @@ -45,16 +44,13 @@ interface Accounts { fun profile(): Accounts /** - * Returns a [AccountsPermissions] instance, which provides functions for checking required - * permissions. + * A reference to the [Contacts] instance that constructed this. This is mostly used internally + * to shorten internal code. + * + * Don't worry, [Contacts] does not keep references to instances of this. There are no circular + * references that could cause leaks =). [Contacts] is just a factory. */ - val permissions: AccountsPermissions - - /** - * Reference to the Application's Context for use in extension functions and external library - * modules. This is safe to hold on to. Not meant for consumer use. - */ - val applicationContext: Context + val contactsApi: Contacts } /** @@ -62,24 +58,22 @@ interface Accounts { */ @Suppress("FunctionName") internal fun Accounts(contacts: Contacts, isProfile: Boolean): Accounts = AccountsImpl( - contacts.applicationContext, - AccountsPermissions(contacts.applicationContext), + contacts, isProfile ) @SuppressWarnings("MissingPermission") private class AccountsImpl( - override val applicationContext: Context, - override val permissions: AccountsPermissions, + override val contactsApi: Contacts, private val isProfile: Boolean ) : Accounts { - override fun query() = AccountsQuery(this, isProfile) + override fun query() = AccountsQuery(contactsApi, isProfile) - override fun queryRawContacts() = AccountsRawContactsQuery(this, isProfile) + override fun queryRawContacts() = AccountsRawContactsQuery(contactsApi, isProfile) override fun updateLocalRawContactsAccount() = - AccountsLocalRawContactsUpdate(this, isProfile) + AccountsLocalRawContactsUpdate(contactsApi, isProfile) - override fun profile(): Accounts = AccountsImpl(applicationContext, permissions, true) + override fun profile(): Accounts = AccountsImpl(contactsApi, true) } \ No newline at end of file diff --git a/core/src/main/java/contacts/core/accounts/AccountsLocalRawContactsUpdate.kt b/core/src/main/java/contacts/core/accounts/AccountsLocalRawContactsUpdate.kt index 5e579609..915a78d7 100644 --- a/core/src/main/java/contacts/core/accounts/AccountsLocalRawContactsUpdate.kt +++ b/core/src/main/java/contacts/core/accounts/AccountsLocalRawContactsUpdate.kt @@ -4,6 +4,7 @@ import android.accounts.Account import android.content.ContentProviderOperation import android.content.ContentProviderOperation.newDelete import android.content.ContentProviderOperation.newUpdate +import android.content.ContentResolver import contacts.core.* import contacts.core.accounts.AccountsLocalRawContactsUpdate.Result.FailureReason import contacts.core.entities.ExistingRawContactEntity @@ -50,7 +51,7 @@ import contacts.core.util.* * .commit() * ``` */ -interface AccountsLocalRawContactsUpdate : Redactable { +interface AccountsLocalRawContactsUpdate : CrudApi { /** * The [Account] that will be associated with the existing local RawContacts specified in @@ -161,7 +162,7 @@ interface AccountsLocalRawContactsUpdate : Redactable { // We have to cast the return type because we are not using recursive generic types. override fun redactedCopy(): AccountsLocalRawContactsUpdate - interface Result : Redactable { + interface Result : CrudApi.Result { /** * True if the RawContacts have been successfully associated to the given Account. @@ -208,13 +209,13 @@ interface AccountsLocalRawContactsUpdate : Redactable { } @Suppress("FunctionName") -internal fun AccountsLocalRawContactsUpdate(accounts: Accounts, isProfile: Boolean): +internal fun AccountsLocalRawContactsUpdate(contacts: Contacts, isProfile: Boolean): AccountsLocalRawContactsUpdate = AccountsLocalRawContactsUpdateImpl( - accounts, isProfile + contacts, isProfile ) private class AccountsLocalRawContactsUpdateImpl( - private val accounts: Accounts, + override val contactsApi: Contacts, private val isProfile: Boolean, private var account: Account? = null, @@ -229,14 +230,14 @@ private class AccountsLocalRawContactsUpdateImpl( isProfile: $isProfile account: $account rawContactIds: $rawContactIds - hasPermission: ${accounts.permissions.canUpdateLocalRawContactsAccount()} + hasPermission: ${accountsPermissions.canUpdateLocalRawContactsAccount()} isRedacted: $isRedacted } """.trimIndent() override fun redactedCopy(): AccountsLocalRawContactsUpdate = AccountsLocalRawContactsUpdateImpl( - accounts, isProfile, + contactsApi, isProfile, // Redact account info account?.redactedCopy(), @@ -264,15 +265,16 @@ private class AccountsLocalRawContactsUpdateImpl( override fun commit() = commit { false } override fun commit(cancel: () -> Boolean): AccountsLocalRawContactsUpdate.Result { - // TODO issue #144 log this + onPreExecute() + val account = account return if ( rawContactIds.isEmpty() - || !accounts.permissions.canUpdateLocalRawContactsAccount() + || !accountsPermissions.canUpdateLocalRawContactsAccount() || cancel() ) { AccountsRawContactsAssociationsUpdateResultFailed(FailureReason.UNKNOWN) - } else if (account?.isInSystem(accounts) != true) { + } else if (account?.isInSystem(contactsApi.accounts()) != true) { // Either account was not specified (null) or it is not in system. AccountsRawContactsAssociationsUpdateResultFailed(FailureReason.INVALID_ACCOUNT) } else { @@ -280,16 +282,17 @@ private class AccountsLocalRawContactsUpdateImpl( for (rawContactId in rawContactIds) { if (cancel() || rawContactId.isProfileId != isProfile) { failureReasons[rawContactId] = FailureReason.UNKNOWN - } else if (accounts.rawContactHasAccount(rawContactId)) { + } else if (contentResolver.rawContactHasAccount(rawContactId)) { failureReasons[rawContactId] = FailureReason.RAW_CONTACT_IS_NOT_LOCAL - } else if (!accounts.setRawContactAccount(account, rawContactId)) { + } else if (!contentResolver.setRawContactAccount(account, rawContactId)) { failureReasons[rawContactId] = FailureReason.UNKNOWN } // else operation succeeded. No Failure reason. } AccountsRawContactsAssociationsUpdateResult(failureReasons) - }.redactedCopyOrThis(isRedacted) - // TODO issue #144 log result + } + .redactedCopyOrThis(isRedacted) + .apply { onPostExecute(contactsApi) } } } @@ -302,9 +305,9 @@ private class AccountsLocalRawContactsUpdateImpl( * Note that local RawContacts may have a group membership to an Account that it is not associated * with. Therefore, we need to delete that membership. */ -private fun Accounts.setRawContactAccount( +private fun ContentResolver.setRawContactAccount( account: Account, rawContactId: Long -): Boolean = applicationContext.contentResolver.applyBatch( +): Boolean = applyBatch( arrayListOf().apply { // First delete existing group memberships. newDelete(if (rawContactId.isProfileId) ProfileUris.DATA.uri else Table.Data.uri) @@ -325,19 +328,18 @@ private fun Accounts.setRawContactAccount( } ) != null -private fun Accounts.rawContactHasAccount(rawContactId: Long): Boolean = - applicationContext.contentResolver.query( - if (rawContactId.isProfileId) ProfileUris.RAW_CONTACTS.uri else Table.RawContacts.uri, - Include(RawContactsFields.Id), - RawContactsFields.run { - Id.equalTo(rawContactId) and - AccountName.isNotNullOrEmpty() and - AccountType.isNotNullOrEmpty() - } - ) { - val matchingRawContactId: Long? = it.getNextOrNull { it.rawContactsCursor().rawContactId } - matchingRawContactId == rawContactId - } ?: false +private fun ContentResolver.rawContactHasAccount(rawContactId: Long): Boolean = query( + if (rawContactId.isProfileId) ProfileUris.RAW_CONTACTS.uri else Table.RawContacts.uri, + Include(RawContactsFields.Id), + RawContactsFields.run { + Id.equalTo(rawContactId) and + AccountName.isNotNullOrEmpty() and + AccountType.isNotNullOrEmpty() + } +) { + val matchingRawContactId: Long? = it.getNextOrNull { it.rawContactsCursor().rawContactId } + matchingRawContactId == rawContactId +} ?: false private class AccountsRawContactsAssociationsUpdateResult private constructor( diff --git a/core/src/main/java/contacts/core/accounts/AccountsQuery.kt b/core/src/main/java/contacts/core/accounts/AccountsQuery.kt index 52cfb5f1..9d96a827 100644 --- a/core/src/main/java/contacts/core/accounts/AccountsQuery.kt +++ b/core/src/main/java/contacts/core/accounts/AccountsQuery.kt @@ -44,7 +44,7 @@ import contacts.core.util.* * query function of Accounts need not be as extensive (or at all) as other Queries. Where, orderBy, * offset, and limit functions are left to consumers to implement if they wish. */ -interface AccountsQuery : Redactable { +interface AccountsQuery : CrudApi { /** * Limits the search to Accounts that have one of the given [accountTypes]. @@ -149,7 +149,7 @@ interface AccountsQuery : Redactable { * * Use [accountFor] to retrieve the Account for the specified RawContact. */ - interface Result : List, Redactable { + interface Result : List, CrudApi.Result { /** * The [Account] retrieved for the [rawContact]. Null if no Account or retrieval failed. @@ -178,19 +178,17 @@ interface AccountsQuery : Redactable { } @Suppress("FunctionName") -internal fun AccountsQuery(accounts: Accounts, isProfile: Boolean): AccountsQuery = +internal fun AccountsQuery(contacts: Contacts, isProfile: Boolean): AccountsQuery = AccountsQueryImpl( - accounts.applicationContext.contentResolver, - AccountManager.get(accounts.applicationContext), - accounts.permissions, + contacts, + AccountManager.get(contacts.applicationContext), isProfile ) @SuppressWarnings("MissingPermission") private class AccountsQueryImpl( - private val contentResolver: ContentResolver, + override val contactsApi: Contacts, private val accountManager: AccountManager, - private val permissions: AccountsPermissions, private val isProfile: Boolean, private val accountTypes: MutableSet = mutableSetOf(), @@ -205,13 +203,13 @@ private class AccountsQueryImpl( isProfile: $isProfile accountType: $accountTypes rawContactIds: $rawContactIds - hasPermission: ${permissions.canQueryAccounts()} + hasPermission: ${accountsPermissions.canQueryAccounts()} isRedacted: $isRedacted } """.trimIndent() override fun redactedCopy(): AccountsQuery = AccountsQueryImpl( - contentResolver, accountManager, permissions, isProfile, + contactsApi, accountManager, isProfile, accountTypes = accountTypes.redactStrings().toMutableSet(), rawContactIds = rawContactIds, @@ -241,14 +239,14 @@ private class AccountsQueryImpl( override fun find(): AccountsQuery.Result = find { false } override fun find(cancel: () -> Boolean): AccountsQuery.Result { - // TODO issue #144 log this + onPreExecute() // We start off with the full set of accounts in the system (which is typically not // more than a handful). Then we'll trim the fat as we process the query parameters. var accounts: Set = accountManager.accounts.toSet() return if ( cancel() - || !permissions.canQueryAccounts() + || !accountsPermissions.canQueryAccounts() // If the isProfile parameter does not match for all RawContacts, fail immediately. || rawContactIds.allAreProfileIds != isProfile // No accounts in the system. No point in processing the rest of the query. @@ -282,9 +280,9 @@ private class AccountsQueryImpl( } AccountsQueryResult(accounts, rawContactIdsAccountsMap) - - }.redactedCopyOrThis(isRedacted) - // TODO issue #144 log result + } + .redactedCopyOrThis(isRedacted) + .apply { onPostExecute(contactsApi) } } } diff --git a/core/src/main/java/contacts/core/accounts/AccountsRawContactsQuery.kt b/core/src/main/java/contacts/core/accounts/AccountsRawContactsQuery.kt index a279d840..fd149a0c 100644 --- a/core/src/main/java/contacts/core/accounts/AccountsRawContactsQuery.kt +++ b/core/src/main/java/contacts/core/accounts/AccountsRawContactsQuery.kt @@ -39,7 +39,7 @@ import contacts.core.util.* * .find() * ``` */ -interface AccountsRawContactsQuery : Redactable { +interface AccountsRawContactsQuery : CrudApi { /** * Limits the [BlankRawContact]s returned by this query to those belonging to one of the given @@ -192,7 +192,7 @@ interface AccountsRawContactsQuery : Redactable { * * You may print individual RawContacts in this list by iterating through it. */ - interface Result : List, Redactable { + interface Result : List, CrudApi.Result { /** * The list of [BlankRawContact]s from the specified [account] ordered by [orderBy]. @@ -208,16 +208,11 @@ interface AccountsRawContactsQuery : Redactable { @Suppress("FunctionName") internal fun AccountsRawContactsQuery( - accounts: Accounts, isProfile: Boolean -): AccountsRawContactsQuery = AccountsRawContactsQueryImpl( - accounts.applicationContext.contentResolver, - accounts.permissions, - isProfile -) + contacts: Contacts, isProfile: Boolean +): AccountsRawContactsQuery = AccountsRawContactsQueryImpl(contacts, isProfile) private class AccountsRawContactsQueryImpl( - private val contentResolver: ContentResolver, - private val permissions: AccountsPermissions, + override val contactsApi: Contacts, private val isProfile: Boolean, private var rawContactsWhere: Where? = DEFAULT_RAW_CONTACTS_WHERE, @@ -238,13 +233,13 @@ private class AccountsRawContactsQueryImpl( orderBy: $orderBy limit: $limit offset: $offset - hasPermission: ${permissions.canQueryRawContacts()} + hasPermission: ${accountsPermissions.canQueryRawContacts()} isRedacted: $isRedacted } """.trimIndent() override fun redactedCopy(): AccountsRawContactsQuery = AccountsRawContactsQueryImpl( - contentResolver, permissions, isProfile, + contactsApi, isProfile, // Redact account info. rawContactsWhere?.redactedCopy(), @@ -309,15 +304,17 @@ private class AccountsRawContactsQueryImpl( override fun find(): AccountsRawContactsQuery.Result = find { false } override fun find(cancel: () -> Boolean): AccountsRawContactsQuery.Result { - // TODO issue #144 log this - return if (!permissions.canQueryRawContacts()) { + onPreExecute() + + return if (!accountsPermissions.canQueryRawContacts()) { AccountsRawContactsQueryResult(emptyList(), emptyMap()) } else { contentResolver.resolve( isProfile, rawContactsWhere, INCLUDE, where, orderBy, limit, offset, cancel ) - }.redactedCopyOrThis(isRedacted) - // TODO issue #144 log result + } + .redactedCopyOrThis(isRedacted) + .apply { onPostExecute(contactsApi) } } private companion object { diff --git a/core/src/main/java/contacts/core/data/Data.kt b/core/src/main/java/contacts/core/data/Data.kt index 45025503..e87aa8d1 100644 --- a/core/src/main/java/contacts/core/data/Data.kt +++ b/core/src/main/java/contacts/core/data/Data.kt @@ -1,8 +1,6 @@ package contacts.core.data -import android.content.Context import contacts.core.Contacts -import contacts.core.ContactsPermissions /** * Provides new [DataQueryFactory], [DataUpdate], and [DataDelete] for Profile OR non-Profile (depending on @@ -40,33 +38,26 @@ interface Data { fun delete(): DataDelete /** - * Returns a [ContactsPermissions] instance, which provides functions for checking required - * permissions. + * A reference to the [Contacts] instance that constructed this. This is mostly used internally + * to shorten internal code. + * + * Don't worry, [Contacts] does not keep references to instances of this. There are no circular + * references that could cause leaks =). [Contacts] is just a factory. */ - val permissions: ContactsPermissions - - /** - * Reference to the Application's Context for use in extension functions and external library - * modules. This is safe to hold on to. Not meant for consumer use. - */ - val applicationContext: Context + val contactsApi: Contacts } @Suppress("FunctionName") internal fun Data(contacts: Contacts, isProfile: Boolean): Data = DataImpl(contacts, isProfile) private class DataImpl( - private val contacts: Contacts, + override val contactsApi: Contacts, private val isProfile: Boolean ) : Data { - override fun query() = DataQuery(contacts, isProfile) - - override fun update() = DataUpdate(contacts, isProfile) - - override fun delete() = DataDelete(contacts, isProfile) + override fun query() = DataQuery(contactsApi, isProfile) - override val permissions: ContactsPermissions = contacts.permissions + override fun update() = DataUpdate(contactsApi, isProfile) - override val applicationContext: Context = contacts.applicationContext + override fun delete() = DataDelete(contactsApi, isProfile) } \ No newline at end of file diff --git a/core/src/main/java/contacts/core/data/DataDelete.kt b/core/src/main/java/contacts/core/data/DataDelete.kt index 91f9011f..8ad96aef 100644 --- a/core/src/main/java/contacts/core/data/DataDelete.kt +++ b/core/src/main/java/contacts/core/data/DataDelete.kt @@ -44,7 +44,7 @@ import contacts.core.util.unsafeLazy * .commit() * ``` */ -interface DataDelete : Redactable { +interface DataDelete : CrudApi { /** * Adds the given [data] to the delete queue, which will be deleted on [commit]. @@ -103,7 +103,7 @@ interface DataDelete : Redactable { // We have to cast the return type because we are not using recursive generic types. override fun redactedCopy(): DataDelete - interface Result : Redactable { + interface Result : CrudApi.Result { /** * True if all data have successfully been deleted. False if even one delete failed. @@ -122,14 +122,11 @@ interface DataDelete : Redactable { @Suppress("FunctionName") internal fun DataDelete(contacts: Contacts, isProfile: Boolean): DataDelete = DataDeleteImpl( - contacts.applicationContext.contentResolver, - contacts.permissions, - isProfile + contacts, isProfile ) private class DataDeleteImpl( - private val contentResolver: ContentResolver, - private val permissions: ContactsPermissions, + override val contactsApi: Contacts, private val isProfile: Boolean, private val dataIds: MutableSet = mutableSetOf(), @@ -148,7 +145,7 @@ private class DataDeleteImpl( """.trimIndent() override fun redactedCopy(): DataDelete = DataDeleteImpl( - contentResolver, permissions, isProfile, + contactsApi, isProfile, dataIds, isRedacted = true ) @@ -162,7 +159,8 @@ private class DataDeleteImpl( } override fun commit(): DataDelete.Result { - // TODO issue #144 log this + onPreExecute() + return if (dataIds.isEmpty() || !permissions.canUpdateDelete()) { DataDeleteResult(emptyMap()) } else { @@ -180,12 +178,14 @@ private class DataDeleteImpl( } DataDeleteResult(dataIdsResultMap) - }.redactedCopyOrThis(isRedacted) - // TODO issue #144 log result + } + .redactedCopyOrThis(isRedacted) + .apply { onPostExecute(contactsApi) } } override fun commitInOneTransaction(): DataDelete.Result { - // TODO issue #144 log this + onPreExecute() + // I know this if-else can be folded. But this is way more readable IMO =) val isSuccessful = if (dataIds.isEmpty() || !permissions.canUpdateDelete()) { false @@ -201,8 +201,9 @@ private class DataDeleteImpl( } } - return DataDeleteAllResult(isSuccessful).redactedCopyOrThis(isRedacted) - // TODO issue #144 log result + return DataDeleteAllResult(isSuccessful) + .redactedCopyOrThis(isRedacted) + .apply { onPostExecute(contactsApi) } } } diff --git a/core/src/main/java/contacts/core/data/DataQuery.kt b/core/src/main/java/contacts/core/data/DataQuery.kt index 1265d29f..06126eb9 100644 --- a/core/src/main/java/contacts/core/data/DataQuery.kt +++ b/core/src/main/java/contacts/core/data/DataQuery.kt @@ -95,9 +95,7 @@ interface DataQueryFactory { @Suppress("FunctionName") internal fun DataQuery(contacts: Contacts, isProfile: Boolean): DataQueryFactory = - DataQueryFactoryImpl( - contacts, isProfile - ) + DataQueryFactoryImpl(contacts, isProfile) private class DataQueryFactoryImpl( private val contacts: Contacts, @@ -218,7 +216,7 @@ private class DataQueryFactoryImpl( * ``` */ interface DataQuery, E : ExistingDataEntity> : - Redactable { + CrudApi { /** * Limits this query to only search for data associated with one of the given [accounts]. @@ -425,7 +423,7 @@ interface DataQuery, E : ExistingData * * You may print individual data in this list by iterating through it. */ - interface Result : List, Redactable { + interface Result : List, CrudApi.Result { // We have to cast the return type because we are not using recursive generic types. override fun redactedCopy(): Result @@ -433,7 +431,7 @@ interface DataQuery, E : ExistingData } private class DataQueryImpl, E : ExistingDataEntity>( - private val contacts: Contacts, + override val contactsApi: Contacts, private val allFields: S, private val mimeType: MimeType, @@ -464,13 +462,13 @@ private class DataQueryImpl, E : Exis orderBy: $orderBy limit: $limit offset: $offset - hasPermission: ${contacts.permissions.canQuery()} + hasPermission: ${permissions.canQuery()} isRedacted: $isRedacted } """.trimIndent() override fun redactedCopy(): DataQuery = DataQueryImpl( - contacts, allFields, mimeType, isProfile, + contactsApi, allFields, mimeType, isProfile, // Redact account info. rawContactsWhere?.redactedCopy(), @@ -548,11 +546,12 @@ private class DataQueryImpl, E : Exis override fun find(): DataQuery.Result = find { false } override fun find(cancel: () -> Boolean): DataQuery.Result { - // TODO issue #144 log this - val data: List = if (!contacts.permissions.canQuery()) { + onPreExecute() + + val data: List = if (!permissions.canQuery()) { emptyList() } else { - contacts.resolveDataEntity( + contactsApi.resolveDataEntity( isProfile, mimeType, rawContactsWhere, include, where, orderBy, limit, offset, @@ -560,8 +559,9 @@ private class DataQueryImpl, E : Exis ) } - return DataQueryResult(data).redactedCopyOrThis(isRedacted) - // TODO issue #144 log result + return DataQueryResult(data) + .redactedCopyOrThis(isRedacted) + .apply { onPostExecute(contactsApi) } } private companion object { @@ -592,8 +592,8 @@ internal fun Contacts.resolveDataEntity( if (rawContactsWhere != null) { // Limit the data to the set associated with the RawContacts found in the RawContacts // table matching the rawContactsWhere. - val rawContactIds = applicationContext.contentResolver - .findRawContactIdsInRawContactsTable(isProfile, rawContactsWhere, cancel) + val rawContactIds = + contentResolver.findRawContactIdsInRawContactsTable(isProfile, rawContactsWhere, cancel) dataWhere = dataWhere and (Fields.RawContact.Id `in` rawContactIds) } @@ -602,7 +602,7 @@ internal fun Contacts.resolveDataEntity( dataWhere = dataWhere and where } - return applicationContext.contentResolver.query( + return contentResolver.query( // mimeType.contentUri(), if (isProfile) ProfileUris.DATA.uri else Table.Data.uri, include, dataWhere, "$orderBy LIMIT $limit OFFSET $offset" diff --git a/core/src/main/java/contacts/core/data/DataUpdate.kt b/core/src/main/java/contacts/core/data/DataUpdate.kt index 8b42b0a8..ce7f01e9 100644 --- a/core/src/main/java/contacts/core/data/DataUpdate.kt +++ b/core/src/main/java/contacts/core/data/DataUpdate.kt @@ -50,7 +50,7 @@ import contacts.core.util.unsafeLazy * .commit() * ``` */ -interface DataUpdate : Redactable { +interface DataUpdate : CrudApi { /** * Specifies that only the given set of [fields] (data) will be updated. @@ -178,7 +178,7 @@ interface DataUpdate : Redactable { // We have to cast the return type because we are not using recursive generic types. override fun redactedCopy(): DataUpdate - interface Result : Redactable { + interface Result : CrudApi.Result { /** * True if all data have successfully been updated. False if even one update failed. */ @@ -196,19 +196,14 @@ interface DataUpdate : Redactable { @Suppress("FunctionName") internal fun DataUpdate(contacts: Contacts, isProfile: Boolean): DataUpdate = DataUpdateImpl( - contacts.applicationContext.contentResolver, - contacts.permissions, - contacts.customDataRegistry, - isProfile + contacts, isProfile ) private class DataUpdateImpl( - private val contentResolver: ContentResolver, - private val permissions: ContactsPermissions, - private val customDataRegistry: CustomDataRegistry, + override val contactsApi: Contacts, private val isProfile: Boolean, - private var include: Include = allDataFields(customDataRegistry), + private var include: Include = contactsApi.includeAllFields(), private val data: MutableSet = mutableSetOf(), override val isRedacted: Boolean = false @@ -226,7 +221,7 @@ private class DataUpdateImpl( """.trimIndent() override fun redactedCopy(): DataUpdate = DataUpdateImpl( - contentResolver, permissions, customDataRegistry, isProfile, + contactsApi, isProfile, include, // Redact contact data. @@ -241,7 +236,7 @@ private class DataUpdateImpl( override fun include(fields: Sequence): DataUpdate = apply { include = if (fields.isEmpty()) { - allDataFields(customDataRegistry) + contactsApi.includeAllFields() } else { Include(fields + Fields.Required.all.asSequence()) } @@ -260,7 +255,8 @@ private class DataUpdateImpl( override fun commit(): DataUpdate.Result = commit { false } override fun commit(cancel: () -> Boolean): DataUpdate.Result { - // TODO issue #144 log this + onPreExecute() + return if (data.isEmpty() || !permissions.canUpdateDelete() || cancel()) { DataUpdateFailed() } else { @@ -280,8 +276,9 @@ private class DataUpdateImpl( } } DataUpdateResult(results) - }.redactedCopyOrThis(isRedacted) - // TODO issue #144 log result + } + .redactedCopyOrThis(isRedacted) + .apply { onPostExecute(contactsApi) } } } diff --git a/core/src/main/java/contacts/core/entities/custom/CustomDataRegistry.kt b/core/src/main/java/contacts/core/entities/custom/CustomDataRegistry.kt index e77b0c03..75a3cf37 100644 --- a/core/src/main/java/contacts/core/entities/custom/CustomDataRegistry.kt +++ b/core/src/main/java/contacts/core/entities/custom/CustomDataRegistry.kt @@ -2,11 +2,14 @@ package contacts.core.entities.custom import contacts.core.AbstractCustomDataField import contacts.core.AbstractCustomDataFieldSet +import contacts.core.Contacts import contacts.core.entities.* /** * Registry of custom data components, enabling queries, inserts, updates, and deletes for custom * data. + * + * There should only be a single instance of [CustomDataRegistry] per [Contacts] instance. */ class CustomDataRegistry { @@ -23,7 +26,7 @@ class CustomDataRegistry { * Register custom data [entries]. */ @SafeVarargs - fun register(vararg entries: Entry<*, *, *, *>) { + fun register(vararg entries: Entry<*, *, *, *>): CustomDataRegistry = apply { for (entry in entries) { @Suppress("UNCHECKED_CAST") entryMap[entry.mimeType.value] = entry as Entry< @@ -37,7 +40,7 @@ class CustomDataRegistry { /** * Register custom data entries via the given [registrations]. */ - fun register(vararg registrations: EntryRegistration) { + fun register(vararg registrations: EntryRegistration): CustomDataRegistry = apply { for (registration in registrations) { registration.registerTo(this) } diff --git a/core/src/main/java/contacts/core/groups/Groups.kt b/core/src/main/java/contacts/core/groups/Groups.kt index c8f78c1c..5e50dca1 100644 --- a/core/src/main/java/contacts/core/groups/Groups.kt +++ b/core/src/main/java/contacts/core/groups/Groups.kt @@ -1,9 +1,7 @@ package contacts.core.groups -import android.content.Context import android.os.Build import contacts.core.Contacts -import contacts.core.ContactsPermissions /** * Provides new [GroupsQuery], [GroupsInsert], [GroupsUpdate], and [GroupsDelete] instances. @@ -44,36 +42,29 @@ interface Groups { fun delete(): GroupsDelete? /** - * Returns a [ContactsPermissions] instance, which provides functions for checking required - * permissions. + * A reference to the [Contacts] instance that constructed this. This is mostly used internally + * to shorten internal code. + * + * Don't worry, [Contacts] does not keep references to instances of this. There are no circular + * references that could cause leaks =). [Contacts] is just a factory. */ - val permissions: ContactsPermissions - - /** - * Reference to the Application's Context for use in extension functions and external library - * modules. This is safe to hold on to. Not meant for consumer use. - */ - val applicationContext: Context + val contactsApi: Contacts } @Suppress("FunctionName") internal fun Groups(contacts: Contacts): Groups = GroupsImpl(contacts) -private class GroupsImpl(private val contacts: Contacts) : Groups { +private class GroupsImpl(override val contactsApi: Contacts) : Groups { - override fun query() = GroupsQuery(contacts) + override fun query() = GroupsQuery(contactsApi) - override fun insert() = GroupsInsert(contacts) + override fun insert() = GroupsInsert(contactsApi) - override fun update() = GroupsUpdate(contacts) + override fun update() = GroupsUpdate(contactsApi) override fun delete() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - GroupsDelete(contacts) + GroupsDelete(contactsApi) } else { null } - - override val permissions: ContactsPermissions = contacts.permissions - - override val applicationContext: Context = contacts.applicationContext } \ No newline at end of file diff --git a/core/src/main/java/contacts/core/groups/GroupsDelete.kt b/core/src/main/java/contacts/core/groups/GroupsDelete.kt index f9ca6e47..0219a533 100644 --- a/core/src/main/java/contacts/core/groups/GroupsDelete.kt +++ b/core/src/main/java/contacts/core/groups/GroupsDelete.kt @@ -1,6 +1,5 @@ package contacts.core.groups -import android.content.ContentResolver import contacts.core.* import contacts.core.entities.ExistingGroupEntity import contacts.core.entities.operation.GroupsOperation @@ -40,7 +39,7 @@ import contacts.core.util.unsafeLazy * * DO NOT USE THIS ON API VERSION BELOW 26! Or use at your own peril =) */ -interface GroupsDelete : Redactable { +interface GroupsDelete : CrudApi { /** * Adds the given [groups] to the delete queue, which will be deleted on [commit]. @@ -101,7 +100,7 @@ interface GroupsDelete : Redactable { // We have to cast the return type because we are not using recursive generic types. override fun redactedCopy(): GroupsDelete - interface Result : Redactable { + interface Result : CrudApi.Result { /** * True if all Groups have successfully been deleted. False if even one delete failed. @@ -119,14 +118,10 @@ interface GroupsDelete : Redactable { } @Suppress("FunctionName") -internal fun GroupsDelete(contacts: Contacts): GroupsDelete = GroupsDeleteImpl( - contacts.applicationContext.contentResolver, - contacts.permissions -) +internal fun GroupsDelete(contacts: Contacts): GroupsDelete = GroupsDeleteImpl(contacts) private class GroupsDeleteImpl( - private val contentResolver: ContentResolver, - private val permissions: ContactsPermissions, + override val contactsApi: Contacts, private val groups: MutableSet = mutableSetOf(), @@ -143,7 +138,7 @@ private class GroupsDeleteImpl( """.trimIndent() override fun redactedCopy(): GroupsDelete = GroupsDeleteImpl( - contentResolver, permissions, + contactsApi, // Redact group data. groups.asSequence().redactedCopies().toMutableSet(), @@ -160,7 +155,8 @@ private class GroupsDeleteImpl( } override fun commit(): GroupsDelete.Result { - // TODO issue #144 log this + onPreExecute() + return if (groups.isEmpty() || !permissions.canUpdateDelete()) { GroupsDeleteResult(emptyMap()) } else { @@ -174,20 +170,23 @@ private class GroupsDeleteImpl( } } GroupsDeleteResult(results) - }.redactedCopyOrThis(isRedacted) - // TODO issue #144 log result + } + .redactedCopyOrThis(isRedacted) + .apply { onPostExecute(contactsApi) } } override fun commitInOneTransaction(): GroupsDelete.Result { - // TODO issue #144 log this + onPreExecute() + val isSuccessful = permissions.canUpdateDelete() && groups.isNotEmpty() // Fail immediately if the set contains a read-only group. && groups.find { it.readOnly } == null && contentResolver.applyBatch(GroupsOperation().delete(groups.map { it.id })) != null - return GroupsDeleteAllResult(isSuccessful).redactedCopyOrThis(isRedacted) - // TODO issue #144 log result + return GroupsDeleteAllResult(isSuccessful) + .redactedCopyOrThis(isRedacted) + .apply { onPostExecute(contactsApi) } } } diff --git a/core/src/main/java/contacts/core/groups/GroupsInsert.kt b/core/src/main/java/contacts/core/groups/GroupsInsert.kt index 50622ee8..1e8582c4 100644 --- a/core/src/main/java/contacts/core/groups/GroupsInsert.kt +++ b/core/src/main/java/contacts/core/groups/GroupsInsert.kt @@ -3,7 +3,6 @@ package contacts.core.groups import android.accounts.Account import android.content.ContentResolver import contacts.core.* -import contacts.core.accounts.AccountsQuery import contacts.core.entities.NewGroup import contacts.core.entities.operation.GroupsOperation import contacts.core.groups.GroupsInsert.Result.FailureReason @@ -44,7 +43,7 @@ import contacts.core.util.unsafeLazy * .commit() * ``` */ -interface GroupsInsert : Redactable { +interface GroupsInsert : CrudApi { /** * Adds a new [NewGroup] to the insert queue, which will be inserted on [commit]. @@ -136,7 +135,7 @@ interface GroupsInsert : Redactable { // We have to cast the return type because we are not using recursive generic types. override fun redactedCopy(): GroupsInsert - interface Result : Redactable { + interface Result : CrudApi.Result { /** * The list of IDs of successfully created Groups. @@ -205,18 +204,10 @@ interface GroupsInsert : Redactable { } @Suppress("FunctionName") -internal fun GroupsInsert(contacts: Contacts): GroupsInsert = GroupsInsertImpl( - contacts.applicationContext.contentResolver, - contacts.accounts().query(), - contacts.groups().query(), - contacts.permissions -) +internal fun GroupsInsert(contacts: Contacts): GroupsInsert = GroupsInsertImpl(contacts) private class GroupsInsertImpl( - private val contentResolver: ContentResolver, - private val accountsQuery: AccountsQuery, - private val groupsQuery: GroupsQuery, - private val permissions: ContactsPermissions, + override val contactsApi: Contacts, private val groups: MutableSet = mutableSetOf(), @@ -233,7 +224,7 @@ private class GroupsInsertImpl( """.trimIndent() override fun redactedCopy(): GroupsInsert = GroupsInsertImpl( - contentResolver, accountsQuery, groupsQuery, permissions, + contactsApi, // Redact group data. groups.asSequence().redactedCopies().toMutableSet(), @@ -255,8 +246,9 @@ private class GroupsInsertImpl( override fun commit(): GroupsInsert.Result = commit { false } override fun commit(cancel: () -> Boolean): GroupsInsert.Result { - // TODO issue #144 log this - val accounts = accountsQuery.find() + onPreExecute() + + val accounts = contactsApi.accounts().query().find() return if ( groups.isEmpty() || !permissions.canInsert() @@ -270,7 +262,7 @@ private class GroupsInsertImpl( val groupsAccounts = groups.map { it.account } // Gather the existing titles per account to prevent duplicates. - val existingGroups = groupsQuery + val existingGroups = contactsApi.groups().query() // Limit the accounts for optimization in case there are a lot of accounts in the system .accounts(groupsAccounts) .find() @@ -312,8 +304,9 @@ private class GroupsInsertImpl( } GroupsInsertResult(results, failureReasons) - }.redactedCopyOrThis(isRedacted) - // TODO issue #144 log result + } + .redactedCopyOrThis(isRedacted) + .apply { onPostExecute(contactsApi) } } } diff --git a/core/src/main/java/contacts/core/groups/GroupsQuery.kt b/core/src/main/java/contacts/core/groups/GroupsQuery.kt index efa41156..e2c6b406 100644 --- a/core/src/main/java/contacts/core/groups/GroupsQuery.kt +++ b/core/src/main/java/contacts/core/groups/GroupsQuery.kt @@ -60,7 +60,7 @@ import contacts.core.util.unsafeLazy * there are no available accounts, the native Contacts app does not show the groups field because * there are no rows in the groups table. */ -interface GroupsQuery : Redactable { +interface GroupsQuery : CrudApi { /** * Limits the group(s) returned by this query to groups belonging to one of the [accounts]. @@ -208,7 +208,7 @@ interface GroupsQuery : Redactable { * * You may print individual groups in this list by iterating through it. */ - interface Result : List, Redactable { + interface Result : List, CrudApi.Result { /** * The list of [Group]s from the specified [account] ordered by [orderBy]. @@ -223,14 +223,10 @@ interface GroupsQuery : Redactable { } @Suppress("FunctionName") -internal fun GroupsQuery(contacts: Contacts): GroupsQuery = GroupsQueryImpl( - contacts.applicationContext.contentResolver, - contacts.permissions -) +internal fun GroupsQuery(contacts: Contacts): GroupsQuery = GroupsQueryImpl(contacts) private class GroupsQueryImpl( - private val contentResolver: ContentResolver, - private val permissions: ContactsPermissions, + override val contactsApi: Contacts, // The Groups table has access to the same sync columns as the RawContacts table, which provides // the Account name and type. @@ -257,7 +253,7 @@ private class GroupsQueryImpl( """.trimIndent() override fun redactedCopy(): GroupsQuery = GroupsQueryImpl( - contentResolver, permissions, + contactsApi, // Redact account info. rawContactsWhere?.redactedCopy(), @@ -319,15 +315,17 @@ private class GroupsQueryImpl( override fun find(): GroupsQuery.Result = find { false } override fun find(cancel: () -> Boolean): GroupsQuery.Result { - // TODO issue #144 log this + onPreExecute() + return if (!permissions.canQuery()) { GroupsQueryResult(emptyList()) } else { contentResolver.resolve( rawContactsWhere, INCLUDE, where, orderBy, limit, offset, cancel ) - }.redactedCopyOrThis(isRedacted) - // TODO issue #144 log result + } + .redactedCopyOrThis(isRedacted) + .apply { onPostExecute(contactsApi) } } companion object { diff --git a/core/src/main/java/contacts/core/groups/GroupsUpdate.kt b/core/src/main/java/contacts/core/groups/GroupsUpdate.kt index 7e7b14da..163f9d9d 100644 --- a/core/src/main/java/contacts/core/groups/GroupsUpdate.kt +++ b/core/src/main/java/contacts/core/groups/GroupsUpdate.kt @@ -2,15 +2,12 @@ package contacts.core.groups import android.accounts.Account import android.content.ContentResolver -import contacts.core.Contacts -import contacts.core.ContactsPermissions -import contacts.core.Redactable +import contacts.core.* import contacts.core.entities.ExistingGroupEntity import contacts.core.entities.Group import contacts.core.entities.MutableGroup import contacts.core.entities.operation.GroupsOperation import contacts.core.groups.GroupsUpdate.Result.FailureReason -import contacts.core.redactedCopyOrThis import contacts.core.util.applyBatch import contacts.core.util.unsafeLazy @@ -50,7 +47,7 @@ import contacts.core.util.unsafeLazy * } * ``` */ -interface GroupsUpdate : Redactable { +interface GroupsUpdate : CrudApi { /** * Adds the given [groups] to the update queue, which will be updated on [commit]. @@ -129,7 +126,7 @@ interface GroupsUpdate : Redactable { // We have to cast the return type because we are not using recursive generic types. override fun redactedCopy(): GroupsUpdate - interface Result : Redactable { + interface Result : CrudApi.Result { /** * True if all Groups have successfully been updated. False if even one update failed. @@ -179,16 +176,10 @@ interface GroupsUpdate : Redactable { } @Suppress("FunctionName") -internal fun GroupsUpdate(contacts: Contacts): GroupsUpdate = GroupsUpdateImpl( - contacts.applicationContext.contentResolver, - contacts.groups().query(), - contacts.permissions -) +internal fun GroupsUpdate(contacts: Contacts): GroupsUpdate = GroupsUpdateImpl(contacts) private class GroupsUpdateImpl( - private val contentResolver: ContentResolver, - private val groupsQuery: GroupsQuery, - private val permissions: ContactsPermissions, + override val contactsApi: Contacts, private val groups: MutableSet = mutableSetOf(), @@ -205,7 +196,7 @@ private class GroupsUpdateImpl( """.trimIndent() override fun redactedCopy(): GroupsUpdate = GroupsUpdateImpl( - contentResolver, groupsQuery, permissions, + contactsApi, // Redact group info. groups.asSequence().map { it?.redactedCopy() }.toMutableSet(), @@ -224,7 +215,8 @@ private class GroupsUpdateImpl( override fun commit(): GroupsUpdate.Result = commit { false } override fun commit(cancel: () -> Boolean): GroupsUpdate.Result { - // TODO issue #144 log this + onPreExecute() + return if (groups.isEmpty() || !permissions.canUpdateDelete() || cancel()) { GroupsUpdateFailed() } else { @@ -232,7 +224,7 @@ private class GroupsUpdateImpl( val groupsAccounts = groups.mapNotNull { it?.account } // Gather the existing groups per account to prevent duplicate titles. - val existingGroups = groupsQuery + val existingGroups = contactsApi.groups().query() // Limit the accounts for optimization in case there are a lot of accounts in the system .accounts(groupsAccounts) .find() @@ -298,8 +290,9 @@ private class GroupsUpdateImpl( } GroupsUpdateResult(failureReasons) - }.redactedCopyOrThis(isRedacted) - // TODO issue #144 log this + } + .redactedCopyOrThis(isRedacted) + .apply { onPostExecute(contactsApi) } } } diff --git a/core/src/main/java/contacts/core/profile/Profile.kt b/core/src/main/java/contacts/core/profile/Profile.kt index 55bf0d5c..6f82b36d 100644 --- a/core/src/main/java/contacts/core/profile/Profile.kt +++ b/core/src/main/java/contacts/core/profile/Profile.kt @@ -1,8 +1,6 @@ package contacts.core.profile -import android.content.Context import contacts.core.Contacts -import contacts.core.ContactsPermissions import contacts.core.data.Data /** @@ -47,34 +45,27 @@ interface Profile { fun data(): Data /** - * Returns a [ContactsPermissions] instance, which provides functions for checking required - * permissions. + * A reference to the [Contacts] instance that constructed this. This is mostly used internally + * to shorten internal code. + * + * Don't worry, [Contacts] does not keep references to instances of this. There are no circular + * references that could cause leaks =). [Contacts] is just a factory. */ - val permissions: ContactsPermissions - - /** - * Reference to the Application's Context for use in extension functions and external library - * modules. This is safe to hold on to. Not meant for consumer use. - */ - val applicationContext: Context + val contactsApi: Contacts } @Suppress("FunctionName") internal fun Profile(contacts: Contacts): Profile = ProfileImpl(contacts) -private class ProfileImpl(private val contacts: Contacts) : Profile { - - override fun query() = ProfileQuery(contacts) - - override fun insert() = ProfileInsert(contacts) +private class ProfileImpl(override val contactsApi: Contacts) : Profile { - override fun update() = ProfileUpdate(contacts) + override fun query() = ProfileQuery(contactsApi) - override fun delete() = ProfileDelete(contacts) + override fun insert() = ProfileInsert(contactsApi) - override fun data() = Data(contacts, true) + override fun update() = ProfileUpdate(contactsApi) - override val permissions: ContactsPermissions = contacts.permissions + override fun delete() = ProfileDelete(contactsApi) - override val applicationContext: Context = contacts.applicationContext + override fun data() = Data(contactsApi, true) } \ No newline at end of file diff --git a/core/src/main/java/contacts/core/profile/ProfileDelete.kt b/core/src/main/java/contacts/core/profile/ProfileDelete.kt index 32e33a74..2be393a6 100644 --- a/core/src/main/java/contacts/core/profile/ProfileDelete.kt +++ b/core/src/main/java/contacts/core/profile/ProfileDelete.kt @@ -42,7 +42,7 @@ import contacts.core.util.unsafeLazy * .commit() * ``` */ -interface ProfileDelete : Redactable { +interface ProfileDelete : CrudApi { /** * Adds the given profile [rawContacts] ([ExistingRawContactEntity.isProfile]) to the delete @@ -137,7 +137,7 @@ interface ProfileDelete : Redactable { // We have to cast the return type because we are not using recursive generic types. override fun redactedCopy(): ProfileDelete - interface Result : Redactable { + interface Result : CrudApi.Result { /** * True if the profile [ExistingContactEntity] has been successfully deleted @@ -162,14 +162,10 @@ interface ProfileDelete : Redactable { } @Suppress("FunctionName") -internal fun ProfileDelete(contacts: Contacts): ProfileDelete = ProfileDeleteImpl( - contacts.applicationContext.contentResolver, - contacts.permissions -) +internal fun ProfileDelete(contacts: Contacts): ProfileDelete = ProfileDeleteImpl(contacts) private class ProfileDeleteImpl( - private val contentResolver: ContentResolver, - private val permissions: ContactsPermissions, + override val contactsApi: Contacts, private val rawContactIds: MutableSet = mutableSetOf(), private var deleteProfileContact: Boolean = false, @@ -189,7 +185,7 @@ private class ProfileDeleteImpl( // There isn't really anything to redact =) override fun redactedCopy(): ProfileDelete = ProfileDeleteImpl( - contentResolver, permissions, + contactsApi, rawContactIds, deleteProfileContact, @@ -213,7 +209,8 @@ private class ProfileDeleteImpl( } override fun commit(): ProfileDelete.Result { - // TODO issue #144 log this + onPreExecute() + return if ((rawContactIds.isEmpty() && !deleteProfileContact) || !permissions.canUpdateDelete()) { ProfileDeleteAllResult(isSuccessful = false) } else if (deleteProfileContact) { @@ -238,12 +235,14 @@ private class ProfileDeleteImpl( rawContactsResult, profileContactDeleteSuccess = false, ) - }.redactedCopyOrThis(isRedacted) - // TODO issue #144 log result + } + .redactedCopyOrThis(isRedacted) + .apply { onPostExecute(contactsApi) } } override fun commitInOneTransaction(): ProfileDelete.Result { - // TODO issue #144 log this + onPreExecute() + return if ((rawContactIds.isEmpty() && !deleteProfileContact) || !permissions.canUpdateDelete()) { ProfileDeleteAllResult(isSuccessful = false) } else if (deleteProfileContact) { @@ -261,8 +260,9 @@ private class ProfileDeleteImpl( ) != null ) } - }.redactedCopyOrThis(isRedacted) - // TODO issue #144 log result + } + .redactedCopyOrThis(isRedacted) + .apply { onPostExecute(contactsApi) } } } diff --git a/core/src/main/java/contacts/core/profile/ProfileInsert.kt b/core/src/main/java/contacts/core/profile/ProfileInsert.kt index 6ea30df3..b795a142 100644 --- a/core/src/main/java/contacts/core/profile/ProfileInsert.kt +++ b/core/src/main/java/contacts/core/profile/ProfileInsert.kt @@ -89,7 +89,7 @@ import contacts.core.util.* * .commit(); * ``` */ -interface ProfileInsert : Redactable { +interface ProfileInsert : CrudApi { /** * If [allowBlanks] is set to true, then blank RawContacts ([NewRawContact.isBlank]) will @@ -269,7 +269,7 @@ interface ProfileInsert : Redactable { // We have to cast the return type because we are not using recursive generic types. override fun redactedCopy(): ProfileInsert - interface Result : Redactable { + interface Result : CrudApi.Result { /** * The ID of the successfully created RawContact. Null if the insertion failed. @@ -290,11 +290,11 @@ interface ProfileInsert : Redactable { internal fun ProfileInsert(contacts: Contacts): ProfileInsert = ProfileInsertImpl(contacts) private class ProfileInsertImpl( - private val contacts: Contacts, + override val contactsApi: Contacts, private var allowBlanks: Boolean = false, private var allowMultipleRawContactsPerAccount: Boolean = false, - private var include: Include = allDataFields(contacts.customDataRegistry), + private var include: Include = contactsApi.includeAllFields(), private var account: Account? = null, private var rawContact: NewRawContact? = null, @@ -309,13 +309,13 @@ private class ProfileInsertImpl( include: $include account: $account rawContact: $rawContact - hasPermission: ${contacts.permissions.canInsert()} + hasPermission: ${permissions.canInsert()} isRedacted: $isRedacted } """.trimIndent() override fun redactedCopy(): ProfileInsert = ProfileInsertImpl( - contacts, + contactsApi, allowBlanks, allowMultipleRawContactsPerAccount, @@ -348,7 +348,7 @@ private class ProfileInsertImpl( override fun include(fields: Sequence): ProfileInsert = apply { include = if (fields.isEmpty()) { - allDataFields(contacts.customDataRegistry) + contactsApi.includeAllFields() } else { Include(fields + Fields.Required.all.asSequence()) } @@ -367,39 +367,37 @@ private class ProfileInsertImpl( override fun commit(): ProfileInsert.Result = commit { false } override fun commit(cancel: () -> Boolean): ProfileInsert.Result { - // TODO issue #144 log this - val rawContact = rawContact + onPreExecute() + val rawContact = rawContact return if (rawContact == null || (!allowBlanks && rawContact.isBlank) - || !contacts.permissions.canInsert() + || !permissions.canInsert() || cancel() ) { ProfileInsertFailed() } else { // This ensures that a valid account is used. Otherwise, null is used. - account = account?.nullIfNotInSystem(contacts.accounts()) + account = account?.nullIfNotInSystem(contactsApi.accounts()) if ( - (!allowMultipleRawContactsPerAccount && - contacts.applicationContext.contentResolver.hasProfileRawContactForAccount( - account - )) + (!allowMultipleRawContactsPerAccount + && contentResolver.hasProfileRawContactForAccount(account)) || cancel() ) { ProfileInsertFailed() } else { // No need to propagate the cancel function to within insertRawContactForAccount // as that operation should be fast and CPU time should be trivial. - val rawContactId = - contacts.insertRawContactForAccount( - account, include.fields, rawContact, IS_PROFILE - ) + val rawContactId = contactsApi.insertRawContactForAccount( + account, include.fields, rawContact, IS_PROFILE + ) return ProfileInsertResult(rawContactId) } - }.redactedCopyOrThis(isRedacted) - // TODO issue #144 log result + } + .redactedCopyOrThis(isRedacted) + .apply { onPostExecute(contactsApi) } } private companion object { diff --git a/core/src/main/java/contacts/core/profile/ProfileQuery.kt b/core/src/main/java/contacts/core/profile/ProfileQuery.kt index d448161a..ca367227 100644 --- a/core/src/main/java/contacts/core/profile/ProfileQuery.kt +++ b/core/src/main/java/contacts/core/profile/ProfileQuery.kt @@ -54,7 +54,7 @@ import contacts.core.util.unsafeLazy * .find(); * ``` */ -interface ProfileQuery : Redactable { +interface ProfileQuery : CrudApi { /** * If [includeBlanks] is set to true, then queries may include blank RawContacts. Otherwise, @@ -225,7 +225,7 @@ interface ProfileQuery : Redactable { /** * Contains the Profile [contact]. */ - interface Result : Redactable { + interface Result : CrudApi.Result { /** * The Profile [Contact], if exist. @@ -238,20 +238,14 @@ interface ProfileQuery : Redactable { } @Suppress("FunctionName") -internal fun ProfileQuery(contacts: Contacts): ProfileQuery = ProfileQueryImpl( - contacts.applicationContext.contentResolver, - contacts.permissions, - contacts.customDataRegistry -) +internal fun ProfileQuery(contacts: Contacts): ProfileQuery = ProfileQueryImpl(contacts) private class ProfileQueryImpl( - private val contentResolver: ContentResolver, - private val permissions: ContactsPermissions, - private val customDataRegistry: CustomDataRegistry, + override val contactsApi: Contacts, private var includeBlanks: Boolean = DEFAULT_INCLUDE_BLANKS, private var rawContactsWhere: Where? = DEFAULT_RAW_CONTACTS_WHERE, - private var include: Include = allDataFields(customDataRegistry), + private var include: Include = contactsApi.includeAllFields(), override val isRedacted: Boolean = false ) : ProfileQuery { @@ -268,7 +262,7 @@ private class ProfileQueryImpl( """.trimIndent() override fun redactedCopy(): ProfileQuery = ProfileQueryImpl( - contentResolver, permissions, customDataRegistry, + contactsApi, includeBlanks, // Redact Account information. @@ -296,7 +290,7 @@ private class ProfileQueryImpl( override fun include(fields: Sequence): ProfileQuery = apply { include = if (fields.isEmpty()) { - allDataFields(customDataRegistry) + contactsApi.includeAllFields() } else { Include(fields + REQUIRED_INCLUDE_FIELDS) } @@ -308,7 +302,8 @@ private class ProfileQueryImpl( override fun find(): ProfileQuery.Result = find { false } override fun find(cancel: () -> Boolean): ProfileQuery.Result { - // TODO issue #144 log this + onPreExecute() + val profileContact = if (!permissions.canQuery()) { null } else { @@ -316,8 +311,9 @@ private class ProfileQueryImpl( customDataRegistry, includeBlanks, rawContactsWhere, include, cancel ) } - return ProfileQueryResult(profileContact).redactedCopyOrThis(isRedacted) - // TODO issue #144 log result + return ProfileQueryResult(profileContact) + .redactedCopyOrThis(isRedacted) + .apply { onPostExecute(contactsApi) } } private companion object { diff --git a/core/src/main/java/contacts/core/profile/ProfileUpdate.kt b/core/src/main/java/contacts/core/profile/ProfileUpdate.kt index 2f08297e..907d34cc 100644 --- a/core/src/main/java/contacts/core/profile/ProfileUpdate.kt +++ b/core/src/main/java/contacts/core/profile/ProfileUpdate.kt @@ -83,7 +83,7 @@ import contacts.core.util.unsafeLazy * However, keeping it separate like this gives us the most flexibility and cohesiveness of * profile APIs. */ -interface ProfileUpdate : Redactable { +interface ProfileUpdate : CrudApi { /** * If [deleteBlanks] is set to true, then updating blank profile RawContacts @@ -242,7 +242,7 @@ interface ProfileUpdate : Redactable { // We have to cast the return type because we are not using recursive generic types. override fun redactedCopy(): ProfileUpdate - interface Result : Redactable { + interface Result : CrudApi.Result { /** * True if all of the RawContacts have successfully been updated. False if even one @@ -264,10 +264,10 @@ interface ProfileUpdate : Redactable { internal fun ProfileUpdate(contacts: Contacts): ProfileUpdate = ProfileUpdateImpl(contacts) private class ProfileUpdateImpl( - private val contacts: Contacts, + override val contactsApi: Contacts, private var deleteBlanks: Boolean = true, - private var include: Include = allDataFields(contacts.customDataRegistry), + private var include: Include = contactsApi.includeAllFields(), private val rawContacts: MutableSet = mutableSetOf(), override val isRedacted: Boolean = false @@ -279,13 +279,13 @@ private class ProfileUpdateImpl( deleteBlanks: $deleteBlanks include: $include rawContacts: $rawContacts - hasPermission: ${contacts.permissions.canUpdateDelete()} + hasPermission: ${permissions.canUpdateDelete()} isRedacted: $isRedacted } """.trimIndent() override fun redactedCopy(): ProfileUpdate = ProfileUpdateImpl( - contacts, + contactsApi, deleteBlanks, include, @@ -305,7 +305,7 @@ private class ProfileUpdateImpl( override fun include(fields: Sequence): ProfileUpdate = apply { include = if (fields.isEmpty()) { - allDataFields(contacts.customDataRegistry) + contactsApi.includeAllFields() } else { Include(fields + Fields.Required.all.asSequence()) } @@ -331,8 +331,9 @@ private class ProfileUpdateImpl( override fun commit(): ProfileUpdate.Result = commit { false } override fun commit(cancel: () -> Boolean): ProfileUpdate.Result { - // TODO issue #144 log this - return if (rawContacts.isEmpty() || !contacts.permissions.canUpdateDelete() || cancel()) { + onPreExecute() + + return if (rawContacts.isEmpty() || !permissions.canUpdateDelete() || cancel()) { ProfileUpdateFailed() } else { val results = mutableMapOf() @@ -347,16 +348,16 @@ private class ProfileUpdateImpl( // design. false } else if (rawContact.isBlank && deleteBlanks) { - contacts.applicationContext.contentResolver - .deleteRawContactWithId(rawContact.id) + contentResolver.deleteRawContactWithId(rawContact.id) } else { - contacts.updateRawContact(include.fields, rawContact) + contactsApi.updateRawContact(include.fields, rawContact) } } ProfileUpdateResult(results) - }.redactedCopyOrThis(isRedacted) - // TODO issue #144 log result + } + .redactedCopyOrThis(isRedacted) + .apply { onPostExecute(contactsApi) } } } diff --git a/core/src/main/java/contacts/core/util/ContactLinks.kt b/core/src/main/java/contacts/core/util/ContactLinks.kt index 5a7bcc5e..57a8aa36 100644 --- a/core/src/main/java/contacts/core/util/ContactLinks.kt +++ b/core/src/main/java/contacts/core/util/ContactLinks.kt @@ -164,7 +164,7 @@ fun ExistingContactEntity.link( val nameRowIdToUseAsDefault = contactsApi.nameRowIdToUseAsDefault(prioritizedContactIds) // Note that the result uri is null. There is no meaningful information we can get here. - contactsApi.applicationContext.contentResolver.applyBatch( + contactsApi.contentResolver.applyBatch( aggregateExceptionsOperations( sortedRawContactIds, ContactsContract.AggregationExceptions.TYPE_KEEP_TOGETHER @@ -291,7 +291,7 @@ fun ExistingContactEntity.unlink(contactsApi: Contacts): ContactUnlinkResult { return ContactUnlinkFailed() } - contactsApi.applicationContext.contentResolver.applyBatch( + contactsApi.contentResolver.applyBatch( aggregateExceptionsOperations( sortedRawContactIds, ContactsContract.AggregationExceptions.TYPE_KEEP_SEPARATE @@ -395,7 +395,7 @@ private fun Contacts.nameRowIdToUseAsDefault(contactIds: Set): Long? { private fun Contacts.nameRawContactIdStructuredNameId(contactId: Long): Long? { val nameRawContactId = nameRawContactId(contactId) ?: return null - return applicationContext.contentResolver.query( + return contentResolver.query( Table.Data, Include(Fields.DataId), (Fields.RawContact.Id equalTo nameRawContactId) @@ -414,48 +414,46 @@ private fun Contacts.nameRawContactIdStructuredNameId(contactId: Long): Long? { */ // [ANDROID X] @RequiresApi(Build.VERSION_CODES.LOLLIPOP) // (not using annotation to avoid dependency on androidx.annotation) -private fun Contacts.nameRawContactId(contactId: Long): Long? = - applicationContext.contentResolver.query( - Table.Contacts, - Include(ContactsFields.DisplayNameSource, ContactsFields.NameRawContactId), - ContactsFields.Id equalTo contactId - ) { - var displayNameSource: Int = ContactsContract.DisplayNameSources.UNDEFINED - var nameRawContactId: Long? = null - - it.getNextOrNull { - val contactsCursor = it.contactsCursor() - displayNameSource = - contactsCursor.displayNameSource ?: ContactsContract.DisplayNameSources.UNDEFINED - nameRawContactId = contactsCursor.nameRawContactId - } +private fun Contacts.nameRawContactId(contactId: Long): Long? = contentResolver.query( + Table.Contacts, + Include(ContactsFields.DisplayNameSource, ContactsFields.NameRawContactId), + ContactsFields.Id equalTo contactId +) { + var displayNameSource: Int = ContactsContract.DisplayNameSources.UNDEFINED + var nameRawContactId: Long? = null + + it.getNextOrNull { + val contactsCursor = it.contactsCursor() + displayNameSource = + contactsCursor.displayNameSource ?: ContactsContract.DisplayNameSources.UNDEFINED + nameRawContactId = contactsCursor.nameRawContactId + } - if (displayNameSource != ContactsContract.DisplayNameSources.STRUCTURED_NAME) { - null - } else { - nameRawContactId - } + if (displayNameSource != ContactsContract.DisplayNameSources.STRUCTURED_NAME) { + null + } else { + nameRawContactId } +} /** * Returns the RawContact IDs of the Contacts with the given [contactIds] in ascending order. */ -private fun Contacts.sortedRawContactIds(contactIds: Set): List = - applicationContext.contentResolver.query( - Table.RawContacts, - Include(RawContactsFields.Id), - RawContactsFields.ContactId `in` contactIds, - RawContactsFields.Id.columnName - ) { - mutableListOf().apply { - val rawContactsCursor = it.rawContactsCursor() - while (it.moveToNext()) { - add(rawContactsCursor.rawContactId) - } +private fun Contacts.sortedRawContactIds(contactIds: Set): List = contentResolver.query( + Table.RawContacts, + Include(RawContactsFields.Id), + RawContactsFields.ContactId `in` contactIds, + RawContactsFields.Id.columnName +) { + mutableListOf().apply { + val rawContactsCursor = it.rawContactsCursor() + while (it.moveToNext()) { + add(rawContactsCursor.rawContactId) } - } ?: emptyList() + } +} ?: emptyList() -private fun Contacts.nameWithId(nameRowId: Long): Name? = applicationContext.contentResolver.query( +private fun Contacts.nameWithId(nameRowId: Long): Name? = contentResolver.query( Table.Data, Include(Fields.Required), Fields.DataId equalTo nameRowId @@ -463,13 +461,12 @@ private fun Contacts.nameWithId(nameRowId: Long): Name? = applicationContext.con it.getNextOrNull { it.nameMapper().value } } -private fun Contacts.contactIdOfRawContact(rawContactId: Long): Long? = - applicationContext.contentResolver.query( - Table.RawContacts, - Include(RawContactsFields.ContactId), - RawContactsFields.Id equalTo rawContactId - ) { - it.getNextOrNull { it.rawContactsCursor().contactId } - } +private fun Contacts.contactIdOfRawContact(rawContactId: Long): Long? = contentResolver.query( + Table.RawContacts, + Include(RawContactsFields.ContactId), + RawContactsFields.Id equalTo rawContactId +) { + it.getNextOrNull { it.rawContactsCursor().contactId } +} // endregion \ No newline at end of file diff --git a/core/src/main/java/contacts/core/util/ContactOptions.kt b/core/src/main/java/contacts/core/util/ContactOptions.kt index 623e1323..745fd26e 100644 --- a/core/src/main/java/contacts/core/util/ContactOptions.kt +++ b/core/src/main/java/contacts/core/util/ContactOptions.kt @@ -36,7 +36,7 @@ fun ExistingContactEntity.options(contacts: Contacts): Options? { return null } - return contacts.applicationContext.contentResolver.query( + return contacts.contentResolver.query( if (isProfile) ProfileUris.CONTACTS.uri else Table.Contacts.uri, Include(ContactsFields.Options), ContactsFields.Id equalTo id @@ -102,7 +102,7 @@ fun ExistingContactEntity.setOptions(contacts: Contacts, options: MutableOptions return false } - return contacts.applicationContext.contentResolver.applyBatch( + return contacts.contentResolver.applyBatch( OptionsOperation().updateContactOptions(id, options) ) != null } diff --git a/core/src/main/java/contacts/core/util/ContactPhoto.kt b/core/src/main/java/contacts/core/util/ContactPhoto.kt index 8a154d45..d0bf4efd 100644 --- a/core/src/main/java/contacts/core/util/ContactPhoto.kt +++ b/core/src/main/java/contacts/core/util/ContactPhoto.kt @@ -52,7 +52,7 @@ fun ExistingContactEntity.photoInputStream(contacts: Contacts): InputStream? { return null } - return contacts.applicationContext.contentResolver.query( + return contacts.contentResolver.query( if (isProfile) ProfileUris.CONTACTS.uri else Table.Contacts.uri, Include(ContactsFields.PhotoUri), ContactsFields.Id equalTo id @@ -146,7 +146,7 @@ fun ExistingContactEntity.photoBitmap(contacts: Contacts): Bitmap? = // [ANDROID X] @WorkerThread (not using annotation to avoid dependency on androidx.annotation) fun ExistingContactEntity.photoBitmapDrawable(contacts: Contacts): BitmapDrawable? = photoInputStream(contacts)?.apply { - BitmapDrawable(contacts.applicationContext.resources, it) + BitmapDrawable(contacts.resources, it) } private fun uriInputStream(contacts: Contacts, uri: Uri?): InputStream? { @@ -156,7 +156,7 @@ private fun uriInputStream(contacts: Contacts, uri: Uri?): InputStream? { var inputStream: InputStream? = null try { - val fd = contacts.applicationContext.contentResolver.openAssetFileDescriptor(uri, "r") + val fd = contacts.contentResolver.openAssetFileDescriptor(uri, "r") inputStream = fd?.createInputStream() } catch (ioe: IOException) { // do nothing @@ -199,7 +199,7 @@ fun ExistingContactEntity.photoThumbnailInputStream(contacts: Contacts): InputSt return null } - return contacts.applicationContext.contentResolver.query( + return contacts.contentResolver.query( if (isProfile) ProfileUris.CONTACTS.uri else Table.Contacts.uri, Include(ContactsFields.PhotoThumbnailUri), ContactsFields.Id equalTo id @@ -293,7 +293,7 @@ fun ExistingContactEntity.photoThumbnailBitmap(contacts: Contacts): Bitmap? = // [ANDROID X] @WorkerThread (not using annotation to avoid dependency on androidx.annotation) fun ExistingContactEntity.photoThumbnailBitmapDrawable(contacts: Contacts): BitmapDrawable? = photoThumbnailInputStream(contacts)?.apply { - BitmapDrawable(contacts.applicationContext.resources, it) + BitmapDrawable(contacts.resources, it) } // endregion @@ -385,7 +385,7 @@ fun ExistingContactEntity.setPhoto(contacts: Contacts, photoDrawable: BitmapDraw setPhoto(contacts, photoDrawable.bitmap.bytes()) private fun ExistingContactEntity.photoFileId(contacts: Contacts): Long? = - contacts.applicationContext.contentResolver.query( + contacts.contentResolver.query( if (isProfile) ProfileUris.CONTACTS.uri else Table.Contacts.uri, Include(ContactsFields.PhotoFileId), ContactsFields.Id equalTo id @@ -395,14 +395,13 @@ private fun ExistingContactEntity.photoFileId(contacts: Contacts): Long? = private fun ExistingContactEntity.rawContactWithPhotoFileId( contacts: Contacts, photoFileId: Long -): TempRawContact? = - contacts.applicationContext.contentResolver.query( - if (isProfile) ProfileUris.DATA.uri else Table.Data.uri, - Include(Fields.RawContact.Id), - Fields.Photo.PhotoFileId equalTo photoFileId - ) { - it.getNextOrNull { it.dataCursor().tempRawContactMapper().value } - } +): TempRawContact? = contacts.contentResolver.query( + if (isProfile) ProfileUris.DATA.uri else Table.Data.uri, + Include(Fields.RawContact.Id), + Fields.Photo.PhotoFileId equalTo photoFileId +) { + it.getNextOrNull { it.dataCursor().tempRawContactMapper().value } +} // endregion @@ -445,7 +444,7 @@ fun ExistingContactEntity.removePhoto(contacts: Contacts): Boolean { return false } - return contacts.applicationContext.contentResolver.applyBatch( + return contacts.contentResolver.applyBatch( newDelete(if (isProfile) ProfileUris.DATA.uri else Table.Data.uri) .withSelection( (Fields.Contact.Id equalTo id) diff --git a/core/src/main/java/contacts/core/util/DefaultContactData.kt b/core/src/main/java/contacts/core/util/DefaultContactData.kt index 68262232..cccb535f 100644 --- a/core/src/main/java/contacts/core/util/DefaultContactData.kt +++ b/core/src/main/java/contacts/core/util/DefaultContactData.kt @@ -55,7 +55,7 @@ fun ExistingDataEntity.setAsDefault(contacts: Contacts): Boolean { return false } - return contacts.applicationContext.contentResolver.applyBatch( + return contacts.contentResolver.applyBatch( clearPrimary(rawContactId), clearSuperPrimary(contactId), setPrimaryAndSuperPrimary(dataId) @@ -111,7 +111,7 @@ fun ExistingDataEntity.clearDefault(contactsApi: Contacts): Boolean { return false } - return contactsApi.applicationContext.contentResolver.applyBatch( + return contactsApi.contentResolver.applyBatch( clearPrimary(rawContactId), clearSuperPrimary(contactId) ) != null diff --git a/core/src/main/java/contacts/core/util/RawContactOptions.kt b/core/src/main/java/contacts/core/util/RawContactOptions.kt index f38bf3cb..ec721c62 100644 --- a/core/src/main/java/contacts/core/util/RawContactOptions.kt +++ b/core/src/main/java/contacts/core/util/RawContactOptions.kt @@ -1,10 +1,10 @@ package contacts.core.util import contacts.core.* -import contacts.core.entities.Options import contacts.core.entities.ExistingRawContactEntity import contacts.core.entities.MutableOptionsEntity import contacts.core.entities.NewOptions +import contacts.core.entities.Options import contacts.core.entities.mapper.rawContactsOptionsMapper import contacts.core.entities.operation.OptionsOperation import contacts.core.entities.table.ProfileUris @@ -35,7 +35,7 @@ fun ExistingRawContactEntity.options(contacts: Contacts): Options? { return null } - return contacts.applicationContext.contentResolver.query( + return contacts.contentResolver.query( if (isProfile) ProfileUris.RAW_CONTACTS.uri else Table.RawContacts.uri, Include(RawContactsFields.Options), RawContactsFields.Id equalTo id @@ -104,7 +104,7 @@ fun ExistingRawContactEntity.setOptions( return false } - return contacts.applicationContext.contentResolver.applyBatch( + return contacts.contentResolver.applyBatch( OptionsOperation().updateRawContactOptions(id, options) ) != null } diff --git a/core/src/main/java/contacts/core/util/RawContactPhoto.kt b/core/src/main/java/contacts/core/util/RawContactPhoto.kt index d42622c6..18274d27 100644 --- a/core/src/main/java/contacts/core/util/RawContactPhoto.kt +++ b/core/src/main/java/contacts/core/util/RawContactPhoto.kt @@ -53,7 +53,7 @@ fun ExistingRawContactEntity.photoInputStream(contacts: Contacts): InputStream? var inputStream: InputStream? = null try { - val fd = contacts.applicationContext.contentResolver.openAssetFileDescriptor(photoUri, "r") + val fd = contacts.contentResolver.openAssetFileDescriptor(photoUri, "r") inputStream = fd?.createInputStream() } catch (ioe: IOException) { // do nothing @@ -124,7 +124,7 @@ fun ExistingRawContactEntity.photoBitmap(contacts: Contacts): Bitmap? = // [ANDROID X] @WorkerThread (not using annotation to avoid dependency on androidx.annotation) fun ExistingRawContactEntity.photoBitmapDrawable(contacts: Contacts): BitmapDrawable? = photoInputStream(contacts)?.apply { - BitmapDrawable(contacts.applicationContext.resources, it) + BitmapDrawable(contacts.resources, it) } internal inline fun InputStream.apply(block: (InputStream) -> T): T { @@ -161,7 +161,7 @@ fun ExistingRawContactEntity.photoThumbnailInputStream(contacts: Contacts): Inpu return null } - return contacts.applicationContext.contentResolver.query( + return contacts.contentResolver.query( if (isProfile) ProfileUris.DATA.uri else Table.Data.uri, Include(Fields.Photo.PhotoThumbnail), (Fields.RawContact.Id equalTo id) @@ -235,7 +235,7 @@ fun ExistingRawContactEntity.photoThumbnailBitmap(contacts: Contacts): Bitmap? = // [ANDROID X] @WorkerThread (not using annotation to avoid dependency on androidx.annotation) fun ExistingRawContactEntity.photoThumbnailBitmapDrawable(contacts: Contacts): BitmapDrawable? = photoThumbnailInputStream(contacts)?.apply { - BitmapDrawable(contacts.applicationContext.resources, it) + BitmapDrawable(contacts.resources, it) } // endregion @@ -328,7 +328,7 @@ internal fun Contacts.setRawContactPhoto( // Didn't want to force unwrap because I'm trying to keep the codebase free of it. // I wanted to fold the if-return using ?: but it results in a lint error about unreachable // code (it's not unreachable). - val fd = applicationContext.contentResolver + val fd = contentResolver .openAssetFileDescriptor(photoUri, "rw") if (fd != null) { val os = fd.createOutputStream() @@ -393,7 +393,7 @@ fun ExistingRawContactEntity.removePhoto(contacts: Contacts): Boolean { return false } - return contacts.applicationContext.contentResolver.applyBatch( + return contacts.contentResolver.applyBatch( newDelete(if (isProfile) ProfileUris.DATA.uri else Table.Data.uri) .withSelection( (Fields.RawContact.Id equalTo id) diff --git a/permissions/src/main/java/contacts/permissions/accounts/AccountsPermissionsRequest.kt b/permissions/src/main/java/contacts/permissions/accounts/AccountsPermissionsRequest.kt index 89fed96d..273aea79 100644 --- a/permissions/src/main/java/contacts/permissions/accounts/AccountsPermissionsRequest.kt +++ b/permissions/src/main/java/contacts/permissions/accounts/AccountsPermissionsRequest.kt @@ -16,8 +16,8 @@ import contacts.permissions.requestWritePermission * If permissions are already granted, then immediately returns a new [AccountsQuery] instance. */ suspend fun Accounts.queryWithPermission(): AccountsQuery { - if (!permissions.canQueryAccounts()) { - applicationContext.requestQueryAccountsPermission() + if (!contactsApi.accountsPermissions.canQueryAccounts()) { + contactsApi.applicationContext.requestQueryAccountsPermission() } return query() @@ -31,8 +31,8 @@ suspend fun Accounts.queryWithPermission(): AccountsQuery { * instance. */ suspend fun Accounts.queryRawContactsWithPermission(): AccountsRawContactsQuery { - if (!permissions.canQueryRawContacts()) { - applicationContext.requestQueryRawContactsPermission() + if (!contactsApi.accountsPermissions.canQueryRawContacts()) { + contactsApi.applicationContext.requestQueryRawContactsPermission() } return queryRawContacts() @@ -48,8 +48,8 @@ suspend fun Accounts.queryRawContactsWithPermission(): AccountsRawContactsQuery */ suspend fun Accounts.updateLocalRawContactsAccountWithPermission(): AccountsLocalRawContactsUpdate { - if (!permissions.canUpdateLocalRawContactsAccount()) { - applicationContext.requestUpdateLocalRawContactsAccountPermission() + if (!contactsApi.accountsPermissions.canUpdateLocalRawContactsAccount()) { + contactsApi.applicationContext.requestUpdateLocalRawContactsAccountPermission() } return updateLocalRawContactsAccount() diff --git a/permissions/src/main/java/contacts/permissions/data/DataPermissionsRequest.kt b/permissions/src/main/java/contacts/permissions/data/DataPermissionsRequest.kt index 2b0db208..bf6cbc66 100644 --- a/permissions/src/main/java/contacts/permissions/data/DataPermissionsRequest.kt +++ b/permissions/src/main/java/contacts/permissions/data/DataPermissionsRequest.kt @@ -15,8 +15,8 @@ import contacts.permissions.requestWritePermission * If permission is already granted, then immediately returns a new [DataQueryFactory] instance. */ suspend fun Data.queryWithPermission(): DataQueryFactory { - if (!permissions.canQuery()) { - applicationContext.requestReadPermission() + if (!contactsApi.permissions.canQuery()) { + contactsApi.applicationContext.requestReadPermission() } return query() @@ -29,8 +29,8 @@ suspend fun Data.queryWithPermission(): DataQueryFactory { * If permission is already granted, then immediately returns a new [DataUpdate] instance. */ suspend fun Data.updateWithPermission(): DataUpdate { - if (!permissions.canUpdateDelete()) { - applicationContext.requestWritePermission() + if (!contactsApi.permissions.canUpdateDelete()) { + contactsApi.applicationContext.requestWritePermission() } return update() @@ -43,8 +43,8 @@ suspend fun Data.updateWithPermission(): DataUpdate { * If permissions are already granted, then immediately returns a new [DataDelete] instance. */ suspend fun Data.deleteWithPermission(): DataDelete { - if (!permissions.canUpdateDelete()) { - applicationContext.requestWritePermission() + if (!contactsApi.permissions.canUpdateDelete()) { + contactsApi.applicationContext.requestWritePermission() } return delete() diff --git a/permissions/src/main/java/contacts/permissions/groups/GroupsPermissionsRequest.kt b/permissions/src/main/java/contacts/permissions/groups/GroupsPermissionsRequest.kt index 08887252..4c2ee93e 100644 --- a/permissions/src/main/java/contacts/permissions/groups/GroupsPermissionsRequest.kt +++ b/permissions/src/main/java/contacts/permissions/groups/GroupsPermissionsRequest.kt @@ -13,8 +13,8 @@ import contacts.permissions.requestWritePermission * If permission is already granted, then immediately returns a new [GroupsQuery] instance. */ suspend fun Groups.queryWithPermission(): GroupsQuery { - if (!permissions.canQuery()) { - applicationContext.requestReadPermission() + if (!contactsApi.permissions.canQuery()) { + contactsApi.applicationContext.requestReadPermission() } return query() @@ -29,9 +29,9 @@ suspend fun Groups.queryWithPermission(): GroupsQuery { * If permissions are already granted, then immediately returns a new [GroupsInsert] instance. */ suspend fun Groups.insertWithPermission(): GroupsInsert { - if (!permissions.canInsert()) { - applicationContext.requestWritePermission() - applicationContext.requestGetAccountsPermission() + if (!contactsApi.permissions.canInsert()) { + contactsApi.applicationContext.requestWritePermission() + contactsApi.applicationContext.requestGetAccountsPermission() } return insert() @@ -44,8 +44,8 @@ suspend fun Groups.insertWithPermission(): GroupsInsert { * If permissions are already granted, then immediately returns a new [GroupsUpdate] instance. */ suspend fun Groups.updateWithPermission(): GroupsUpdate { - if (!permissions.canUpdateDelete()) { - applicationContext.requestWritePermission() + if (!contactsApi.permissions.canUpdateDelete()) { + contactsApi.applicationContext.requestWritePermission() } return update() @@ -58,8 +58,8 @@ suspend fun Groups.updateWithPermission(): GroupsUpdate { * If permissions are already granted, then immediately returns a new [GroupsDelete] instance. */ suspend fun Groups.deleteWithPermission(): GroupsDelete? { - if (!permissions.canUpdateDelete()) { - applicationContext.requestWritePermission() + if (!contactsApi.permissions.canUpdateDelete()) { + contactsApi.applicationContext.requestWritePermission() } return delete() diff --git a/permissions/src/main/java/contacts/permissions/profile/ProfilePermissionsRequest.kt b/permissions/src/main/java/contacts/permissions/profile/ProfilePermissionsRequest.kt index 25d89f1c..155e401f 100644 --- a/permissions/src/main/java/contacts/permissions/profile/ProfilePermissionsRequest.kt +++ b/permissions/src/main/java/contacts/permissions/profile/ProfilePermissionsRequest.kt @@ -13,8 +13,8 @@ import contacts.permissions.requestWritePermission * If permission is already granted, then immediately returns a new [ProfileQuery] instance. */ suspend fun Profile.queryWithPermission(): ProfileQuery { - if (!permissions.canQuery()) { - applicationContext.requestReadPermission() + if (!contactsApi.permissions.canQuery()) { + contactsApi.applicationContext.requestReadPermission() } return query() @@ -29,9 +29,9 @@ suspend fun Profile.queryWithPermission(): ProfileQuery { * If permission is already granted, then immediately returns a new [ProfileInsert] instance. */ suspend fun Profile.insertWithPermission(): ProfileInsert { - if (!permissions.canInsert()) { - applicationContext.requestWritePermission() - applicationContext.requestGetAccountsPermission() + if (!contactsApi.permissions.canInsert()) { + contactsApi.applicationContext.requestWritePermission() + contactsApi.applicationContext.requestGetAccountsPermission() } return insert() @@ -44,8 +44,8 @@ suspend fun Profile.insertWithPermission(): ProfileInsert { * If permission is already granted, then immediately returns a new [ProfileUpdate] instance. */ suspend fun Profile.updateWithPermission(): ProfileUpdate { - if (!permissions.canUpdateDelete()) { - applicationContext.requestWritePermission() + if (!contactsApi.permissions.canUpdateDelete()) { + contactsApi.applicationContext.requestWritePermission() } return update() @@ -58,8 +58,8 @@ suspend fun Profile.updateWithPermission(): ProfileUpdate { * If permission is already granted, then immediately returns a new [ProfileDelete] instance. */ suspend fun Profile.deleteWithPermission(): ProfileDelete { - if (!permissions.canUpdateDelete()) { - applicationContext.requestWritePermission() + if (!contactsApi.permissions.canUpdateDelete()) { + contactsApi.applicationContext.requestWritePermission() } return delete() diff --git a/sample/src/main/java/contacts/sample/SampleApp.kt b/sample/src/main/java/contacts/sample/SampleApp.kt index 508c1700..a37e1064 100644 --- a/sample/src/main/java/contacts/sample/SampleApp.kt +++ b/sample/src/main/java/contacts/sample/SampleApp.kt @@ -13,12 +13,10 @@ class SampleApp : Application() { val contacts: Contacts by lazy(LazyThreadSafetyMode.NONE) { Contacts( this, - CustomDataRegistry().apply { - register( - GenderRegistration(), - HandleNameRegistration() - ) - } + CustomDataRegistry().register( + GenderRegistration(), + HandleNameRegistration() + ) ) } } \ No newline at end of file diff --git a/test/src/main/java/contacts/test/TestContacts.kt b/test/src/main/java/contacts/test/TestContacts.kt index 9abc3bf6..75fc5dd8 100644 --- a/test/src/main/java/contacts/test/TestContacts.kt +++ b/test/src/main/java/contacts/test/TestContacts.kt @@ -35,9 +35,9 @@ object ContactsFactory { /** * TODO document this */ -private class TestContacts(private val contacts: Contacts) : Contacts { +private class TestContacts(private val contactsApi: Contacts) : Contacts { - override fun query(): Query = TestQuery(contacts.query()) + override fun query(): Query = TestQuery(contactsApi.query(), contactsApi) override fun broadQuery(): BroadQuery { TODO("Not yet implemented") @@ -73,9 +73,13 @@ private class TestContacts(private val contacts: Contacts) : Contacts { TODO("Not yet implemented") } - override val permissions: ContactsPermissions = contacts.permissions + override val permissions = contactsApi.permissions - override val applicationContext: Context = contacts.applicationContext + override val accountsPermissions = contactsApi.accountsPermissions - override val customDataRegistry: CustomDataRegistry = contacts.customDataRegistry + override val applicationContext = contactsApi.applicationContext + + override val customDataRegistry = contactsApi.customDataRegistry + + override val apiListenerRegistry = contactsApi.apiListenerRegistry } \ No newline at end of file diff --git a/test/src/main/java/contacts/test/TestQuery.kt b/test/src/main/java/contacts/test/TestQuery.kt index 3ab38869..9e3545d5 100644 --- a/test/src/main/java/contacts/test/TestQuery.kt +++ b/test/src/main/java/contacts/test/TestQuery.kt @@ -14,12 +14,13 @@ import contacts.test.entities.TestDataFields */ internal class TestQuery( private val query: Query, + override val contactsApi: Contacts, override val isRedacted: Boolean = false ) : Query { override fun toString(): String = query.toString() - override fun redactedCopy() = TestQuery(query.redactedCopy(), isRedacted = true) + override fun redactedCopy() = TestQuery(query.redactedCopy(), contactsApi, isRedacted = true) override fun includeBlanks(includeBlanks: Boolean): TestQuery = apply { query.includeBlanks(includeBlanks)