contacts-android

repository·main·Indexed 20 days ago

https://github.com/vestrel00/contacts-android

An Android library providing a high-level, type-safe API for interacting with the Android Contacts Provider. It abstracts low-level cursor and database operations, supporting both Kotlin and Java. Key features include a type-safe query DSL with pagination, full CRUD operations for Contacts and RawContacts, and support for all Contacts Provider data types. The library is modular, offering a framework-agnostic core with optional extensions for Kotlin Coroutines (async execution and permissions) and UI components.

Tokens
96.3K
Snippets
322
Records
447
Agent score
70%

What's inside contacts-android

  1. Overview of contacts-android

    main

    The contacts-android library is a comprehensive set of APIs designed to simplify interacting with Android Contacts. It abstracts away the complexities of the [Contacts Provider], database operations, and manual cursor management.

    Developers can use it for simple tasks like fetching specific contact details or for building full-fledged contact management applications with capabilities similar to AOSP or Google Contacts. The library supports both Kotlin and Java, provides full Java interoperability, and maintains a clean separation between Contacts and RawContacts.

  2. Java Support and Dependencies

    main

    The library is designed to be Java-friendly and maintains a minimal dependency footprint.

    • Java Compatibility: The API is written to be usable in Java projects without significant friction.
    • Minimal Dependencies: The core modules avoid adding AndroidX or other external dependencies to keep the library lightweight and avoid dependency bloat.
    • Permissions: Some non-core modules may include a permissions handling library, but the core remains dependency-free.
  3. Manage SIM card contacts

    main

    The library provides dedicated APIs to perform CRUD operations on contacts stored directly on the SIM card. Use the following classes to interact with SIM storage:

    • SimContactsQuery: Read contacts from the SIM.
    • SimContactsInsert: Add new contacts to the SIM.
    • SimContactsUpdate: Modify existing SIM contacts.
    • SimContactsDelete: Remove contacts from the SIM.
  4. Execute inserts with commit vs commitInChunks

    main

    Use .commit() to execute the insertion. Use .commitInChunks() for high-performance bulk inserts.

    MethodBest Use CaseBehavior/Caveats
    .commit()Small amounts of contacts (e.g., 1-20)Each contact is inserted separately. If one fails, others are unaffected.
    .commitInChunks()Large amounts of contacts (e.g., 500+)Much faster (3x-5x). However, if one contact in a chunk fails, other contacts in that same chunk may also fail to insert.

    Note: For a single NewRawContact, both methods behave identically.

  5. Difference between Contacts._ID and Contacts.LOOKUP_KEY

    main

    When working with contacts, it is crucial to distinguish between the unique row ID and the lookup key:

    • Contacts._ID: A unique identifier for a specific row in the Contacts table. This ID may change due to aggregation or synchronization processes.
    • Contacts.LOOKUP_KEY: A unique identifier for an aggregate Contact (a person). While it can also change, it is more stable for finding the aggregate contact and is often consistent across devices for synced accounts.

    Finding contacts via LookupKey: When multiple RawContacts are linked, their lookupKey values are combined, separated by a period (.). Because of this, you can often find a linked contact even if you only have the lookupKey of one of its constituent RawContacts by using a contains query:

    .where { Contact.LookupKey contains lookupKey }

    Assumption: This works provided the lookup key is unique and no other lookup key is a substring of a valid one (which is generally true for the pattern of synced vs. local keys).

  6. Understand multiple RawContacts per Contact

    main

    A single row in the Contacts table can be associated with multiple rows in the RawContacts table. This occurs when the Contacts Provider consolidates multiple contacts from different accounts into a single aggregate entry, or when contacts have been "linked", "merged", or "joined".

    Key Concepts:

    • Aggregation: The process of combining contacts from different accounts into one Contacts entry while maintaining separate RawContacts entries.
    • Terminology Evolution:
      • API 22 and below: join / separate
      • API 23: merge / unmerge
      • API 24 and above: link / unlink
    • Internal Mechanism: Regardless of the API version, the underlying operations use KEEP_TOGETHER and KEEP_SEPARATE via ContactsContract.AggregationExceptions.
  7. Manage Primary and Super Primary (Default) data rows

    main

    In the Contacts Provider, data rows (like emails) can have IS_PRIMARY and IS_SUPER_PRIMARY statuses.

    • Primary: There should be only one primary data row of a specific mimetype per RawContact.
    • Super Primary (Default): There should be only one super primary data row of a specific mimetype per aggregate Contact. A super primary row must also be a primary row.

    Important Implementation Details

    • Validation: The Contacts Provider does not automatically validate these columns. It is possible to have multiple primary rows for a RawContact or a super primary row that is not primary. Developers must uphold this contract manually.
    • Behavior during Linking: When linking RawContacts into a single Contact, setting a new 'default' (super primary) for the aggregate Contact will change the status of existing rows. For example, if Email C becomes the super primary for the aggregate Contact, Email B (which was primary for its own RawContact) remains primary but loses its super primary status.
    • Unlinking: When unlinking RawContacts, the library follows the AOSP pattern of retaining the primary status of data rows.
  8. Redact sensitive contact data

    main
    All Entity objects in this library implement the Redactable interface. This is intended for use when logging contact data to remote servers (e.g., for analytics or crash reporting) to ensure sensitive user information is removed for legal and privacy compliance. Refer to the documentation on redacting APIs and entities for implementation details.
  9. Important considerations for deleting groups

    main

    Permissions

    Deletions require the android.permission.WRITE_CONTACTS permission. If the permission is not granted, the operation will do nothing and return a failed result. Use the permissions module extensions to handle this.

    Read-only Groups

    System-created groups are typically read-only and cannot be deleted. Common examples include:

    • systemId: Contacts, title: My Contacts
    • systemId: null, title: Starred in Android
    • systemId: Friends, title: Friends
    • systemId: Family, title: Family
    • systemId: Coworkers, title: Coworkers

    Deletion Lifecycle

    • Group Memberships: When a group is deleted, any memberships to that group are immediately deleted by the Contacts Provider.
    • Group Deletion: The groups themselves may not be deleted immediately. They are marked for deletion and processed in the background by the Contacts Provider based on sync settings and network availability.
  10. Redact entities and API input/output for privacy compliance

    main

    All Entity objects and CRUD APIs (Create/Query, Read/Query, Update, and Delete) in this library are Redactable. This feature allows you to strip sensitive private user data (e.g., replacing email addresses or phone numbers with asterisks) before logging data to remote servers for analytics or crash reporting, helping to comply with privacy guidelines like GDPR.

    Key Characteristics of Redaction:

    • String Replacement: Sensitive string data is replaced with * characters. The length of the original string and whether it was null is preserved.
    • Non-string Data: Database row IDs and typically non-string properties are generally not redacted unless they contain sensitive information.
    • isRedacted Flag: Redacted copies have their isRedacted property set to true to indicate the data has been modified.
    • redactedCopy(): This function returns a new instance of the entity that is a copy of the original, but with sensitive fields redacted. This can be used for both logging and creating UI views that hide sensitive information.
    // Example of how an entity looks when redacted
    // Original: Contact: id=1, email { address="vestrel00@gmail.com" }
    // Redacted: Contact: id=1, email { address="*******************" }
  11. Avoid UI choppiness by performing SIM deletes asynchronously

    main

    By default, commit() and commitInOneTransaction() execute on the same thread as the call-site. Because SIM operations can be slow, calling these on the main thread may cause UI stuttering.

    To prevent this, use the Kotlin coroutine extensions provided in the async module to run the deletion on a background thread.

  12. Understand the Immutable vs Mutable Entity model

    main

    The library uses a strict separation between immutable and mutable entities to ensure data integrity and thread safety.

    Immutable Entities

    • Guaranteed Immutability: All properties are defined with val and use immutable collection types (e.g., List instead of MutableList).
    • Thread Safety: Immutable entities are inherently thread-safe and can be shared across multiple threads without synchronization.
    • Usage: Use these for reading data and performing operations that do not require modification.

    Mutable Entities

    • Not Thread-Safe: Consumers are responsible for performing their own synchronization if using mutable entities in multi-threaded scenarios.
    • Usage: Use these when you need to modify contact data.

    Exhaustive Handling

    Because entities are organized under sealed interface hierarchies (e.g., ContactEntity), you can use Kotlin's when expression to handle both types exhaustively:

    fun handleContact(contact: ContactEntity) = when (contact) {
        is Contact -> { /* Handle immutable contact */ }
        is MutableContact -> { /* Handle mutable contact */ }
    }
    sealed interface ContactEntity {
        val rawContacts: List<RawContactEntity>
    }
    data class Contact(
        override val rawContacts: List<RawContact>
    ) : ContactEntity
    data class MutableContact(
        override val rawContacts: List<MutableRawContact>
    ) : ContactEntity
    
    sealed interface RawContactEntity
    data class RawContact(
        val addresses: List<Address>
    ) : RawContactEntity
    data class MutableRawContact(
        val addresses: MutableList<MutableAddress>
    ) : MutableRawContact
    
    data class Address(
        val formattedAddress: String?
    )
    data class MutableAddress(
        var formattedAddress: String?
    )