Skip to content

Commit

Permalink
Closes #154 Standardize interface for CRUD APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
vestrel00 committed Dec 30, 2021
1 parent 334c18e commit 5b804ef
Show file tree
Hide file tree
Showing 40 changed files with 646 additions and 542 deletions.
29 changes: 12 additions & 17 deletions core/src/main/java/contacts/core/BroadQuery.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -444,29 +444,23 @@ 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<Contact>, Redactable {
interface Result : List<Contact>, CrudApi.Result {

// We have to cast the return type because we are not using recursive generic types.
override fun redactedCopy(): Result
}
}

@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<RawContactsField>? = DEFAULT_RAW_CONTACTS_WHERE,
private var groupMembershipWhere: Where<GroupMembershipField>? = DEFAULT_GROUP_MEMBERSHIP_WHERE,
private var include: Include<AbstractDataField> = allDataFields(customDataRegistry),
private var include: Include<AbstractDataField> = contactsApi.includeAllFields(),
private var searchString: String? = DEFAULT_SEARCH_STRING,
private var orderBy: CompoundOrderBy<ContactsField> = DEFAULT_ORDER_BY,
private var limit: Int = DEFAULT_LIMIT,
Expand All @@ -491,10 +485,9 @@ private class BroadQueryImpl(
""".trimIndent()

override fun redactedCopy(): BroadQuery = BroadQueryImpl(
contentResolver, permissions, customDataRegistry,
contactsApi,

includeBlanks,

// Redact Account information.
rawContactsWhere?.redactedCopy(),
groupMembershipWhere,
Expand Down Expand Up @@ -539,7 +532,7 @@ private class BroadQueryImpl(

override fun include(fields: Sequence<AbstractDataField>): BroadQuery = apply {
include = if (fields.isEmpty()) {
allDataFields(customDataRegistry)
contactsApi.includeAllFields()
} else {
Include(fields + REQUIRED_INCLUDE_FIELDS)
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
49 changes: 40 additions & 9 deletions core/src/main/java/contacts/core/Contacts.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
}

/**
Expand All @@ -132,32 +146,39 @@ 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 {

@JvmStatic
@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)
Expand All @@ -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
138 changes: 138 additions & 0 deletions core/src/main/java/contacts/core/CrudApi.kt
Original file line number Diff line number Diff line change
@@ -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<CrudApi.Listener>()

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

0 comments on commit 5b804ef

Please sign in to comment.