diff --git a/accounts/associate-device-local-raw-contacts-to-an-account/index.html b/accounts/associate-device-local-raw-contacts-to-an-account/index.html index 9e2a4fc3..6eafd316 100644 --- a/accounts/associate-device-local-raw-contacts-to-an-account/index.html +++ b/accounts/associate-device-local-raw-contacts-to-an-account/index.html @@ -1983,8 +1983,8 @@

Associate a local RawContact
val accountsLocalRawContactsUpdate = Contacts(context).accounts().updateLocalRawContactsAccount()
 
-

For more info on local RawContacts, read about Local (device-only) contacts.

-

For more info on syncing, read Sync contact data across devices.

+

ℹ️ For more info on local RawContacts, read about Local (device-only) contacts.

+

ℹ️ For more info on syncing, read Sync contact data across devices.

Basic usage

To associate/add the given local RawContacts to the given account,

@@ -2037,7 +2037,7 @@

Performing t read Execute work outside of the UI thread using coroutines.

You may, of course, use other multi-threading libraries or just do it yourself =)

-

Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

+

ℹ️ Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

Performing the update with permission

These updates require the android.permission.GET_ACCOUNTS and android.permission.WRITE_CONTACTS. @@ -2053,11 +2053,6 @@

Profile dataAll updates will be limited to the Profile RawContacts, whether it exists or not.


Developer notes (or for advanced users)

-
-

The following section are note from developers of this library for other developers. It is copied -from the DEV_NOTES. You may still read the following as a consumer of the library -in case you need deeper insight.

-

Due to certain limitations and behaviors imposed by the Contacts Provider, this library only provides an API to support;

-

Please update the above list whenever adding new custom data modules.

+

ℹ️ Please update the above list whenever adding new custom data modules.

diff --git a/customdata/integrate-gender-custom-data/index.html b/customdata/integrate-gender-custom-data/index.html index fdcedc9c..973daa6f 100644 --- a/customdata/integrate-gender-custom-data/index.html +++ b/customdata/integrate-gender-custom-data/index.html @@ -1871,7 +1871,7 @@

Integrate the gender custom dataThis library provides extensions for Gender custom data that allows you to read and write gender data for all of your contacts. These (optional) extensions live in the customdata-gender module.

-

If you are looking to create your own custom data or get more insight on how the Gender custom +

ℹ️ If you are looking to create your own custom data or get more insight on how the Gender custom data was built, read Integrate custom data.

Register the gender custom data with the Contacts API instance

diff --git a/customdata/integrate-googlecontacts-custom-data/index.html b/customdata/integrate-googlecontacts-custom-data/index.html index 0d2c8157..501ed0e0 100644 --- a/customdata/integrate-googlecontacts-custom-data/index.html +++ b/customdata/integrate-googlecontacts-custom-data/index.html @@ -1940,7 +1940,7 @@

Integrate the Google Contacts FileAs and UserDefined, which allows you to read and write Google Contacts data for all of your contacts. These (optional) extensions live in the customdata-googlecontacts module.

-

If you are looking to create your own custom data or get more insight on how the FileAs and +

ℹ️ If you are looking to create your own custom data or get more insight on how the FileAs and UserDefined custom data was built, read Integrate custom data.

Register the Google Contacts custom data with the Contacts API instance

@@ -2034,14 +2034,14 @@

Google Contacts app UI
-

For more info on local contacts, read about Local (device-only) contacts.

+

ℹ️ For more info on local contacts, read about Local (device-only) contacts.

Syncing Google Contacts custom data

The Google Contacts app comes with sync adapters that is responsible for syncing FileAs and UserDefined custom data. As long as you have the Google Contacts app installed, these custom data should remain synced depending on account sync settings.

-

This library does not provide sync adapters for Google Contacts custom data.

+

ℹ️ This library does not provide sync adapters for Google Contacts custom data.

For more info, read Sync contact data across devices.

diff --git a/customdata/integrate-handlename-custom-data/index.html b/customdata/integrate-handlename-custom-data/index.html index 17dd3f87..eee32fa0 100644 --- a/customdata/integrate-handlename-custom-data/index.html +++ b/customdata/integrate-handlename-custom-data/index.html @@ -1872,7 +1872,7 @@

Integrate the handle name custom handle name data for all of your contacts. These (optional) extensions live in the customdata-handlename module.

-

If you are looking to create your own custom data or get more insight on how the HandleName +

ℹ️ If you are looking to create your own custom data or get more insight on how the HandleName custom data was built, read Integrate custom data.

Register the handle name custom data with the Contacts API instance

diff --git a/customdata/integrate-pokemon-custom-data/index.html b/customdata/integrate-pokemon-custom-data/index.html index a5262c80..a4da0847 100644 --- a/customdata/integrate-pokemon-custom-data/index.html +++ b/customdata/integrate-pokemon-custom-data/index.html @@ -1872,7 +1872,7 @@

Integrate the Pokemon custom datacustomdata-pokemon module.

-

If you are looking to create your own custom data or get more insight on how the Pokemon +

ℹ️ If you are looking to create your own custom data or get more insight on how the Pokemon custom data was built, read Integrate custom data.

Register the pokemon custom data with the Contacts API instance

diff --git a/customdata/integrate-rpg-custom-data/index.html b/customdata/integrate-rpg-custom-data/index.html index 1294f215..f20664dc 100644 --- a/customdata/integrate-rpg-custom-data/index.html +++ b/customdata/integrate-rpg-custom-data/index.html @@ -1912,7 +1912,7 @@

Integrate the Role Play write rpg data for all of your contacts. These (optional) extensions live in the customdata-rpg module.

-

If you are looking to create your own custom data or get more insight on how the RpgStats and +

ℹ️ If you are looking to create your own custom data or get more insight on how the RpgStats and RpgProfession custom data was built, read Integrate custom data.

Register the RPG custom data with the Contacts API instance

diff --git a/customdata/query-custom-data/index.html b/customdata/query-custom-data/index.html index 71ac499a..8cfa4daa 100644 --- a/customdata/query-custom-data/index.html +++ b/customdata/query-custom-data/index.html @@ -1904,14 +1904,14 @@

Query custom dataIntegrate the gender custom data +

ℹ️ For more info, read Integrate the gender custom data and Integrate the handle name custom data.

Getting custom data from a Contact or RawContact

Custom data, just like regular data kinds, are attached to a RawContact. They follow the same rules as regular data kinds.

-

For more info, read about API Entities.

+

ℹ️ For more info, read about API Entities.

For example, you are able to get the handle names and gender of a RawContact,

val handleNames = rawContact.handleNames(contactsApi)
diff --git a/customdata/update-custom-data/index.html b/customdata/update-custom-data/index.html
index b252e28e..c47488b0 100644
--- a/customdata/update-custom-data/index.html
+++ b/customdata/update-custom-data/index.html
@@ -1886,13 +1886,13 @@ 

Update custom dataIntegrate custom data.

+

ℹ️ For more info about custom data, read Integrate custom data.

Updating custom data via Contacts/RawContacts

Custom data, just like regular data kinds, are attached to a RawContact. They follow the same rules as regular data kinds.

-

For more info, read about API Entities.

+

ℹ️ For more info, read about API Entities.

For example, you are able to update existing handle names and the gender of an existing RawContact,

mutableRawContact.handleNames(contactsApi).firstOrNull()?.apply {
diff --git a/data/delete-data-sets/index.html b/data/delete-data-sets/index.html
index 045dc3d3..b83f091b 100644
--- a/data/delete-data-sets/index.html
+++ b/data/delete-data-sets/index.html
@@ -1928,9 +1928,8 @@ 

Delete existing sets of data
val delete = Contacts(context).data().delete()
 

-

To delete all kinds of data via Contacts/RawContacts, you may remove them from the -Contact/RawContact and then perform an update. -For more info, read Update contacts.

+

ℹ️ To delete all kinds of data via Contacts/RawContacts, you may remove them from the +Contact/RawContact and then perform an update. For more info, read Update contacts.

A basic delete

To delete a set of data,

@@ -1972,7 +1971,7 @@

Performing t For more info, read Execute work outside of the UI thread using coroutines.

You may, of course, use other multi-threading libraries or just do it yourself =)

-

Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

+

ℹ️ Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

Performing the delete with permission

Deletes require the android.permission.WRITE_CONTACTS permissions. If not granted, the delete diff --git a/data/insert-data-sets/index.html b/data/insert-data-sets/index.html index fd5fcd92..40b0b907 100644 --- a/data/insert-data-sets/index.html +++ b/data/insert-data-sets/index.html @@ -1839,7 +1839,7 @@

Insert data into new or exist .commit()

-

For more info, read Insert contacts.

+

ℹ️ For more info, read Insert contacts.

To insert an email into a new Profile contact using the ProfileInsert API,

Contacts(context)
@@ -1851,7 +1851,7 @@ 

Insert data into new or exist .commit()

-

For more info, read Insert device owner Contact profile.

+

ℹ️ For more info, read Insert device owner Contact profile.

To insert an email into an existing contact using the Update API,

Contacts(context)
@@ -1862,7 +1862,7 @@ 

Insert data into new or exist .commit()

-

For more info, read Update contacts.

+

ℹ️ For more info, read Update contacts.

To insert an email into an the existing Profile Contact using the ProfileUpdate API,

Contacts(context)
@@ -1874,7 +1874,7 @@ 

Insert data into new or exist .commit()

-

For more info, read Update device owner Contact profile.

+

ℹ️ For more info, read Update device owner Contact profile.

Blank data are not inserted

Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are diff --git a/data/query-data-sets/index.html b/data/query-data-sets/index.html index 60a6a560..3d2b55cf 100644 --- a/data/query-data-sets/index.html +++ b/data/query-data-sets/index.html @@ -2012,8 +2012,7 @@

Query specific data kinds
val query = Contacts(context).data().query()
 
-

To retrieve all kinds of data via Contacts/RawContacts, read -Query contacts +

ℹ️ To retrieve all kinds of data via Contacts/RawContacts, read Query contacts and Query contacts (advanced).

Data queries

@@ -2057,7 +2056,7 @@

Specifying Accounts
.accounts(Account("john.doe@gmail.com", "com.google"))
 
-

For more info, read Query for Accounts.

+

ℹ️ For more info, read Query for Accounts.

If no accounts are specified (this function is not called or called with no Accounts), then all data are included in the search.

@@ -2066,7 +2065,7 @@

Specifying AccountsLocal (device-only) contacts.

-

Note that this may affect performance. This may require one or more additional queries, internally +

ℹ️ This may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it.

@@ -2099,8 +2098,8 @@

Limiting and offsetting

This is useful for pagination =)

-

Note that it is recommended to limit the number of data when querying to increase performance -and decrease memory cost.

+

ℹ️ It is recommended to limit the number of data when querying to increase performance and +decrease memory cost.

Executing the query

To execute the query,

@@ -2130,7 +2129,7 @@

Performing the query asynchronously For more info, read Execute work outside of the UI thread using coroutines.

You may, of course, use other multi-threading libraries or just do it yourself =)

-

Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for +

ℹ️ Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes.

Performing the query with permission

@@ -2152,8 +2151,8 @@

Using the w

Use the corresponding contacts.core.Fields combined with the extensions from contacts.core.Where to form WHERE clauses.

-

This docs page will not provide a tutorial on database where clauses. It assumes that you know the basics. -If you don't know the basics, then search for sqlite where clause.

+

ℹ️ This docs page will not provide a tutorial on database where clauses. It assumes that you know +the basics. If you don't know the basics, then search for sqlite where clause.

For example, to get all nicknames from all contacts,

val nicknames = Contacts(context).data().query().nicknames().find()
diff --git a/data/update-data-sets/index.html b/data/update-data-sets/index.html
index c95f4069..6f54e7cc 100644
--- a/data/update-data-sets/index.html
+++ b/data/update-data-sets/index.html
@@ -1964,13 +1964,14 @@
 
 
 

Update existing sets of data

-

This library provides the DataUpdate API that allows you to update a list of any data kinds -directly without having to update them via Contacts/RawContacts.

+

This library provides the DataUpdate API that allows you to update a list of any data kinds +in the Contacts Provider database directly without having to update them via Contacts/RawContacts. +This ensures that the Contacts Provider database contains the same data you have in memory.

An instance of the DataUpdate API is obtained by,

val update = Contacts(context).data().update()
 
-

To update all kinds of data via Contacts/RawContacts, read Update contacts.

+

ℹ️ To update all kinds of data via Contacts/RawContacts, read Update contacts.

A basic update

To update a set of data,

@@ -1989,13 +1990,13 @@

A basic updateBlank data are deleted

Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are -deleted by update APIs.

+deleted by update APIs, unless the corresponding fields are not included in the operation.

For more info, read about Blank data.

Including only specific data

-

To include only the given set of fields (data) in each of the update operation,

+

To perform update operations only the given set of fields (data),

.include(fields)
 
-

For example, to only include email and name fields,

+

For example, to perform updates on only email and name fields,

.include { Email.all + Name.all }
 

For more info, read Include only certain fields for read and write operations.

@@ -2031,7 +2032,7 @@

Handling the update result .find()

-

For more info, read Query specific data kinds.

+

ℹ️ For more info, read Query specific data kinds.

Alternatively, you may use the extensions provided in DataRefresh.

To get the updated phone,

@@ -2058,7 +2059,7 @@

Performing t For more info, read Execute work outside of the UI thread using coroutines.

You may, of course, use other multi-threading libraries or just do it yourself =)

-

Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

+

ℹ️ Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

Performing the update with permission

Updates require the android.permission.WRITE_CONTACTS permissions. If not granted, the update diff --git a/dev-notes/index.html b/dev-notes/index.html index 3cf12ebf..9dfa8fea 100644 --- a/dev-notes/index.html +++ b/dev-notes/index.html @@ -2288,9 +2288,6 @@

Developer NotesContacts Provider / ContactsContract

It is important to know about the ins and outs of Android's Contacts Provider. After all, this API is just a wrapper around it.

-
-

A very sweet, sugary wrapper! Sugar. Spice. And everything nice. :D

-

It is important to get familiar with the official documentation of the Contact's Provider.

Here is a summary;

There are 3 main database tables used in dealing with contacts;

@@ -2300,7 +2297,7 @@

Contacts Provider / ContactsContract
  • Data
  • -

    There are more but that is covered later.

    +

    ℹ️ There are more but that is covered later.

    All of these tables and their fields are enumerated and documented in android.provider.ContactsContract.

    @@ -2355,7 +2352,7 @@

    Contacts; Display Name
    -

    In the case of StructuredName, the Contacts.DISPLAY_NAME is made up of the prefix, given, +

    ℹ️ In the case of StructuredName, the Contacts.DISPLAY_NAME is made up of the prefix, given, middle, family name, and suffix and not the unstructured display name.

    If no data rows suitable to be a display name are available, then the Contacts row display name will @@ -2398,7 +2395,7 @@

    Contact Display Name and Def automatically set as the Contact display name by the Contacts Provider. Otherwise, the Contacts Provider chooses from any of the other suitable data from the aggregate Contact.

    -

    The ContactsColumns.NAME_RAW_CONTACT_ID is automatically updated by the Contacts Provider +

    ℹ️ The ContactsColumns.NAME_RAW_CONTACT_ID is automatically updated by the Contacts Provider along with the display name.

    The default status of other sources (e.g. email) does not affect the Contact display name.

    @@ -2425,7 +2422,7 @@

    Contacts; ID vs LOOKUP_KEY
    #### Contacts table
    @@ -2599,9 +2596,9 @@ 

    Contacts; ID vs LOOKUP_KEYℹ️ As mentioned earlier in this section, the "55" in "0r55-" seems to be referencing the +RawContact ID. In other words, since local RawContacts are not synced or tracked in a remote +database where Contacts -> RawContacts mappings exist, the Contacts Provider most likely uses this "0r-" pattern to make the connection. This is not really relevant for us as we are not relying on this mechanism. I'm just pointing out my observations, which could be incorrect.

    @@ -2647,7 +2644,7 @@

    Contacts; ID vs LOOKUP_KEY
    -

    The RawContact and its Data also remained the same in this case.

    +

    ℹ️ The RawContact and its Data also remained the same in this case.

    Removing the account from it results in...

    #### Contacts table
    @@ -2669,8 +2666,8 @@ 

    RawContacts; Accounts + Contacts0 or more rows in the Data table with a reference to the new Contacts and RawContacts Ids
    -

    It is possible to create RawContacts without any rows in the Data table. See the Data required -section for more details.

    +

    ℹ️ It is possible to create RawContacts without any rows in the Data table. See the +Data required section for more details.

    For example, creating 4 new contacts using the native Android Contacts app results in;

    Contact id: 4, displayName: First Local Contact
    @@ -2806,7 +2803,7 @@ 

    RawContacts; DeletionBehavi done by the native Contacts app manually by setting Contact Y's Data name row to be the "default" (isPrimary and isSuperPrimary both set to 1).

    -

    The AggregationExceptions table records the linked RawContacts's IDs in ascending order regardless -of the order used in RAW_CONTACT_ID1 and RAW_CONTACT_ID2 at the time of merging.

    +

    ℹ️ The AggregationExceptions table records the linked RawContacts' IDs in ascending order +regardless of the order used in RAW_CONTACT_ID1 and RAW_CONTACT_ID2 at the time of merging.

    The RawContacts and Data table remains the same except the joined contactId column values have now been changed to the id of Contact X. All Data rows' isSuperPrimary value has been set to 0 though @@ -2889,7 +2886,7 @@

    Behavi For more info on Contact display name resolution, read the Contact Display Name and Default Name Rows section.

    -

    Note that display name resolution is different for APIs below 21 (pre-lollipop).

    +

    ℹ️ Display name resolution is different for APIs below 21 (pre-lollipop).

    The display name of the RawContacts remain the same.

    The Groups table remains unmodified.

    @@ -2902,17 +2899,16 @@

    Behavi the Data table in a photo mimetype row. A Contact's full-sized photo and thumbnail are simply references to the "chosen" RawContact's full-sized photo and thumbnail (though the URIs may differ).

    -

    Note that when removing the photo in the native contacts app, the photo data row is not -immediately deleted, though the PHOTO_FILE_ID is immediately set to null. This may result in -the PHOTO_URI and PHOTO_THUMBNAIL_URI to still have a valid image uri even though the photo -has been "removed". This library immediately deletes the photo data row, which seems to work -perfectly.

    +

    ℹ️ When removing the photo in the native contacts app, the photo data row is not immediately +deleted, though the PHOTO_FILE_ID is immediately set to null. This may result in the PHOTO_URI +and PHOTO_THUMBNAIL_URI to still have a valid image uri even though the photo has been +"removed". This library immediately deletes the photo data row, which seems to work perfectly.

    Data inserts

    In the native Contacts app, Data inserted in combined (raw) contacts mode will be associated to the first RawContact in the list sorted by the RawContact ID.

    -

    This may not be the same as the RawContact referenced by ContactsColumns.NAME_RAW_CONTACT_ID.

    +

    ℹ️ This may not be the same as the RawContact referenced by ContactsColumns.NAME_RAW_CONTACT_ID.

    UI changes?

    The native Contacts App does not display the groups field when displaying / editing Contacts that @@ -3238,7 +3234,7 @@

    Data Primary and Super Primary Rows

    The above behavior is observed from the native Contacts app. The "super primary" data of an aggregate Contact is referred to as the "default".

    -

    At this point, the native Contacts app still shows email B as the first email in the list even +

    ℹ️ At this point, the native Contacts app still shows email B as the first email in the list even though it isn't the "default" (super primary) because it is still a primary. This adds a bit of confusion in my opinion, especially when more than 2, 3, or 4 RawContacts are linked. A "fix" would be to only order the list of emails using "super primary" instead of "super primary" and @@ -3278,7 +3274,7 @@

    Data RequiredNote, underlying value defaults to null
    -

    Note that all of the above rows are only automatically created for RawContacts that are associated +

    ℹ️ All of the above rows are only automatically created for RawContacts that are associated with an Account.

    If a valid account is provided, the default (auto add) system group membership row is automatically @@ -3355,7 +3351,7 @@

    Groups Table & Accounts
    -

    In newer versions, the group with the duplicate title gets deleted either automatically by the +

    ℹ️ In newer versions, the group with the duplicate title gets deleted either automatically by the Contacts Provider or when viewing groups in the native Contacts app. It's not an immediate failure on insert or update. This could lead to bugs!

    @@ -3398,7 +3394,7 @@

    Groups; DeletionUser Profile ContactsContract.isProfileId.

    -

    Note that the Contacts Provider will throw an IllegalArgument exception when attempting to include +

    ℹ️ The Contacts Provider will throw an IllegalArgument exception when attempting to include ContactsColumns.IS_USER_PROFILE and RawContactsColumns.RAW_CONTACT_IS_USER_PROFILE columns in Data table queries. I have not yet tried including these columns in the Contacts or RawContacts table queries.

    @@ -3459,7 +3455,7 @@

    User Profile
    -

    From my experience, profile RawContacts associated to an Account is not carried over / synced +

    ℹ️ From my experience, profile RawContacts associated to an Account is not carried over / synced across devices or users.

    Despite the documentation of "one profile RawContact per one Account", the Contacts Provider allows @@ -3531,7 +3527,7 @@

    Creating Entities & data classcopy method as it may lead to unwanted side effects when updating and deleting contacts.

    -

    We could just use regular classes instead of data classes but entities should be data classes +

    ℹ️ We could just use regular classes instead of data classes but entities should be data classes because it is what they are (know what I mean?!). Also, I'd hate to have to generate equals and hashcode functions for them, which will make the code harder to maintain. Though, we might do this anyways at some point if we want to make it possible for a mutable entity to equal an immutable @@ -3568,7 +3564,7 @@

    Immutable vs Mutable Entities)

    -

    Note the use of sealed class is to prevent consumers from defining their own entities. This +

    ℹ️ The use of sealed class is to prevent consumers from defining their own entities. This restriction may or may not change in the future.

    Notice that there is nothing mutable in the immutable Contact. Everything are vals and the data @@ -3588,8 +3584,8 @@

    Immutable vs Mutable Entities}

    -

    Note that the mutable entities provided in this library are NOT thread-safe. Consumers will -have to perform their own synchronizations if they want to use and mutate mutable entities in +

    ℹ️ The mutable entities provided in this library are NOT thread-safe. Consumers will have to +perform their own synchronizations if they want to use and mutate mutable entities in multi-threaded scenarios.

    The cost of the current immutability implementation

    @@ -3633,10 +3629,10 @@

    Avoiding the cost... Shortcuts

    Notice that there is a non-concrete declaration (i.e. Contact, RawContact, and Address) and just one concrete implementation (i.e. MutableContact, MutableRawContact, and MutableAddress).

    -

    Note that a val declaration can be overridden by a var. Keep in mind that val only requires -getters whereas var requires both getters and setters. Therefore, a var cannot be overridden -by a val. Or maybe there is a different reason Kotlin imposes this restriction =) -On a similar note, the List interface can be overridden to a MutableList.

    +

    ℹ️ A val declaration can be overridden by a var. Keep in mind that val only requires getters +whereas var requires both getters and setters. Therefore, a var cannot be overridden by a +val. Or maybe there is a different reason Kotlin imposes this restriction. On a similar note, +the List interface can be overridden to a MutableList.

    We, as API contributors, can avoid having to write seemingly duplicate functions and extensions!

    However! Can you see what's wrong with this setup? If we do this, we would either be deceiving diff --git a/entities/about-api-entities/index.html b/entities/about-api-entities/index.html index 230bddf3..69926841 100644 --- a/entities/about-api-entities/index.html +++ b/entities/about-api-entities/index.html @@ -384,6 +384,19 @@ Contacts API Entities +

    +
  • @@ -398,13 +411,6 @@ Data kinds Account restrictions -
  • - -
  • - - Automatic data kinds creation - -
  • @@ -433,6 +439,26 @@ Syncing contact data +
  • + +
  • + + Developer notes (or for advanced users) + + + +
  • @@ -1868,6 +1894,19 @@ Contacts API Entities + +
  • @@ -1882,13 +1921,6 @@ Data kinds Account restrictions -
  • - -
  • - - Automatic data kinds creation - -
  • @@ -1917,6 +1949,26 @@ Syncing contact data +
  • + +
  • + + Developer notes (or for advanced users) + + + +
  • @@ -1963,7 +2015,7 @@

    Contacts Provider / Co
    -

    There are more tables but it won't be covered in this docs for brevity.

    +

    ℹ️ There are more tables but it won't be covered in this docs for brevity.

    In the example given (E.G.) above,

      @@ -2023,10 +2075,40 @@

      Contacts API EntitiesIntegrate custom data.

      -

      Default native and custom data may be retrieved, set, or cleared. -For more info, read Get set clear default Contact data.

      +

      ℹ️ Custom data kinds may also be integrated into the contacts database (though not synced across +devices). For more info, read Integrate custom data.

      +

      ℹ️ Default native and custom data may be retrieved, set, or cleared. For more info, read +Get set clear default Contact data.

      + +

      Contacts API Fields

      +

      The fields defined in contacts.core.Fields.kt specify what properties of entities to include in +read and write operations. For example, to include only the contact display name, organization +company, and all phone number fields in a query/insert/update operation,

      +
      queryInsertUpdate.include(mutableSetOf<AbstractDataField>().apply {
      +    add(Fields.Contact.DisplayNamePrimary)
      +    add(Fields.Organization.Company)
      +    addAll(Fields.Phone.all)
      +})
      +
      +

      The following entity properties are are used in the read/write operation,

      +
      Contact {
      +    displayNamePrimary
      +
      +    RawContact {
      +        organization {
      +            company
      +        }
      +        phones {
      +            number
      +            normalizedNumber
      +            type
      +            label
      +        }
      +    }
      +}
      +
      +
      +

      ℹ️ For more info, read Include only certain fields for read and write operations.

      Data kinds count restrictions

      A RawContact may have at most one OR no limits of certain kinds of data.

      @@ -2059,27 +2141,6 @@

      Data kinds Account restrictionsEntries of some data kinds should not be allowed to exist for local RawContacts (those that are not associated with an Account).

      For more info, read about Local (device-only) contacts.

      -

      Automatic data kinds creation

      -

      An entry of each of the following data kinds are automatically created for all contacts, if not -provided;

      -
        -
      • GroupMembership, underlying value defaults to the account's default system group
      • -
      • Name, underlying value defaults to null
      • -
      • Nickname, underlying value defaults to null
      • -
      • Note, underlying value defaults to null
      • -
      -

      This automatic creation occur automatically in the background (typically after creation) only for -RawContacts that are associated with an Account. If a valid account is provided, membership to the -(auto add) system group is automatically created immediately by the Contacts Provider at the time of -creation. The name, nickname, and note are automatically created at a later time.

      -
      -

      Note that the query APIs in this library do not return blanks in results. In this case, the Name, -Nickname, and Note will not be included in the RawContact because their primary values are all -null. Blanks are also ignored on insert and deleted on update. -For more info, read about Blank data.

      -
      -

      If a valid account is not provided, no entries of the above are automatically created.

      -

      To determine if a RawContact is associated with an Account or not, read Query for Accounts.

      Data integrity

      There is a section in the official Contacts Provider documentation about "Data Integrity"; https://developer.android.com/guide/topics/providers/contacts-provider#DataIntegrity

      @@ -2108,92 +2169,94 @@

      Data integrityIntegrate custom data from other apps.

      Accessing contact data

      -

      When you have an instance of Contact, you have complete (and correct) access to data stored in it.

      +

      When you have an instance of Contact, you have complete access to data stored in it.

      To access data of a Contact with only one RawContact,

      -
      val contact: Contact
      -val rawContact: RawContact = contact.rawContacts.first()
      -Log.d(
      -    "Contact",
      -    """
      -        ID: ${contact.id}
      -        Lookup Key: ${contact.lookupKey}
      -
      -        Display name: ${contact.displayNamePrimary}
      -        Display name alt: ${contact.displayNameAlt}
      -
      -        Photo Uri: ${contact.photoUri}
      -        Thumbnail Uri: ${contact.photoThumbnailUri}
      -
      -        Last updated: ${contact.lastUpdatedTimestamp}
      -
      -        Starred?: ${contact.options?.starred}
      -        Send to voicemail?: ${contact.options?.sendToVoicemail}
      -        Ringtone: ${contact.options?.customRingtone}
      -
      -        Addresses: ${rawContact.addresses}
      -        Emails: ${rawContact.emails}
      -        Events: ${rawContact.events}
      -        Group memberships: ${rawContact.groupMemberships}
      -        IMs: ${rawContact.ims}
      -        Name: ${rawContact.name}
      -        Nickname: ${rawContact.nickname}
      -        Note: ${rawContact.note}
      -        Organization: ${rawContact.organization}
      -        Phones: ${rawContact.phones}
      -        Relations: ${rawContact.relations}
      -        SipAddress: ${rawContact.sipAddress}
      -        Websites: ${rawContact.websites}
      -    """.trimIndent()
      -    // Photo require separate blocking function calls.
      -)
      +
      val contact: Contact
      +val rawContact: RawContact = contact.rawContacts.first()
      +Log.d(
      +    "Contact",
      +    """
      +        ID: ${contact.id}
      +        Lookup Key: ${contact.lookupKey}
      +
      +        Display name: ${contact.displayNamePrimary}
      +        Display name alt: ${contact.displayNameAlt}
      +
      +        Photo Uri: ${contact.photoUri}
      +        Thumbnail Uri: ${contact.photoThumbnailUri}
      +
      +        Last updated: ${contact.lastUpdatedTimestamp}
      +
      +        Starred?: ${contact.options?.starred}
      +        Send to voicemail?: ${contact.options?.sendToVoicemail}
      +        Ringtone: ${contact.options?.customRingtone}
      +
      +        Addresses: ${rawContact.addresses}
      +        Emails: ${rawContact.emails}
      +        Events: ${rawContact.events}
      +        Group memberships: ${rawContact.groupMemberships}
      +        IMs: ${rawContact.ims}
      +        Name: ${rawContact.name}
      +        Nickname: ${rawContact.nickname}
      +        Note: ${rawContact.note}
      +        Organization: ${rawContact.organization}
      +        Phones: ${rawContact.phones}
      +        Relations: ${rawContact.relations}
      +        SipAddress: ${rawContact.sipAddress}
      +        Websites: ${rawContact.websites}
      +    """.trimIndent()
      +    // Photo require separate blocking function calls.
      +)
       
      -

      To access data of a Contact with possibly more than one RawContact, we can use ContactData +

      To access data of a Contact with possibly more than one RawContact, we can use ContactData.kt extensions to make our life easier,

      -
      val contact: Contact
      -Log.d(
      -    "Contact",
      -    """
      -        ID: ${contact.id}
      -        Lookup Key: ${contact.lookupKey}
      -
      -        Display name: ${contact.displayNamePrimary}
      -        Display name alt: ${contact.displayNameAlt}
      -
      -        Photo Uri: ${contact.photoUri}
      -        Thumbnail Uri: ${contact.photoThumbnailUri}
      -
      -        Last updated: ${contact.lastUpdatedTimestamp}
      -
      -        Starred?: ${contact.options?.starred}
      -        Send to voicemail?: ${contact.options?.sendToVoicemail}
      -        Ringtone: ${contact.options?.customRingtone}
      -
      -        Aggregate data from all RawContacts of the contact
      -        -----------------------------------
      -        Addresses: ${contact.addressList()}
      -        Emails: ${contact.emailList()}
      -        Events: ${contact.eventList()}
      -        Group memberships: ${contact.groupMembershipList()}
      -        IMs: ${contact.imList()}
      -        Names: ${contact.nameList()}
      -        Nicknames: ${contact.nicknameList()}
      -        Notes: ${contact.noteList()}
      -        Organizations: ${contact.organizationList()}
      -        Phones: ${contact.phoneList()}
      -        Relations: ${contact.relationList()}
      -        SipAddresses: ${contact.sipAddressList()}
      -        Websites: ${contact.websiteList()}
      -        -----------------------------------
      -    """.trimIndent()
      -    // There are also aggregate data functions that return a sequence instead of a list.
      -)
      +
      val contact: Contact
      +Log.d(
      +    "Contact",
      +    """
      +        ID: ${contact.id}
      +        Lookup Key: ${contact.lookupKey}
      +
      +        Display name: ${contact.displayNamePrimary}
      +        Display name alt: ${contact.displayNameAlt}
      +
      +        Photo Uri: ${contact.photoUri}
      +        Thumbnail Uri: ${contact.photoThumbnailUri}
      +
      +        Last updated: ${contact.lastUpdatedTimestamp}
      +
      +        Starred?: ${contact.options?.starred}
      +        Send to voicemail?: ${contact.options?.sendToVoicemail}
      +        Ringtone: ${contact.options?.customRingtone}
      +
      +        Aggregate data from all RawContacts of the contact
      +        -----------------------------------
      +        Addresses: ${contact.addressList()}
      +        Emails: ${contact.emailList()}
      +        Events: ${contact.eventList()}
      +        Group memberships: ${contact.groupMembershipList()}
      +        IMs: ${contact.imList()}
      +        Names: ${contact.nameList()}
      +        Nicknames: ${contact.nicknameList()}
      +        Notes: ${contact.noteList()}
      +        Organizations: ${contact.organizationList()}
      +        Phones: ${contact.phoneList()}
      +        Relations: ${contact.relationList()}
      +        SipAddresses: ${contact.sipAddressList()}
      +        Websites: ${contact.websiteList()}
      +        -----------------------------------
      +    """.trimIndent()
      +    // There are also aggregate data functions that return a sequence instead of a list.
      +)
       

      Each Contact may have more than one of the following data if the Contact is made up of 2 or more RawContacts; name, nickname, note, organization, sip address.

      -

      For more info on how to easily aggregate data from all RawContacts in a Contact, read +

      +

      ℹ️ For more info on how to easily aggregate data from all RawContacts in a Contact, read Convenience functions.

      -

      To look into the actual Contacts Provider tables, read Debug the Contacts Provider tables.

      -

      To learn more about the Contact lookup key, read about Contact lookup key vs ID.

      +

      ℹ️ To learn more about the Contact lookup key, read about Contact lookup key vs ID.

      +

      ℹ️ To look into the actual Contacts Provider tables, read Debug the Contacts Provider tables.

      +

      Redacting entities

      All Entity in this library are Redactable, which indicates that there could be sensitive private user data that could be redacted, for legal purposes. If you are logging contact data in production @@ -2204,6 +2267,28 @@

      Syncing contact dataSync contact data across devices.

      +
      +

      Developer notes (or for advanced users)

      +

      Automatic data kinds creation

      +

      An entry of each of the following data kinds are automatically created for all contacts, if not +provided;

      +
        +
      • GroupMembership, underlying value defaults to the account's default system group
      • +
      • Name, underlying value defaults to null
      • +
      • Nickname, underlying value defaults to null
      • +
      • Note, underlying value defaults to null
      • +
      +

      This automatic creation occur automatically in the background (typically after creation) only for +RawContacts that are associated with an Account. If a valid account is provided, membership to the +(auto add) system group is automatically created immediately by the Contacts Provider at the time of +creation. The name, nickname, and note are automatically created at a later time.

      +
      +

      ℹ️ Query APIs in this library do not return blanks in results. In this case, the Name, +Nickname, and Note will not be included in the RawContact because their primary values are all +null. Blanks are also ignored on insert and deleted on update. For more info, read about +Blank data.

      +
      +

      If a valid account is not provided, no entries of the above are automatically created.

      diff --git a/entities/about-blank-contacts/index.html b/entities/about-blank-contacts/index.html index fd79c130..951dfe5c 100644 --- a/entities/about-blank-contacts/index.html +++ b/entities/about-blank-contacts/index.html @@ -1848,7 +1848,7 @@

      Blank contactsBlank contactsBlanks in queries

      -

      A where clause that uses any fields from the Data table Fields will exclude blanks in the -result (even if they are OR'ed) There are some joined fields that can be used to match blanks -as long as no other fields are in the where clause;

      +

      A where clause that uses any fields from the Data table Fields may exclude blanks in the result. +There are some joined fields that can be used to match blanks as long as no other fields are in the +where clause...

      • Fields.Contact enables matching blank Contacts. The result will include all RawContact(s) @@ -1905,7 +1905,7 @@

        Blank Contacts/RawContacts vs b non-blank data.

        Blank data are data entities that have only null, empty, or blank primary value(s).

        -

        For more info, read about Blank data.

        +

        ℹ️ For more info, read about Blank data.

        diff --git a/entities/about-blank-data/index.html b/entities/about-blank-data/index.html index 7f6b07d1..33c10434 100644 --- a/entities/about-blank-data/index.html +++ b/entities/about-blank-data/index.html @@ -1850,14 +1850,13 @@

        Blank dataBlank Data vs blank Contacts/RawContacts

        Blank data are data entities that have only null, empty, or blank primary value(s).

        Blank RawContacts and blank Contacts do not have any rows in the Data table. These do not have any non-blank data.

        -

        For more info, read about Blank contacts.

        +

        ℹ️ For more info, read about Blank contacts.

        diff --git a/entities/about-contact-lookup-key/index.html b/entities/about-contact-lookup-key/index.html index 3b196b53..a1ecd056 100644 --- a/entities/about-contact-lookup-key/index.html +++ b/entities/about-contact-lookup-key/index.html @@ -1904,7 +1904,7 @@

        Contact lookup key vs ID2059i4a27289d88a0a4e7, 0r62-2A2C2E, ...

        The official documentation for the Contact lookup key is,

        -

        An opaque value that contains hints on how to find the contact if its row id changed as a result +

        ℹ️ An opaque value that contains hints on how to find the contact if its row id changed as a result of a sync or aggregation.

        Let's dissect the documentation,

        @@ -1931,7 +1931,7 @@

        Contact lookup key vs ID
        -

        Actually, it seems like the Contact lookup key is a reference to a RawContact (or all of its +

        ℹ️ Actually, it seems like the Contact lookup key is a reference to a RawContact (or all of its constituent RawContacts). RawContacts have a reference to the parent Contact via the Contact ID. Similarly, the parent Contact has a reference to all of its constituent RawContacts via the lookup key.

        @@ -1974,7 +1974,7 @@

        How to get Contacts using lookup constituent RawContacts), and the linked Contact is unlinked, then the query will return multiple Contacts.

        -

        For more info, read Query contacts (advanced).

        +

        ℹ️ For more info, read Query contacts (advanced).

        Moving RawContacts between accounts and the lookup key

        Associating a local (device-only) RawContact to an Account will change the Contact lookup key. In @@ -1982,7 +1982,7 @@

        Moving RawContac the changes to the lookup key will only be applied after the Contacts Provider and sync adapters sync the changes. This means that the local changes are not immediately applied.

        -

        For more info, read Sync contact data across devices.

        +

        ℹ️ For more info, read Sync contact data across devices.

        Changing a RawContact's Account will result in a failed lookup using lookup keys prior to the Account change.

        @@ -1997,7 +1997,7 @@

        Moving RawContac

        Both Contacts apps will say that the Contact no longer exist or has been removed. This is not a bug. It is expected behavior due to the way the Contacts Provider works.

        -

        For more info, read Associate local RawContacts to an Account.

        +

        ℹ️ For more info, read Associate local RawContacts to an Account.

        Linking/unlinking contacts and the lookup key

        Linking and unlinking RawContacts will change the value of the lookup key. However, as discussed @@ -2019,12 +2019,12 @@

        Linking/unlinking contacts

        In both cases, the shortcut successfully opens the correct aggregate Contact.

        -

        For more info on linking/unlinking, read Link unlink Contacts.

        +

        ℹ️ For more info on linking/unlinking, read Link unlink Contacts.


        Developer notes (or for advanced users)

        -

        The following section are note from developers of this library for other developers. It is copied +

        ℹ️ The following section are note from developers of this library for other developers. It is copied from the DEV_NOTES. You may still read the following as a consumer of the library in case you need deeper insight.

        @@ -2035,7 +2035,8 @@

        Developer notes (or for advanced

        Unlike the Contact ID, the lookup key is the same across devices (for contacts that are associated with an Account and are synced).

        -

        Note that I did the following investigation with a much larger data set. I simplified it here for brevity.

        +

        ℹ️ The following investigation was done with a much larger data set. I has been simplified here +for brevity.

        Let's take a look at the following Contacts and RawContacts table rows,

        #### Contacts table
        @@ -2211,9 +2212,9 @@ 

        Developer notes (or for advanced

        Notice that the indirect relationship between the lookup key and RawContacts remains the same before and after the link-unlink even though the Contact IDs changed.

        -

        As mentioned earlier in this section, the "55" in "0r55-" seems to be referencing the RawContact ID. -In other words, since local RawContacts are not synced or tracked in a remote database where -Contacts -> RawContacts mappings exist, the Contacts Provider most likely uses this +

        ℹ️ As mentioned earlier in this section, the "55" in "0r55-" seems to be referencing the +RawContact ID. In other words, since local RawContacts are not synced or tracked in a remote +database where Contacts -> RawContacts mappings exist, the Contacts Provider most likely uses this "0r-" pattern to make the connection. This is not really relevant for us as we are not relying on this mechanism. I'm just pointing out my observations, which could be incorrect.

        @@ -2259,7 +2260,7 @@

        Developer notes (or for advanced having Contact details activity opened in the AOSP Contacts app will result in "error Contact does not exist" message in the AOSP Contacts app!

        -

        The RawContact and its Data also remained the same in this case.

        +

        ℹ️ The RawContact and its Data also remained the same in this case.

        Removing the account from it results in...

        #### Contacts table
        diff --git a/entities/about-local-contacts/index.html b/entities/about-local-contacts/index.html
        index a0e12d3e..bf2fdc92 100644
        --- a/entities/about-local-contacts/index.html
        +++ b/entities/about-local-contacts/index.html
        @@ -1859,7 +1859,7 @@ 

        Local (device-only) contactsSync contact data across devices.

        +

        ℹ️ For more info, read Sync contact data across devices.

        Associating a local RawContact to an Account

        Local RawContacts can be associated to an Account to enable syncing.

        @@ -1882,13 +1882,13 @@

        Adding an Account to the deviceRemoving the Account will remove all of the associated rows in the RawContact, Data, and Groups tables locally. This includes user Profile data in those tables.

        -

        Note that when all RawContacts of a Contact is removed, the Contact is also automatically removed -by the Contacts Provider.

        +

        ℹ️ When all RawContacts of a Contact is removed, the Contact is also automatically removed by the +Contacts Provider.

        Data kinds Account restrictions

        Entries of some data kinds should not be allowed to exist for local RawContacts.

        -

        The native Contacts app hides the following UI fields when inserting or updating local +

        ℹ️ The native Contacts app hides the following UI fields when inserting or updating local RawContacts. To enforce this behavior, this library ignores all of the above during inserts and updates for local raw contacts.

        diff --git a/entities/include-only-desired-data/index.html b/entities/include-only-desired-data/index.html index 72e73c78..b551ba86 100644 --- a/entities/include-only-desired-data/index.html +++ b/entities/include-only-desired-data/index.html @@ -415,15 +415,63 @@
        • - - Custom data support + + Using include in query APIs + + +
        • + +
        • + + Using include in insert APIs + + +
        • + +
        • + + Using include in update APIs + + + +
        • - - Performing updates on entities with partial includes + + Custom data support
        • @@ -1808,15 +1856,63 @@
          • - - Custom data support + + Using include in query APIs
          • - - Performing updates on entities with partial includes + + Using include in insert APIs + + +
          • + +
          • + + Using include in update APIs + + + + +
          • + +
          • + + Custom data support
          • @@ -1840,21 +1936,19 @@

            Include only certain fields for read and write operations

            -

            When using query APIs such as Query, BroadQuery, ProfileQuery, DataQuery, you are able to -specify all or only some kinds of data that you want to be included in the returned results.

            -

            When using insert APIs such as Insert and ProfileInsert, you are able to specify all or only -some kinds of data that you want to be included in the insert operation.

            -

            When using update APIs such as Update, ProfileUpdate, and DataUpdate, you are able to specify -all or only some kinds of data that you want to be included in the update operation.

            -

            Each field corresponds with an Entity property. For example, to include only the contact -display name, organization company, and all phone number fields,

            -
            query.include(mutableSetOf<AbstractDataField>().apply {
            +

            The read (query) and write (insert, update) APIs in this library provides an include function that +allows you to specify all (default) or some fields to read or write in the Contacts Provider +database.

            +

            The fields defined in contacts.core.Fields.kt specify what properties of entities to include in +read and write operations. For example, to include only the contact display name, organization +company, and all phone number fields in a query/insert/update operation,

            +
            queryInsertUpdate.include(mutableSetOf<AbstractDataField>().apply {
                 add(Fields.Contact.DisplayNamePrimary)
                 add(Fields.Organization.Company)
                 addAll(Fields.Phone.all)
             })
             
            -

            The following properties are populated with non-blank data (or null if no data is found),

            +

            The following entity properties are are used in the read/write operation,

            Contact {
                 displayNamePrimary
             
            @@ -1871,54 +1965,164 @@ 

            Include only } }

            +
            +

            ℹ️ For more info, read about API Entities.

            +

            To explicitly include everything,

            query.include(Fields.all)
             
            -

            Not invoking the include function will default to including everything, including custom data. +

            ℹ️ Not invoking the include function will default to including everything, including custom data. The above code will exclude custom data. Read the Custom data support section for more info.

            -

            The matching contacts may have non-null data for each of the included fields. Fields that are -included will not guarantee non-null data in the returned contact instances because some data may -actually be null in the database.

            +

            The matching contacts may have non-null data corresponding to each of the included fields. +Fields that are included will not guarantee non-null data in the returned contact instances because +some data may actually be null in the database.

            If no fields are specified, then all fields are included. Otherwise, only the specified fields will be included in addition to required API fields (e.g. IDs), which are always included.

            -

            Note that this may affect performance. It is recommended to only include fields that will be used -to save CPU and memory.

            +

            ℹ️ This may affect performance. It is recommended to only include fields that will be used to save +CPU and memory.

            +
            +

            Using include in query APIs

            +

            When using query APIs such as Query, BroadQuery, ProfileQuery, and DataQuery, you are able +to specify all or only some kinds of data that you want to be included in the returned results.

            +

            When all fields are included in a query operation, all properties of Contacts, RawContacts, and Data +are populated with values from the database. Properties of fields that are included are not +guaranteed to be non-null because the database may actually have no data for the corresponding +field.

            +

            When only some fields are included, only those included properties of Contacts, RawContacts, and +Data are populated with values from the database. Properties of fields that are not included are +guaranteed to be null.

            +

            Using include in insert APIs

            +

            When using insert APIs such as Insert and ProfileInsert, you are able to specify all or only +some kinds of data that you want to be included in the insert operation.

            +

            When all fields are included in an insert operation, all properties of Contacts, RawContacts, and +Data are inserted into the database.

            +

            When only some fields are included, only those included properties of Contacts, RawContacts, and +Data are inserted into the database. Properties of fields that are not included are NOT inserted +into the database.

            +

            Using include in update APIs

            +

            When using update APIs such as Update, ProfileUpdate, and DataUpdate, you are able to specify +all or only some kinds of data that you want to be included in the update operation.

            +

            An "update" operation consists of insertion, updates, and deletions

            +

            To ensure that the database matches the data contained in the entities being passed into the update +operation, a combination of insert, update, or delete operations are performed internally by the +update API. The following is what constitutes an "updated" event;

            +
              +
            • A RawContact can have 0 or 1 name.
                +
              • If it is null or blank, then the update operation will...
                  +
                • delete the name row of the RawContact from the database, if it exist
                • +
                +
              • +
              • If it is not null, then the update operation will do one of the following...
                  +
                • update an existing name row, if it exist
                • +
                • or insert a new name row, if one does not exist
                • +
                +
              • +
              +
            • +
            • A RawContact can have 0, 1 or more emails.
                +
              • If the list of emails is empty (or contains only blanks), then the update operation will...
                  +
                • delete all email rows of the RawContact from the database, if any exist
                • +
                +
              • +
              • If the list of emails is not empty, then the update operation will do all of the following...
                  +
                • update email rows for emails that already exist in the database
                • +
                • insert new email rows for emails that do not yet exist in the database
                • +
                • delete email rows for emails that exist in the database but not in the (in-memory) entity
                • +
                +
              • +
              +
            • +
            +

            Blank data are deleted

            +

            Blank data are deleted from the database, unless the the complete set of corresponding fields are +not included in the update operation.

            +
            +

            ⚠️ Prior to version 0.3.0 where +include in update APIs have been overhauled, +blank data are deleted from the database even if the corresponding fields are not included.

            +

            ℹ️ For more info on blank data, read about Blank data.

            +

            Including complete field sets for "update"

            +

            When all fields are included in an update operation, all properties of Contacts, RawContacts, and +Data are "updated" in the database.

            +

            When only some fields are included, only those included properties of Contacts, RawContacts, and +Data are "updated" in the database. Properties of fields that are not included are NOT +"updated".

            +

            To get all contacts including all fields, then modify the emails, phones, and addresses and perform +an update operation on all fields,

            +
            val contacts = query.find() 
            +val contactsWithModifiedEmailPhoneAddress = modifyEmailPhoneAddressIn(contacts)
            +update.contacts(contactsWithModifiedEmailPhoneAddress).commit()
            +
            +

            To modify all emails of all contacts without updating anything else into the database,

            +
            val contactsWithOnlyEmailData = query.include(Fields.Email.all).find()
            +val contactsWithModifiedEmailData = modifyEmailsIn(contactsWithOnlyEmailData)
            +update.contacts(contactsWithModifiedEmailData).include(Fields.Email.all).commit()
            +
            +

            To remove all emails from all contacts without updating anything else in the database,

            +
            val contactsWithAllData = query.find()
            +val contactsWithNoEmailData = removeEmailsFrom(contactsWithAllData)
            +update.contacts(contactsWithNoEmailData).include(Fields.Email.all).commit()
            +
            +

            Or alternatively,

            +
            val contactsWithNoData = query
            +    .include(Fields.Required.all) // does not have to be Fields.Required.
            +    .find()
            +update.contacts(contactsWithNoData).include(Fields.Email.all).commit()
            +
            +

            Including a subset of field sets for "update"

            +

            Including only a subset of a set of fields results in,

            +
              +
            • deletion of blanks (same as if the complete set of fields are included)
            • +
            • update of properties corresponding to included fields
            • +
            • no-op on properties corresponding to excluded fields
            • +
            +

            For example, the following set the given name and family name to the non-null values but does not +set all others (i.e. display name, middle name, prefix, suffix, phonetic given middle family name).

            +
            contacts
            +    .update()
            +    .include(
            +        Fields.Name.GivenName,
            +        Fields.Name.FamilyName,
            +    )
            +    .rawContacts(
            +        existingRawContact.mutableCopy {
            +            setName {
            +                displayName = "Mr. "
            +                prefix = "Mr."
            +                givenName = "First"
            +                middleName = "Middle"
            +                familyName = "Last"
            +                suffix = "Jr."
            +                phoneticGivenName = "fUHRst"
            +                phoneticMiddleName = "mIdl"
            +                phoneticFamilyName = "lAHst"
            +            }
            +        }
            +    )
            +    .commit()
            +
            +

            If the name row for the RawContact did not exist before the update operation, then a new name row +will be inserted into the database for the RawContact. The given name and family name columns will +be set to the specified values. All other columns will be set to null.

            +

            If the name row for the RawContact already exists before the update operation, then the name row +will be updated. The given name and family name columns will be set to the specified values. All +other columns will remain unchanged (the null or non-null values will remain null and non-null +respectively).

            Custom data support

            The include function supports registered custom data fields, which my be combined with native (non-custom) data fields.

            -

            By default, not calling the include function will include all fields, including custom data. +

            By default, not calling the include function will include all fields, including custom data. However, the below code will include all native fields but exclude custom data;

            -
            .include(Fields.all)
            +
            .include(Fields.all)
             
            -

            If you want to include everything, including custom data, and for some reason you must invoke the +

            If you want to include everything, including custom data, and for some reason you must invoke the include function,

            -
            .include(Fields.all + contactsApi.customDataRegistry.allFields())
            -
            -

            Performing updates on entities with partial includes

            -

            When the query include function is used, only certain data will be included in the returned -entities. All other data are guaranteed to be null (except for those in Fields.Required).

            -

            When performing updates on entities that have only partial data included, make sure to use the same -included fields in the update operation as the included fields used in the query. This will ensure -that the set of data queried and updated are the same. For example, in order to get and set only -email addresses and leave everything the same in the database...

            -
            val contacts = query.include(Fields.Email.Address).find()
            -val mutableContacts = setEmailAddresses(contacts)
            -update.contacts(mutableContacts).include(Fields.Email.Address).commit()
            -
            -

            On the other hand, you may intentionally include only some data and perform updates on all data -(not just the included ones) to effectively delete all non-included data. This is, currently, -a feature- not a bug! For example, in order to get and set only email addresses and set all other -data to null (such as phone numbers, name, etc) in the database...

            -
            val contacts = query.include(Fields.Email.Address).find()
            -val mutableContacts = setEmailAddresses(contacts)
            -update.contacts(mutableContacts).include(Fields.all).commit()
            +
            .include(Fields.all + contactsApi.customDataRegistry.allFields())
             
            -

            This gives you the most flexibility when it comes to specifying what fields to include/exclude in -queries, inserts, and updates, which will allow you to do things beyond your wildest imagination!

            diff --git a/entities/redact-apis-and-entities/index.html b/entities/redact-apis-and-entities/index.html index 87abcab8..837b8e08 100644 --- a/entities/redact-apis-and-entities/index.html +++ b/entities/redact-apis-and-entities/index.html @@ -506,8 +506,8 @@
          • - - Developer notes + + Developer notes (or for advanced users)
          • @@ -1871,8 +1871,8 @@
          • - - Developer notes + + Developer notes (or for advanced users)
          • @@ -1904,7 +1904,7 @@

            Redact entities contact data in production to remote data centers for analytics or crash reporting, then it is important to redact certain parts of every contact's data.

            -

            For more info on logging, read Log API input and output.

            +

            ℹ️ For more info on logging, read Log API input and output.

            This library is written and maintained purely by software developers with no official education or @@ -1931,7 +1931,7 @@

            Redactable entitiesRedactable APIs

            All Create (Query), Read (Query), Update, and Delete APIs @@ -2006,12 +2006,12 @@

            Logging API input and outputLog API input and output.

            -

            Developer notes

            -

            I know that we cannot prevent consumers of this API from violating privacy laws if they really -want to. BUT, the library should provide consumers an easy way to be GDPR-compliant! This is not -necessary for all libraries to implement but this library deals with sensitive, private user data. -Therefore, we need to be extra careful and provide consumers a GDPR-compliant way to log everything -in this library!

            +
            +

            Developer notes (or for advanced users)

            +

            We cannot prevent users of this API from violating privacy laws if they really want to. BUT, the +library should provide consumers an easy way to be GDPR-compliant! This is not necessary for all +libraries to implement but this library deals with sensitive, private user data. Therefore, we need +to be extra careful and provide consumers a GDPR-compliant way to log everything in this library!

            diff --git a/entities/sync-contact-data/index.html b/entities/sync-contact-data/index.html index 29d2ccb3..b3c17906 100644 --- a/entities/sync-contact-data/index.html +++ b/entities/sync-contact-data/index.html @@ -1908,11 +1908,11 @@

            Sync contact data across devices
            -

            Besides Google Accounts, there is also Samsung, Yahoo, MSN/Hotmail, etc.

            +

            ℹ️ Besides Google Accounts, there is also Samsung, Yahoo, MSN/Hotmail, etc.

            Syncing contacts across devices is possible with sync adapters and Contacts' lookup key.

            -

            For more info, read about Contact lookup key vs ID.

            +

            ℹ️ For more info, read about Contact lookup key vs ID.

            Adding or removing Accounts

            When an Account is added to the system and Contacts syncing is enabled and there is network @@ -1931,7 +1931,7 @@

            Only conta depends on the account sync settings, which can be configured in the system settings app and possibly through some remote configuration.

            -

            For more info, read about Local (device-only) contacts.

            +

            ℹ️ For more info, read about Local (device-only) contacts.

            When are changes synced?

            In general, the Contacts Provider and the registered sync adapters are responsible for triggering @@ -1954,19 +1954,19 @@

            Some custom da synced because they are not account specific and they have no sync adapters and no remote service to interface with.

            -

            For more info, read Integrate custom data.

            +

            ℹ️ For more info, read Integrate custom data.

            Custom data from other apps may be synced

            This library does not sync contact data that belongs to other apps and services. For example, Google Contacts, WhatsApp, and other apps define their own set of custom data that their own sync adapters sync with their own remote services, which requires authentication.

            -

            For more info, read Integrate custom data from other apps.

            +

            ℹ️ For more info, read Integrate custom data from other apps.

            This library does not provide sync adapters

            This library does not have any APIs related to syncing. It is considered out of scope of this library as it requires access to remote databases and account-specific data. Let's talk about it -though. However, it is good to know how it works if you just want more insight :grin:.

            +though. However, it is good to know how it works if you just want more insight.

            https://developer.android.com/guide/topics/providers/contacts-provider#SyncAdapters

            The Contacts Provider is specifically designed for handling synchronization of contacts data @@ -1991,9 +1991,9 @@

            This library does not provi thing that typically happens outside of an application UI. This library is focused on Create, Read, Update, and Delete (CRUD) operations on native and custom data to and from the local database. Syncing the local database to and from a remote database in the background is a totally different -story altogether :grin:

            +story altogether.

            -

            For more info, read Integrate custom data.

            +

            ℹ️ For more info, read Integrate custom data.

            diff --git a/groups/delete-groups/index.html b/groups/delete-groups/index.html index 0930a475..2e704ae1 100644 --- a/groups/delete-groups/index.html +++ b/groups/delete-groups/index.html @@ -1959,7 +1959,7 @@

            Performing t For more info, read Execute work outside of the UI thread using coroutines.

            You may, of course, use other multi-threading libraries or just do it yourself =)

            -

            Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

            +

            ℹ️ Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

            Performing the delete with permission

            Deletes require the android.permission.WRITE_CONTACTS permissions. If not granted, the delete diff --git a/groups/insert-groups/index.html b/groups/insert-groups/index.html index 9957cd4c..cd0ad70c 100644 --- a/groups/insert-groups/index.html +++ b/groups/insert-groups/index.html @@ -1989,7 +1989,7 @@

            Groups and AccountsQuery groups.

            +

            ℹ️ For more info on the relationship of Groups and Accounts, read Query groups.

            Groups and duplicate titles

            The Contacts Provider allows multiple groups with the same title (case-sensitive comparison) @@ -2022,7 +2022,7 @@

            Handling the insert result .find()

            -

            For more info, read Query groups.

            +

            ℹ️ For more info, read Query groups.

            Alternatively, you may use the extensions provided in GroupsInsertResult. To get all newly created Groups,

            @@ -2063,7 +2063,7 @@

            Performing t read Execute work outside of the UI thread using coroutines.

            You may, of course, use other multi-threading libraries or just do it yourself =)

            -

            Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

            +

            ℹ️ Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

            Performing the insert with permission

            Inserts require the android.permission.WRITE_CONTACTS and android.permission.GET_ACCOUNTS diff --git a/groups/query-groups/index.html b/groups/query-groups/index.html index e862bfec..10b9e1f6 100644 --- a/groups/query-groups/index.html +++ b/groups/query-groups/index.html @@ -1979,7 +1979,7 @@

            A basic query .find()

            -

            Note that it is recommended to get sets of groups for a single account at a time to avoid confusion.

            +

            ℹ️ It is recommended to get sets of groups for a single account at a time to avoid confusion.

            Specifying Accounts

            To limit the search to only those Groups associated with one of the given accounts,

            @@ -1989,7 +1989,7 @@

            Specifying Accounts
            .accounts(Account("john.doe@gmail.com", "com.google"))
             

            -

            For more info, read Query for Accounts.

            +

            ℹ️ For more info, read Query for Accounts.

            If no accounts are specified (this function is not called or called with no Accounts), then all Groups of all accounts are included in the search.

            @@ -2016,8 +2016,8 @@

            Limiting and offsetting

      This is useful for pagination =)

      -

      Note that it is recommended to limit the number of groups when querying to increase performance -and decrease memory cost.

      +

      ℹ️ It is recommended to limit the number of groups when querying to increase performance and +decrease memory cost.

      Executing the query

      To execute the query,

      @@ -2047,7 +2047,7 @@

      Performing the query asynchronously For more info, read Execute work outside of the UI thread using coroutines.

      You may, of course, use other multi-threading libraries or just do it yourself =)

      -

      Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

      +

      ℹ️ Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

      Performing the query with permission

      Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will @@ -2059,8 +2059,8 @@

      Using the w

      Use the contacts.core.GroupsFields combined with the extensions from contacts.core.Where to form WHERE clauses.

      -

      This docs page will not provide a tutorial on database where clauses. It assumes that you know the basics. -If you don't know the basics, then search for sqlite where clause.

      +

      ℹ️ This docs page will not provide a tutorial on database where clauses. It assumes that you know +the basics. If you don't know the basics, then search for sqlite where clause.

      For example, to find groups with a specific title,

      .where { Title equalToIgnoreCase "friends" }
      diff --git a/groups/update-groups/index.html b/groups/update-groups/index.html
      index 224c0f60..4001a329 100644
      --- a/groups/update-groups/index.html
      +++ b/groups/update-groups/index.html
      @@ -2014,8 +2014,8 @@ 

      Read-only GroupsGroups and duplicate titles

      @@ -2066,7 +2066,7 @@

      Performing t read Execute work outside of the UI thread using coroutines.

      You may, of course, use other multi-threading libraries or just do it yourself =)

      -

      Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

      +

      ℹ️ Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

      Performing the update with permission

      Updates require the android.permission.WRITE_CONTACTS. If not granted, the update will do nothing diff --git a/index.html b/index.html index 82b0c3d8..0aa0d566 100644 --- a/index.html +++ b/index.html @@ -1993,7 +1993,7 @@

      Android Contacts, Reborn

      -

      Written with ♥️ and 🔥 since December 2018. Open sourced since October 2021.

      +

      ℹ️ Written with ♥️ and 🔥 since December 2018. Open sourced since October 2021.

      Android Contacts, Reborn banner

      JitPack @@ -2021,7 +2021,7 @@

    @@ -2099,7 +2099,7 @@

    FeaturesInstallation

    -

    This library is a multi-module project published with JitPack +

    ℹ️ This library is a multi-module project published with JitPack JitPack

    First, include JitPack in the repositories list,

    @@ -2132,8 +2132,8 @@

    Installation}

    -

    ⚠️ IMPORTANT! Starting with version 0.2.0, installing all modules in a single line is only -supported when using the dependencyResolutionManagement in settings.gradle. +

    ⚠️ Starting with version 0.2.0, installing all modules in a single line is only supported when +using the dependencyResolutionManagement in settings.gradle. You are still able to install all modules by specifying them individually.

    For more info about the different modules and dependency resolution management, @@ -2153,7 +2153,7 @@

    Quick Start .find()
    -

    For more info, read Query contacts.

    +

    ℹ️ For more info, read Query contacts.

    Something a bit more advanced...

    To retrieve the first 5 contacts (including only the contact id, display name, and phone numbers in @@ -2194,7 +2194,7 @@

    Quick Start .find()
    -

    Fore more info, read Query contacts (advanced).

    +

    ℹ️ For more info, read Query contacts (advanced).

    Once you have the contacts, you now have access to all of their data!

    val contact: Contact
    @@ -2237,7 +2237,7 @@ 

    Quick Start)

    -

    For more info, read about API Entities.

    +

    ℹ️ For more info, read about API Entities.

    More than enough APIs that will allow you to build your own contacts app!

    This library is capable of doing more than just querying contacts. Actually, you can build your own @@ -2256,7 +2256,7 @@

    Query specific data kinds.

    +

    ℹ️ For more info, read Query specific data kinds.

    To CREATE/INSERT a contact with a name of "John Doe" who works at Amazon with a work email of "john.doe@amazon.com" (in Kotlin),

    @@ -2317,7 +2317,7 @@

    .commit()
    -

    For more info, read Insert contacts.

    +

    ℹ️ For more info, read Insert contacts.

    If John Doe switches jobs and heads over to Microsoft, we can UPDATE his data,

    Contacts(context)
    @@ -2334,7 +2334,7 @@ 

    .commit()

    -

    For more info, read Update contacts.

    +

    ℹ️ For more info, read Update contacts.

    If we no longer like John Doe, we can DELETE him from our life,

    Contacts(context)
    @@ -2343,7 +2343,7 @@ 

    .commit()

    -

    For more info, read Delete Contacts.

    +

    ℹ️ For more info, read Delete Contacts.

    Threading and permissions

    This library provides Kotlin coroutine extensions in the permissions module for all API functions @@ -2362,7 +2362,7 @@

    Threading and permissions}
    -

    For more info, read Permissions handling using coroutines +

    ℹ️ For more info, read Permissions handling using coroutines and Execute work outside of the UI thread using coroutines.

    So, if we call the above function and we don't yet have permission. The user will be prompted to @@ -2370,8 +2370,8 @@

    Threading and permissionsFull documentation, guides, and samples

    The above examples barely scratches the surface of what this library provides. For more in-depth @@ -2422,7 +2422,7 @@

    Requirements

      diff --git a/logging/log-api-input-output/index.html b/logging/log-api-input-output/index.html index 7e8b59ad..0d81855e 100644 --- a/logging/log-api-input-output/index.html +++ b/logging/log-api-input-output/index.html @@ -1849,7 +1849,7 @@

      Log API input and output)
      -

      For more info on Contacts API setup, read Contacts API Setup.

      +

      ℹ️ For more info on Contacts API setup, read Contacts API Setup.

      Invoking the find or commit functions in query, insert, update, and delete APIs will result in the following output in the Logcat,

      diff --git a/other/convenience-functions/index.html b/other/convenience-functions/index.html index fff19eea..67cf0639 100644 --- a/other/convenience-functions/index.html +++ b/other/convenience-functions/index.html @@ -1913,8 +1913,8 @@

      Convenience functionsContact data getter and setters

      Contacts can be made up of one or more RawContacts. In the case that a Contact has two or more @@ -1925,7 +1925,7 @@

      Contact data getter and setterscontact.mutableCopy().rawContacts.first().emails.add(NewEmail())
      -

      For more info, read about API Entities.

      +

      ℹ️ For more info, read about API Entities.

      To simplify things, getter/setter extensions are provided in the ContactData.kt file,

      // get all emails from all RawContacts belonging to the Contact
      @@ -1935,9 +1935,9 @@ 

      Contact data getter and setterscontact.mutableCopy().addEmail(NewEmail())

      -

      Newer versions of the Android Open Source Project Contacts app and the Google Contacts app shows -data coming from all RawContacts in a Contact details screen. However, they only allow editing -a single RawContact and not the aggregate Contact in a single screen to avoid confusion. +

      ℹ️ Newer versions of the Android Open Source Project Contacts app and the Google Contacts app +shows data coming from all RawContacts in a Contact details screen. However, they only allow +editing a single RawContact and not the aggregate Contact in a single screen to avoid confusion. With this in mind, feel free to use the getter extensions but be very careful with using the setters!

      @@ -1964,7 +1964,7 @@

      Mutable and New RawContact data these extensions, the property being passed will be redacted if the Contact/RawContact it is being added to is redacted.

      -

      For more info, read Redact entities and API input and output in production.

      +

      ℹ️ For more info, read Redact entities and API input and output in production.

      Getting the parent Contact of a RawContact or Data

      Using the Query API, it is easy to get the parent Contact of a RawContact or Data,

      @@ -1972,7 +1972,7 @@

      Getting the parent C val contactOfData = contactsApi.query().where { Contact.Id equalTo data.contactId }.find().firstOrNull()
      -

      For more info, read Query contacts (advanced).

      +

      ℹ️ For more info, read Query contacts (advanced).

      To shorten things, you can use the extensions in RawContactContact.kt and DataContact.kt,

      val contactOfRawContact = rawContact.contact(contactsApi)
      @@ -1983,7 +1983,7 @@ 

      Getting the parent C

      These are blocking calls so you might want to do them outside the UI thread.

      -

      For more info, read Execute work outside of the UI thread using coroutines.

      +

      ℹ️ For more info, read Execute work outside of the UI thread using coroutines.

      Refresh Contact, RawContact, and Data references

      In-memory references to these entities could become inaccurate due to changes in the database that @@ -2005,7 +2005,7 @@

      Refresh Contact, RawCont

      These are blocking calls so you might want to do them outside the UI thread.

      -

      For more info, read Execute work outside of the UI thread using coroutines.

      +

      ℹ️ For more info, read Execute work outside of the UI thread using coroutines.

      Sort Contacts by data fields

      The Query and BroadQuery APIs allows you to sort Contacts based on fields in the Contacts table @@ -2039,7 +2039,7 @@

      Get the Group of a GroupMembership

      These are blocking calls so you might want to do them outside the UI thread.

      -

      For more info, read Execute work outside of the UI thread using coroutines.

      +

      ℹ️ For more info, read Execute work outside of the UI thread using coroutines.

      Get the RawContact of a BlankRawContact

      The Query API allows you to get the RawContact version of a BlankRawContact,

      @@ -2053,7 +2053,7 @@

      Get the RawContact of a BlankRa

      These are blocking calls so you might want to do them outside the UI thread.

      -

      For more info, read Execute work outside of the UI thread using coroutines.

      +

      ℹ️ For more info, read Execute work outside of the UI thread using coroutines.

      diff --git a/other/get-set-clear-contact-raw-contact-options/index.html b/other/get-set-clear-contact-raw-contact-options/index.html index 85baaeb4..8235b4af 100644 --- a/other/get-set-clear-contact-raw-contact-options/index.html +++ b/other/get-set-clear-contact-raw-contact-options/index.html @@ -1574,7 +1574,7 @@
    • - Can contacts be inserted with options + Can contacts be inserted with options?
    • @@ -1935,7 +1935,7 @@
    • - Can contacts be inserted with options + Can contacts be inserted with options?
    • @@ -2045,7 +2045,7 @@

      Performing options managem For more info, read Execute work outside of the UI thread using coroutines.

      You may, of course, use other multi-threading libraries or just do it yourself =)

      -

      Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

      +

      ℹ️ Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

      Performing options management with permission

      Getting and setting options require the android.permission.READ_CONTACTS and @@ -2070,18 +2070,21 @@

      Starred in Android (Favorites)FAQs

      -

      Can contacts be inserted with options

      +

      Can contacts be inserted with options?

      +

      No, you cannot get/set/update options for Contacts/RawContacts that have not yet been inserted in +the Contacts Provider database. In other words, only Contacts/RawContacts retrieved via query or +result APIs can use the extension functions in contacts.core.util.ContactOptions.kt and +contacts.core.util.RawContactOptions.kt.

      -

      Related issues; #120

      +

      ℹ️ Issue #120 will change the answer +to "yes". THe underlying mechanism will not change but the outward public facing API will change. +Internally, the insert operation will insert a new row in the Contacts/RawContacts table and then +attempt to set the options immediately after.

      -

      You cannot get/set/update options for Contacts/RawContacts that have not yet been inserted in the -Contacts Provider database. In other words, only Contacts/RawContacts retrieved via query or result -APIs can use the extension functions in contacts.core.util.ContactOptions.kt and -contacts.core.util.RawContactOptions.kt.

      To insert a new contact "with options", you should insert the contact first. Then, if the insert succeeds, proceed to set the options.

      -

      For more info about insert, read Insert contacts.

      +

      ℹ️ For more info about insert, read Insert contacts.

      diff --git a/other/get-set-clear-default-data/index.html b/other/get-set-clear-default-data/index.html index db408d11..fe73cea4 100644 --- a/other/get-set-clear-default-data/index.html +++ b/other/get-set-clear-default-data/index.html @@ -1916,7 +1916,7 @@

      Get set clear default Contact data
      -

      For more info on the common data kinds, read about API Entities.

      +

      ℹ️ For more info on the common data kinds, read about API Entities.

      Getting default data

      To get the default Contact email and phone from all RawContacts,

      @@ -1993,7 +1993,7 @@

      Performing default da For more info, read Execute work outside of the UI thread using coroutines.

      You may, of course, use other multi-threading libraries or just do it yourself =)

      -

      Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

      +

      ℹ️ Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

      Performing default data management with permission

      Getting and setting/clearing default data require the android.permission.READ_CONTACTS and @@ -2001,11 +2001,6 @@

      Performing default d setting/clearing default data will fail.


      Developer notes (or for advanced users)

      -
      -

      The following section are note from developers of this library for other developers. It is copied -from the DEV_NOTES. You may still read the following as a consumer of the library -in case you need deeper insight.

      -

      As per documentation, for a set of data rows with the same mimetype (e.g. a set of emails), there should only be one primary data row (e.g. email) per RawContact and one super primary data row per Contact. Furthermore, a data row that is super primary must also be primary.

      diff --git a/other/get-set-remove-contact-raw-contact-photo/index.html b/other/get-set-remove-contact-raw-contact-photo/index.html index 544a73ba..f75804ef 100644 --- a/other/get-set-remove-contact-raw-contact-photo/index.html +++ b/other/get-set-remove-contact-raw-contact-photo/index.html @@ -2002,7 +2002,7 @@

      Contact and RawContact photosLink unlink Contacts.

      +

      ℹ️ For more info, read Link unlink Contacts.

      Full-sized photos and thumbnails

      Each RawContact may be assigned one photo. The thumbnail is just a downsized version of the @@ -2033,7 +2033,7 @@

      Getting contact photo}
      -

      For more info, read Query contacts +

      ℹ️ For more info, read Query contacts and Query contacts (advanced).

      Using one of the extension functions in contacts.core.util.ContactPhoto.kt to get photo data,

      @@ -2059,7 +2059,7 @@

      Getting contact photoval photoThumbnailBitmapDrawable = rawContact.photoThumbnailBitmapDrawable(contactsApi)
      -

      Keep in mind that the Contact photo is just a reference to one of its RawContact's photo.

      +

      ℹ️ The Contact photo is just a reference to one of its RawContact's photo.

      Setting contact photo

      Setting the photo can only be done after the Contact or RawContact has been inserted. In other @@ -2079,7 +2079,7 @@

      Setting contact photorawContact.setPhoto(contactsApi, photoBitmapDrawable)
      -

      Keep in mind that the Contact photo is just a reference to one of its RawContact's photo.

      +

      ℹ️ The Contact photo is just a reference to one of its RawContact's photo.

      Removing contact photo

      To remove the Contact (and corresponding RawContact) photo (full-sized and thumbnail),

      @@ -2089,8 +2089,7 @@

      Removing contact photo
      rawContact.removePhoto(contactsApi)
       
      -

      Keep in mind that the Contact photo is just a reference to one of its RawContact's photo. -A few things to keep in mind.

      +

      ℹ️ The Contact photo is just a reference to one of its RawContact's photo.

      Changes are immediate and are not applied to the receiver

      These apply to set and remove functions.

      @@ -2157,7 +2156,7 @@

      Performing photo management For more info, read Execute work outside of the UI thread using coroutines.

      You may, of course, use other multi-threading libraries or just do it yourself =)

      -

      Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

      +

      ℹ️ Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

      Performing photo management with permission

      Getting and setting photos require the android.permission.READ_CONTACTS and @@ -2167,27 +2166,32 @@

      Performing photo management

      FAQs

      Can contacts be insert with photo?

      -

      Related issues; #116 -and #119

      +

      ℹ️ Asked in issue #116

      -

      You cannot get/set/remove photos for Contacts/RawContacts that have not yet been inserted in the +

      No, you cannot get/set/remove photos for Contacts/RawContacts that have not yet been inserted in the Contacts Provider database. In other words, only Contacts/RawContacts retrieved via query or result APIs can use the extension functions in contacts.core.util.ContactPhoto.kt and contacts.core.util.RawContactPhoto.kt.

      +
      +

      ℹ️ Issue #119 will change the answer +to yes". THe underlying mechanism will not change but the outward public facing API will change. +Internally, the insert operation will insert a new row in the Contacts/RawContacts table and then +attempt to set the photo immediately after.

      +

      To insert a new contact "with photo", you should insert the contact first. Then, if the insert succeeds, proceed to set the photo.

      -

      For more info about insert, read Insert contacts.

      -

      Note for contributors; It is possible to include photo thumbnail data as part of the insertion -of a new RawContact using ContactsContract.CommonDataKinds.Photo.PHOTO. The Contacts Provider -will use the thumbnail as the full-sized photo as well. However, this is not good practice as the -full-sized photo will have a really low resolution. Showing the full-sized photo in a big view -will not look good. Therefore, this library does not allow this. Consumers must first insert their -new RawContact so that they can set the full-sized photo.

      +

      ℹ️ For more info about insert, read Insert contacts.

      +

      🗒 Note for contributors; It is possible to include photo thumbnail data as part of the +insertion of a new RawContact using ContactsContract.CommonDataKinds.Photo.PHOTO. The Contacts +Provider will use the thumbnail as the full-sized photo as well. However, this is not good +practice as the full-sized photo will have a really low resolution. Showing the full-sized photo +in a big view will not look good. Therefore, this library does not allow this. Consumers must +first insert their new RawContact so that they can set the full-sized photo.

      Can photo be set using a uri instead of bytes and bitmaps?

      -

      Related issues; #109

      +

      ℹ️ Asked in discussion #195

      No and yes. The core APIs provided in this library only provides functions that the Contacts Provider natively supports. This means setting Contact or RawContact photo only using bytes (and diff --git a/other/link-unlink-contacts/index.html b/other/link-unlink-contacts/index.html index 818870ab..3226c732 100644 --- a/other/link-unlink-contacts/index.html +++ b/other/link-unlink-contacts/index.html @@ -1633,16 +1633,16 @@ -

    - - - - -
  • +
  • Effects of linking/unlinking contacts +
  • + + + + @@ -1999,16 +1999,16 @@ - - - - - -
  • +
  • Effects of linking/unlinking contacts +
  • + + + + @@ -2038,7 +2038,6 @@

    Link unlink ContactsLinking

    To link three Contacts and all of their constituent RawContacts into a single Contact,

    val linkResult = contact1.link(contactsApi, contact2, contact3)
    @@ -2082,7 +2081,7 @@ 

    LinkingHandling the link result

    @@ -2093,7 +2092,7 @@
    -

    Note that the contactId will belong to one of the linked Contacts.

    +

    ℹ️ The contactId will belong to one of the linked Contacts.

    Once you have the Contact ID, you can retrieve the Contact via the Query API,

    val contact = contactsApi
    @@ -2102,13 +2101,12 @@ 
    -

    For more info, read Query contacts (advanced).

    +

    ℹ️ For more info, read Query contacts (advanced).

    Alternatively, you may use the extensions provided in ContactLikResult. To get the parent Contact of all linked RawContacts,

    val contact = linkResult.contact(contactsApi)
     
    -

    Unlinking

    To unlink a Contacts with more than one RawContact into a separate Contacts,

    val unlinkResult = contact.unlink(contactsApi)
    @@ -2117,7 +2115,7 @@ 

    UnlinkingHandling the unlink result

    @@ -2134,13 +2132,12 @@
    -

    For more info, read Query contacts (advanced).

    +

    ℹ️ For more info, read Query contacts (advanced).

    Alternatively, you may use the extensions provided in ContactLikResult. To get the Contacts of all unlinked RawContacts,

    val contacts = unlinkResult.contacts(contactsApi)
     
    -

    Changes are immediate and are not applied to the receiver

    These apply to set and clear functions.

      @@ -2165,7 +2162,7 @@

      Performing linking/unlinking read Execute work outside of the UI thread using coroutines.

      You may, of course, use other multi-threading libraries or just do it yourself =)

      -

      Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

      +

      ℹ️ Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

      Performing linking/unlinking with permission

      Getting and setting/clearing default data require the android.permission.WRITE_CONTACTS @@ -2175,20 +2172,15 @@

      Syncing is done at the RawConta

      You may link Contacts with RawContacts that belong to different Accounts. Any RawContact Data modifications are synced per Account sync settings.

      -

      For more info, read Sync contact data across devices.

      +

      ℹ️ For more info, read Sync contact data across devices.

      RawContacts that are not associated with an Account are local to the device and therefore will not be synced even if it is linked to a Contact with a RawContact that is associated with an Account.

      -

      For more info, read about Local (device-only) contacts.

      +

      ℹ️ For more info, read about Local (device-only) contacts.


      Developer notes (or for advanced users)

      -
      -

      The following section are note from developers of this library for other developers. It is copied -from the DEV_NOTES. You may still read the following as a consumer of the library -in case you need deeper insight.

      -

      Behavior of linking/merging/joining contacts (AggregationExceptions)

      The native Contacts app terminology has changed over time;

        @@ -2242,8 +2234,8 @@

        Behavi by the native Contacts app manually by setting Contact Y's Data name row to be the "default" (isPrimary and isSuperPrimary both set to 1).

        -

        The AggregationExceptions table records the linked RawContacts's IDs in ascending order regardless -of the order used in RAW_CONTACT_ID1 and RAW_CONTACT_ID2 at the time of merging.

        +

        ℹ️ The AggregationExceptions table records the linked RawContacts IDs in ascending order +regardless of the order used in RAW_CONTACT_ID1 and RAW_CONTACT_ID2 at the time of merging.

        The RawContacts and Data table remains the same except the joined contactId column values have now been changed to the id of Contact X. All Data rows' isSuperPrimary value has been set to 0 though @@ -2255,7 +2247,7 @@

        Behavi more info on Contact display name resolution, read the Contact Display Name and Default Name Rows section.

        -

        Note that display name resolution is different for APIs below 21 (pre-lollipop).

        +

        ℹ️ Display name resolution is different for APIs below 21 (pre-lollipop).

        The display name of the RawContacts remain the same.

        The Groups table remains unmodified.

        @@ -2268,17 +2260,16 @@

        Behavi the Data table in a photo mimetype row. A Contact's full-sized photo and thumbnail are simply references to the "chosen" RawContact's full-sized photo and thumbnail (though the URIs may differ).

        -

        Note that when removing the photo in the native contacts app, the photo data row is not -immediately deleted, though the PHOTO_FILE_ID is immediately set to null. This may result in -the PHOTO_URI and PHOTO_THUMBNAIL_URI to still have a valid image uri even though the photo -has been "removed". This library immediately deletes the photo data row, which seems to work -perfectly.

        +

        ℹ️ When removing the photo in the native contacts app, the photo data row is not immediately +deleted, though the PHOTO_FILE_ID is immediately set to null. This may result in the PHOTO_URI +and PHOTO_THUMBNAIL_URI to still have a valid image uri even though the photo has been +"removed". This library immediately deletes the photo data row, which seems to work perfectly.

        Data inserts

        In the native Contacts app, Data inserted in combined (raw) contacts mode will be associated to the first RawContact in the list sorted by the RawContact ID.

        -

        This may not be the same as the RawContact referenced by ContactsColumns.NAME_RAW_CONTACT_ID.

        +

        ℹ️ This may not be the same as the RawContact referenced by ContactsColumns.NAME_RAW_CONTACT_ID.

        UI changes?

        The native Contacts App does not display the groups field when displaying / editing Contacts that @@ -2351,7 +2342,7 @@

        Contact Display Name and Def automatically set as the Contact display name by the Contacts Provider. Otherwise, the Contacts Provider chooses from any of the other suitable data from the aggregate Contact.

        -

        The ContactsColumns.NAME_RAW_CONTACT_ID is automatically updated by the Contacts Provider +

        ℹ️ The ContactsColumns.NAME_RAW_CONTACT_ID is automatically updated by the Contacts Provider along with the display name.

        The default status of other sources (e.g. email) does not affect the Contact display name.

        @@ -2370,7 +2361,7 @@

        Contact Display Name and Def display name somehow. I'm not sure how. If someone figures it out, please let me know. I tried updating the Contact DISPLAY_NAME directly but it does not work. Setting a name row as default also does not affect the Contact DISPLAY_NAME.

        -

        Effects of linking/unlinking contacts

        +

        Effects of linking/unlinking contacts

        When two or more Contacts (along with their constituent RawContacts) are linked into a single Contact those Contacts will be merged into one of the existing Contact row. The Contacts that have been merged into the single Contact will have their entries/rows in the Contacts table deleted.

        diff --git a/permissions/permissions-handling-coroutines/index.html b/permissions/permissions-handling-coroutines/index.html index 87cf17af..73e81e8c 100644 --- a/permissions/permissions-handling-coroutines/index.html +++ b/permissions/permissions-handling-coroutines/index.html @@ -1877,7 +1877,7 @@

        Using withPermission exten

        If permission(s) are not granted, then the operation will immediately fail and the result you get is incorrect (usually null or empty when it should not be).

        -

        Prior to Android 6.0 Marshmallow (API level 23), users are NOT prompted for permission at runtime +

        ℹ️ Prior to Android 6.0 Marshmallow (API level 23), users are NOT prompted for permission at runtime because users must already grant all permissions prior to app install.

        Not compatible with Java

        diff --git a/profile/delete-profile/index.html b/profile/delete-profile/index.html index fa64f993..18cd1cca 100644 --- a/profile/delete-profile/index.html +++ b/profile/delete-profile/index.html @@ -1885,15 +1885,15 @@

        Delete device owner Contact profile

        This library provides the ProfileDelete API, which allows you to delete the device owner Profile Contact or only some of its constituent RawContacts.

        -

        Note that there can be only one device owner Contact, which is either set (not null) or not yet -set (null). However, like other regular Contacts, the Profile Contact may have one or more +

        ℹ️ There can be only one device owner Contact, which is either set (not null) or not yet set +(null). However, like other regular Contacts, the Profile Contact may have one or more RawContacts.

        An instance of the ProfileDelete API is obtained by,

        val delete = Contacts(context).profile().delete()
         
        -

        If you want to delete non-Profile Contacts, read Delete Contacts

        +

        ℹ️ If you want to delete non-Profile Contacts, read Delete Contacts

        A basic delete

        To delete a the profile Contact (if it exist) and all of its RawContacts,

        @@ -1925,13 +1925,13 @@

        Performing t For more info, read Execute work outside of the UI thread using coroutines.

        You may, of course, use other multi-threading libraries or just do it yourself =)

        -

        Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

        +

        ℹ️ Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

        Performing the delete with permission

        Deletes require the android.permission.WRITE_CONTACTS permissions. If not granted, the delete will do nothing and return a failed result.

        -

        For API 22 and below, the permission "android.permission.WRITE_PROFILE" is also required but +

        ℹ️ For API 22 and below, the permission "android.permission.WRITE_PROFILE" is also required but only at the manifest level. Prior to API 23 (Marshmallow), permissions needed to be granted prior to installation instead of at runtime.

        diff --git a/profile/insert-profile/index.html b/profile/insert-profile/index.html index 510181ad..31be2846 100644 --- a/profile/insert-profile/index.html +++ b/profile/insert-profile/index.html @@ -2035,15 +2035,15 @@

        Insert the device owner Contact

        This library provides the ProfileInsert API that allows you to insert one or more RawContacts and Data.

        -

        Note that there can be only one device owner Contact, which is either set (not null) or not yet -set (null). However, like other regular Contacts, the Profile Contact may have one or more +

        ℹ️ There can be only one device owner Contact, which is either set (not null) or not yet set +(null). However, like other regular Contacts, the Profile Contact may have one or more RawContacts.

        An instance of the ProfileInsert API is obtained by,

        val insert = Contacts(context).profile().insert()
         
        -

        If you want to create/insert non-Profile Contacts, read Insert contacts.

        +

        ℹ️ If you want to create/insert non-Profile Contacts, read Insert contacts.

        A basic insert

        To create/insert a raw contact with a name of "John Doe" who works at Amazon with a work email of @@ -2137,18 +2137,18 @@

        Associating an Account
        .forAccount(Account("john.doe@gmail.com", "com.google"))
         
        -

        For more info, read Query for Accounts.

        +

        ℹ️ For more info, read Query for Accounts.

        Local RawContacts

        If no Account is provided, or null is provided, or if an incorrect account is provided, the RawContacts inserted will not be associated with an Account. RawContacts inserted without an associated account are considered local or device-only contacts, which are not synced.

        -

        For more info, read Sync contact data across devices.

        +

        ℹ️ For more info, read Sync contact data across devices.

        There are also certain data kinds that are ignored on insert or update if the RawContact is local.

        -

        For more info, read about Local (device-only) contacts.

        +

        ℹ️ For more info, read about Local (device-only) contacts.

        Including only specific data

        To include only the given set of fields (data) in each of the insert operation,

        @@ -2186,7 +2186,7 @@

        Handling the insert result .find()
        -

        For more info, read Query contacts (advanced).

        +

        ℹ️ For more info, read Query contacts (advanced).

        Alternatively, you may use the extensions provided in ProfileInsertResult. To get the newly created Contact,

        @@ -2216,13 +2216,13 @@

        Performing t For more info, read Execute work outside of the UI thread using coroutines.

        You may, of course, use other multi-threading libraries or just do it yourself =)

        -

        Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

        +

        ℹ️ Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

        Performing the insert with permission

        Inserts require the android.permission.WRITE_CONTACTS and android.permission.GET_ACCOUNTS permissions. If not granted, the insert will do nothing and return a failed result.

        -

        For API 22 and below, the permission "android.permission.WRITE_PROFILE" is also required but +

        ℹ️ For API 22 and below, the permission "android.permission.WRITE_PROFILE" is also required but only at the manifest level. Prior to API 23 (Marshmallow), permissions needed to be granted prior to installation instead of at runtime.

        diff --git a/profile/query-profile/index.html b/profile/query-profile/index.html index 277e492f..ee6fa90a 100644 --- a/profile/query-profile/index.html +++ b/profile/query-profile/index.html @@ -1926,15 +1926,15 @@

        Query device owner Contact profile

        This library provides the ProfileQuery API that allows you to get the device owner Profile Contact.

        -

        Note that there can be only one device owner Contact, which is either set (not null) or not yet -set (null). However, like other regular Contacts, the Profile Contact may have one or more +

        ℹ️ There can be only one device owner Contact, which is either set (not null) or not yet set +(null). However, like other regular Contacts, the Profile Contact may have one or more RawContacts.

        An instance of the ProfileQuery API is obtained by,

        val query = Contacts(context).profile().query()
         
        -

        If you want to get non-Profile Contacts, read +

        ℹ️ If you want to get non-Profile Contacts, read Query contacts and Query contacts (advanced).

        @@ -1955,7 +1955,7 @@

        Specifying Accounts
        .accounts(Account("john.doe@gmail.com", "com.google"))
         
        -

        For more info, read Query for Accounts.

        +

        ℹ️ For more info, read Query for Accounts.

        The RawContacts returned will only belong to the specified accounts.

        If no accounts are specified (this function is not called or called with no Accounts), then all @@ -1965,7 +1965,7 @@

        Specifying AccountsLocal (device-only) contacts.

        -

        Note that this may affect performance. This may require one or more additional queries, internally +

        ℹ️ This may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it.

        @@ -1998,14 +1998,14 @@

        Performing the query asynchronously For more info, read Execute work outside of the UI thread using coroutines.

        You may, of course, use other multi-threading libraries or just do it yourself =)

        -

        Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for +

        ℹ️ Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes.

        Performing the query with permission

        Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return null.

        -

        For API 22 and below, the permission "android.permission.READ_PROFILE" is also required but +

        ℹ️ For API 22 and below, the permission "android.permission.READ_PROFILE" is also required but only at the manifest level. Prior to API 23 (Marshmallow), permissions needed to be granted prior to installation instead of at runtime.

        diff --git a/profile/update-profile/index.html b/profile/update-profile/index.html index 39d675d1..68f5a181 100644 --- a/profile/update-profile/index.html +++ b/profile/update-profile/index.html @@ -1978,10 +1978,12 @@

        Update device owner Contact profile

        -

        This library provides the ProfileUpdate API that allows you to update the device owner Profile Contact.

        +

        This library provides the ProfileUpdate API that allows you to update the Profile contact in the +Contacts Provider database to ensure that it contains the same data as the contact and raw contacts +you have in memory.

        -

        Note that there can be only one device owner Contact, which is either set (not null) or not yet -set (null). However, like other regular Contacts, the Profile Contact may have one or more +

        ℹ️ There can be only one device owner Contact, which is either set (not null) or not yet set +(null). However, like other regular Contacts, the Profile Contact may have one or more RawContacts.

        An instance of the ProfileUpdate API is obtained by,

        @@ -2015,13 +2017,13 @@

        Deleting blanksBlank contacts.

        Blank data are deleted

        Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are -deleted by update APIs.

        +deleted by update APIs, unless the corresponding fields are not included in the operation.

        For more info, read about Blank data.

        Including only specific data

        -

        To include only the given set of fields (data) in each of the update operation,

        +

        To perform update operations only the given set of fields (data),

        .include(fields)
         
        -

        For example, to only include email and name fields,

        +

        For example, to perform updates on only email and name fields,

        .include { Email.all + Name.all }
         

        For more info, read Include only certain fields for read and write operations.

        @@ -2050,7 +2052,7 @@

        Handling the update result
        val updatedProfile = Contacts(context).profile().query().find()
         
        -

        For more info, read Query device owner Contact profile.

        +

        ℹ️ For more info, read Query device owner Contact profile.

        Alternatively, you may use the extensions provided in ContactRefresh and RawContactRefresh.

        To get the updated profile Contact and all of its RawContacts and Data,

        @@ -2080,13 +2082,13 @@

        Performing t For more info, read Execute work outside of the UI thread using coroutines.

        You may, of course, use other multi-threading libraries or just do it yourself =)

        -

        Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

        +

        ℹ️ Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

        Performing the update with permission

        Updates require the android.permission.WRITE_CONTACTS permissions. If not granted, the update will do nothing and return a failed result.

        -

        For API 22 and below, the permission "android.permission.WRITE_PROFILE" is also required but +

        ℹ️ For API 22 and below, the permission "android.permission.WRITE_PROFILE" is also required but only at the manifest level. Prior to API 23 (Marshmallow), permissions needed to be granted prior to installation instead of at runtime.

        diff --git a/search/search_index.json b/search/search_index.json index 747ed9bc..317b3b58 100644 --- a/search/search_index.json +++ b/search/search_index.json @@ -1 +1 @@ -{"config":{"indexing":"full","lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"","text":"Android Contacts, Reborn \u00b6 Written with \u2665\ufe0f and \ud83d\udd25 since December 2018. Open sourced since October 2021. This library provides a complete set of APIs to do everything you need with Contacts in Android. You no longer have to deal with the Contacts Provider , database operations, and cursors. Whether you just need to get all or some Contacts for a small part of your app (written in Kotlin or Java), or you are looking to create your own full-fledged Contacts app with the same capabilities as the native (AOSP) Android Contacts app and Google Contacts app, this library is for you! Please help support this project \ud83d\ude4f\u2764\ufe0f\u2b50\ufe0f Quick links \u00b6 \ud83d\udcdc Documentation \ud83d\ude89 Current release - 0.2.0 \ud83d\ude82 Upcoming release - v0.2.1 \ud83d\uddfa Project roadmap \ud83d\udc8c Why use this library? Features \u00b6 The core module provides, \u2705 All data kinds in the Contacts Provider; address, email, event, group membership, IM, name, nickname, note, organization, phone, photo, relation, SIP address, and website . \u2705 Custom data integration . \u2705 Broad queries and advanced queries of Contacts and RawContacts from zero or more Accounts and/or Groups. \u2705 Contact lookup keys \u2705 Include only desired fields in read/write operations to optimize CPU and memory . \u2705 Powerful, type-safe query DSL . \u2705 Pagination using order by, limit, and offset database functions. \u2705 Insert one or more RawContacts with an associated Account, causing automatic insertion of a new Contact subject to automatic aggregation by the Contacts Provider. \u2705 Update one or more Contacts, RawContacts, and Data. \u2705 Delete one or more Contacts, RawContacts, and Data. \u2705 Query , insert , update , and delete Profile (device owner) Contact, RawContact, and Data. \u2705 Query , insert , update , and delete Groups . \u2705 Query , insert update , and delete specific kinds of data . \u2705 Query , insert , update , and delete custom data . \u2705 Query , insert , and delete Blocked Numbers . \u2705 Query , insert , update , and delete SIM card contacts . \u2705 Query for Accounts in the system or RawContacts table. \u2705 Query for just RawContacts. \u2705 Associate local RawContacts (no Account) to an Account . \u2705 Link/unlink two or more Contacts. \u2705 Get/set contact options ; starred (favorite), custom ringtone, send to voicemail . \u2705 Get/set Contacts/RawContact photo and thumbnail . \u2705 Get/set default (primary) Contact Data (e.g. default/primary phone number, email, etc). \u2705 Convenience functions . \u2705 Contact data is synced automatically across devices . \u2705 Support for logging API input and output \u2705 Redactable entities and API input and output for production-safe logging that upholds user data privacy laws to meet GDPR guidelines (this is not legal advice) . \u2705 Full in-depth documentation/guides . \u2705 Full Java interoptibilty . \u2705 Core APIs have zero dependency . \u2705 Clean separation between Contacts vs RawContacts . \u2705 Clear distinction between truly deeply immutable, mutable, new, and existing entities allowing for thread safety and JetPack compose optimizations . There are also extensions that add functionality to every core function, \ud83e\uddf0 Asynchronous work using Kotlin Coroutines . \ud83e\uddf0 Permissions request/handling using Kotlin Coroutines . \ud83d\udd1c Kotlin Flow extensions \ud83d\udd1c RxJava extensions Also included are some pre-baked goodies to be used as is or just for reference, \ud83c\udf6c Gender custom data . \ud83c\udf6c Google Contacts custom data . \ud83c\udf6c Handle name custom data . \ud83c\udf6c Pokemon custom data \ud83c\udf6c Role Playing Game (RPG) custom data . \ud83c\udf6c Rudimentary contacts-integrated UI components . \ud83c\udf6c Debug functions to aid in development There are also more features that are on the way! \u2622\ufe0f Work profile contacts \u2622\ufe0f Dynamically integrate custom data from other apps \u2622\ufe0f Read/write from/to .VCF file . Installation \u00b6 This library is a multi-module project published with JitPack First, include JitPack in the repositories list, repositories { maven { url \"https://jitpack.io\" } } To install individual modules, dependencies { implementation 'com.github.vestrel00.contacts-android:core:0.2.0' implementation 'com.github.vestrel00.contacts-android:async:0.2.0' implementation 'com.github.vestrel00.contacts-android:customdata-gender:0.2.0' implementation 'com.github.vestrel00.contacts-android:customdata-googlecontacts:0.2.0' implementation 'com.github.vestrel00.contacts-android:customdata-handlename:0.2.0' implementation 'com.github.vestrel00.contacts-android:customdata-pokemon:0.2.0' implementation 'com.github.vestrel00.contacts-android:customdata-rpg:0.2.0' implementation 'com.github.vestrel00.contacts-android:debug:0.2.0' implementation 'com.github.vestrel00.contacts-android:permissions:0.2.0' implementation 'com.github.vestrel00.contacts-android:test:0.2.0' implementation 'com.github.vestrel00.contacts-android:ui:0.2.0' // Notice that when importing specific modules/subprojects, the first \":\" comes after \"contacts-android\". } The core module is really all you need. All other modules are optional. To install all modules in a single line, dependencies { implementation 'com.github.vestrel00:contacts-android:0.2.0' // Notice that when importing all modules, the first \":\" comes after \"vestrel00\". } \u26a0\ufe0f IMPORTANT! Starting with version 0.2.0, installing all modules in a single line is only supported when using the dependencyResolutionManagement in settings.gradle . You are still able to install all modules by specifying them individually. For more info about the different modules and dependency resolution management, read the Installation guide . Setup \u00b6 There is no setup required. It's up to you how you want to create and retain instances of the contacts.core.Contacts(context) API. For more info, read Contacts API Setup . It is also useful to read about API Entities . Quick Start \u00b6 To retrieve all contacts containing all available contact data, val contacts = Contacts ( context ). query (). find () To simply search for Contacts, yielding the exact same results as the native Contacts app, val contacts = Contacts ( context ) . broadQuery () . whereAnyContactDataPartiallyMatches ( searchText ) . find () For more info, read Query contacts . Something a bit more advanced... To retrieve the first 5 contacts (including only the contact id, display name, and phone numbers in the results) ordered by display name in descending order, matching ALL of these rules; - a first name starting with \"leo\" - has emails from gmail or hotmail - lives in the US - has been born prior to making this query - is favorited (starred) - has a nickname of \"DarEdEvil\" (case sensitive) - works for Facebook - has a note - belongs to the account of \"john.doe@gmail.com\" or \"john.doe@myspace.com\" val contacts = Contacts ( context ) . query () . where { ( Name . GivenName startsWith \"leo\" ) and ( Email . Address { endsWith ( \"gmail.com\" ) or endsWith ( \"hotmail.com\" ) }) and ( Address . Country equalToIgnoreCase \"us\" ) and ( Event { ( Date lessThan Date (). toWhereString ()) and ( Type equalTo EventEntity . Type . BIRTHDAY ) }) and ( Contact . Options . Starred equalTo true ) and ( Nickname . Name equalTo \"DarEdEvil\" ) and ( Organization . Company `in` listOf ( \"facebook\" , \"FB\" )) and ( Note . Note . isNotNullOrEmpty ()) } . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" ), Account ( \"john.doe@myspace.com\" , \"com.myspace\" ), ) . include { setOf ( Contact . Id , Contact . DisplayNamePrimary , Phone . Number ) } . orderBy ( ContactsFields . DisplayNamePrimary . desc ()) . offset ( 0 ) . limit ( 5 ) . find () Fore more info, read Query contacts (advanced) . Once you have the contacts, you now have access to all of their data! val contact : Contact Log . d ( \"Contact\" , \"\"\" ID: ${ contact . id } Lookup Key: ${ contact . lookupKey } Display name: ${ contact . displayNamePrimary } Display name alt: ${ contact . displayNameAlt } Photo Uri: ${ contact . photoUri } Thumbnail Uri: ${ contact . photoThumbnailUri } Last updated: ${ contact . lastUpdatedTimestamp } Starred?: ${ contact . options ?. starred } Send to voicemail?: ${ contact . options ?. sendToVoicemail } Ringtone: ${ contact . options ?. customRingtone } Aggregate data from all RawContacts of the contact ----------------------------------- Addresses: ${ contact . addressList () } Emails: ${ contact . emailList () } Events: ${ contact . eventList () } Group memberships: ${ contact . groupMembershipList () } IMs: ${ contact . imList () } Names: ${ contact . nameList () } Nicknames: ${ contact . nicknameList () } Notes: ${ contact . noteList () } Organizations: ${ contact . organizationList () } Phones: ${ contact . phoneList () } Relations: ${ contact . relationList () } SipAddresses: ${ contact . sipAddressList () } Websites: ${ contact . websiteList () } ----------------------------------- \"\"\" . trimIndent () // There are also aggregate data functions that return a sequence instead of a list. ) For more info, read about API Entities . More than enough APIs that will allow you to build your own contacts app! \u00b6 This library is capable of doing more than just querying contacts. Actually, you can build your own full-fledged contacts app with it! Let's take a look at a few other APIs this library provides... To get the first 20 gmail emails ordered by email address in descending order, val emails = Contacts ( context ) . data () . query () . emails () . where { Email . Address endsWith \"gmail.com\" } . orderBy ( Fields . Email . Address . desc ( ignoreCase = true )) . offset ( 0 ) . limit ( 20 ) . find () It's not just for emails. It's for all data kinds (including custom data). For more info, read Query specific data kinds . To CREATE/INSERT a contact with a name of \"John Doe\" who works at Amazon with a work email of \"john.doe@amazon.com\" (in Kotlin), val insertResult = Contacts ( context ) . insert () . rawContacts ( NewRawContact (). apply { name = NewName (). apply { givenName = \"John\" familyName = \"Doe\" } organization = NewOrganization (). apply { company = \"Amazon\" title = \"Superstar\" } emails . add ( NewEmail (). apply { address = \"john.doe@amazon.com\" type = EmailEntity . Type . WORK }) }) . commit () Or alternatively, in a more Kotlinized style using named arguments, val insertResult = Contacts ( context ) . insert () . rawContacts ( NewRawContact ( name = NewName ( givenName = \"John\" , familyName = \"Doe\" ), organization = NewOrganization ( company = \"Amazon\" , title = \"Superstar\" ), emails = mutableListOf ( NewEmail ( address = \"john.doe@amazon.com\" , type = EmailEntity . Type . WORK )) )) . commit () Or alternatively, using extension functions, val insertResult = Contacts ( context ) . insert () . rawContact { setName { givenName = \"John\" familyName = \"Doe\" } setOrganization { company = \"Amazon\" title = \"Superstar\" } addEmail { address = \"john.doe@amazon.com\" type = EmailEntity . Type . WORK } } . commit () For more info, read Insert contacts . If John Doe switches jobs and heads over to Microsoft, we can UPDATE his data, Contacts ( context ) . update () . contacts ( johnDoe . mutableCopy { setOrganization { company = \"Microsoft\" title = \"Newb\" } emails (). first (). apply { address = \"john.doe@microsoft.com\" } }) . commit () For more info, read Update contacts . If we no longer like John Doe, we can DELETE him from our life, Contacts ( context ) . delete () . contacts ( johnDoe ) . commit () For more info, read Delete Contacts . Threading and permissions \u00b6 This library provides Kotlin coroutine extensions in the permissions module for all API functions to handle permissions and async module for executing work in background threads. launch { val contacts = Contacts ( context ) . queryWithPermission () ... . findWithContext () val deferredResult = Contacts ( context ) . insertWithPermission () ... . commitAsync () val result = deferredResult . await () } For more info, read Permissions handling using coroutines and Execute work outside of the UI thread using coroutines . So, if we call the above function and we don't yet have permission. The user will be prompted to give the appropriate permissions before the query proceeds. Then, the work is done in the coroutine context of choice (default is Dispatchers.IO). If the user does not give permission, the query will return no results. Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes. Full documentation, guides, and samples \u00b6 The above examples barely scratches the surface of what this library provides. For more in-depth documentation, visit the GitHub Pages . For a sample app reference, take a look at and run the sample module. All APIs in the library are optimized! \u00b6 Some other APIs or util functions out there typically perform one internal database query per contact returned. They do this to fetch the data per contact. This means that if there are 1,000 matching contacts, then an extra 1,000 internal database queries are performed! This is not cool! To address this issue, the query APIs provided in the Contacts, Reborn library, perform only at least two and at most six or seven internal database queries no matter how many contacts are matched! Even if there are 100,000 contacts matched, the library will only perform two to seven internal database queries (depending on your query parameters). Of course, if you don't want to fetch all hundreds of thousands of contacts, the query APIs support pagination with limit and offset functions :sunglasses: Cancellations are also supported! To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val contacts = query . find { ! isActive } } // Or, using the coroutine extensions in the async module... val contacts = query . findWithContext () } All core APIs are framework-agnostic and works well with Java and Kotlin \u00b6 The API does not and will not force you to use any frameworks (e.g. RxJava or Coroutines/Flow)! All core functions of the API live in the core module, which you can import to your project all by itself. Don't believe me? Take a look at the dependencies in the core/build.gradle :D So, feel free to use the core API however you want with whatever libraries or frameworks you want, such as Reactive, Coroutines/Flow, AsyncTask (hope not), WorkManager, and whatever permissions handling APIs you want to use. All other modules in this library are optional and are just there for your convenience or for reference. I also made sure that all core functions and entities are interoperable with Java. So, if you were wondering why I\u2019m using a semi-builder pattern instead of using named arguments with default values, that is why. I\u2019ve also made some other intentional decisions about API design to ensure the best possible experience for both Kotlin and Java consumers without sacrificing Kotlin language standards. It is Kotlin-first, Java-second (with love and care). Modules other than the core module are not guaranteed to be compatible with Java. Requirements \u00b6 Min SDK 19+ Proguard \u00b6 If you use Proguard and the async and/or permissions , you may need to add rules for Coroutines . License \u00b6 Copyright 2022 Contacts Contributors Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.","title":"Overview"},{"location":"#android-contacts-reborn","text":"Written with \u2665\ufe0f and \ud83d\udd25 since December 2018. Open sourced since October 2021. This library provides a complete set of APIs to do everything you need with Contacts in Android. You no longer have to deal with the Contacts Provider , database operations, and cursors. Whether you just need to get all or some Contacts for a small part of your app (written in Kotlin or Java), or you are looking to create your own full-fledged Contacts app with the same capabilities as the native (AOSP) Android Contacts app and Google Contacts app, this library is for you! Please help support this project \ud83d\ude4f\u2764\ufe0f\u2b50\ufe0f","title":"Android Contacts, Reborn"},{"location":"#quick-links","text":"\ud83d\udcdc Documentation \ud83d\ude89 Current release - 0.2.0 \ud83d\ude82 Upcoming release - v0.2.1 \ud83d\uddfa Project roadmap \ud83d\udc8c Why use this library?","title":"Quick links"},{"location":"#features","text":"The core module provides, \u2705 All data kinds in the Contacts Provider; address, email, event, group membership, IM, name, nickname, note, organization, phone, photo, relation, SIP address, and website . \u2705 Custom data integration . \u2705 Broad queries and advanced queries of Contacts and RawContacts from zero or more Accounts and/or Groups. \u2705 Contact lookup keys \u2705 Include only desired fields in read/write operations to optimize CPU and memory . \u2705 Powerful, type-safe query DSL . \u2705 Pagination using order by, limit, and offset database functions. \u2705 Insert one or more RawContacts with an associated Account, causing automatic insertion of a new Contact subject to automatic aggregation by the Contacts Provider. \u2705 Update one or more Contacts, RawContacts, and Data. \u2705 Delete one or more Contacts, RawContacts, and Data. \u2705 Query , insert , update , and delete Profile (device owner) Contact, RawContact, and Data. \u2705 Query , insert , update , and delete Groups . \u2705 Query , insert update , and delete specific kinds of data . \u2705 Query , insert , update , and delete custom data . \u2705 Query , insert , and delete Blocked Numbers . \u2705 Query , insert , update , and delete SIM card contacts . \u2705 Query for Accounts in the system or RawContacts table. \u2705 Query for just RawContacts. \u2705 Associate local RawContacts (no Account) to an Account . \u2705 Link/unlink two or more Contacts. \u2705 Get/set contact options ; starred (favorite), custom ringtone, send to voicemail . \u2705 Get/set Contacts/RawContact photo and thumbnail . \u2705 Get/set default (primary) Contact Data (e.g. default/primary phone number, email, etc). \u2705 Convenience functions . \u2705 Contact data is synced automatically across devices . \u2705 Support for logging API input and output \u2705 Redactable entities and API input and output for production-safe logging that upholds user data privacy laws to meet GDPR guidelines (this is not legal advice) . \u2705 Full in-depth documentation/guides . \u2705 Full Java interoptibilty . \u2705 Core APIs have zero dependency . \u2705 Clean separation between Contacts vs RawContacts . \u2705 Clear distinction between truly deeply immutable, mutable, new, and existing entities allowing for thread safety and JetPack compose optimizations . There are also extensions that add functionality to every core function, \ud83e\uddf0 Asynchronous work using Kotlin Coroutines . \ud83e\uddf0 Permissions request/handling using Kotlin Coroutines . \ud83d\udd1c Kotlin Flow extensions \ud83d\udd1c RxJava extensions Also included are some pre-baked goodies to be used as is or just for reference, \ud83c\udf6c Gender custom data . \ud83c\udf6c Google Contacts custom data . \ud83c\udf6c Handle name custom data . \ud83c\udf6c Pokemon custom data \ud83c\udf6c Role Playing Game (RPG) custom data . \ud83c\udf6c Rudimentary contacts-integrated UI components . \ud83c\udf6c Debug functions to aid in development There are also more features that are on the way! \u2622\ufe0f Work profile contacts \u2622\ufe0f Dynamically integrate custom data from other apps \u2622\ufe0f Read/write from/to .VCF file .","title":"Features"},{"location":"#installation","text":"This library is a multi-module project published with JitPack First, include JitPack in the repositories list, repositories { maven { url \"https://jitpack.io\" } } To install individual modules, dependencies { implementation 'com.github.vestrel00.contacts-android:core:0.2.0' implementation 'com.github.vestrel00.contacts-android:async:0.2.0' implementation 'com.github.vestrel00.contacts-android:customdata-gender:0.2.0' implementation 'com.github.vestrel00.contacts-android:customdata-googlecontacts:0.2.0' implementation 'com.github.vestrel00.contacts-android:customdata-handlename:0.2.0' implementation 'com.github.vestrel00.contacts-android:customdata-pokemon:0.2.0' implementation 'com.github.vestrel00.contacts-android:customdata-rpg:0.2.0' implementation 'com.github.vestrel00.contacts-android:debug:0.2.0' implementation 'com.github.vestrel00.contacts-android:permissions:0.2.0' implementation 'com.github.vestrel00.contacts-android:test:0.2.0' implementation 'com.github.vestrel00.contacts-android:ui:0.2.0' // Notice that when importing specific modules/subprojects, the first \":\" comes after \"contacts-android\". } The core module is really all you need. All other modules are optional. To install all modules in a single line, dependencies { implementation 'com.github.vestrel00:contacts-android:0.2.0' // Notice that when importing all modules, the first \":\" comes after \"vestrel00\". } \u26a0\ufe0f IMPORTANT! Starting with version 0.2.0, installing all modules in a single line is only supported when using the dependencyResolutionManagement in settings.gradle . You are still able to install all modules by specifying them individually. For more info about the different modules and dependency resolution management, read the Installation guide .","title":"Installation"},{"location":"#setup","text":"There is no setup required. It's up to you how you want to create and retain instances of the contacts.core.Contacts(context) API. For more info, read Contacts API Setup . It is also useful to read about API Entities .","title":"Setup"},{"location":"#quick-start","text":"To retrieve all contacts containing all available contact data, val contacts = Contacts ( context ). query (). find () To simply search for Contacts, yielding the exact same results as the native Contacts app, val contacts = Contacts ( context ) . broadQuery () . whereAnyContactDataPartiallyMatches ( searchText ) . find () For more info, read Query contacts . Something a bit more advanced... To retrieve the first 5 contacts (including only the contact id, display name, and phone numbers in the results) ordered by display name in descending order, matching ALL of these rules; - a first name starting with \"leo\" - has emails from gmail or hotmail - lives in the US - has been born prior to making this query - is favorited (starred) - has a nickname of \"DarEdEvil\" (case sensitive) - works for Facebook - has a note - belongs to the account of \"john.doe@gmail.com\" or \"john.doe@myspace.com\" val contacts = Contacts ( context ) . query () . where { ( Name . GivenName startsWith \"leo\" ) and ( Email . Address { endsWith ( \"gmail.com\" ) or endsWith ( \"hotmail.com\" ) }) and ( Address . Country equalToIgnoreCase \"us\" ) and ( Event { ( Date lessThan Date (). toWhereString ()) and ( Type equalTo EventEntity . Type . BIRTHDAY ) }) and ( Contact . Options . Starred equalTo true ) and ( Nickname . Name equalTo \"DarEdEvil\" ) and ( Organization . Company `in` listOf ( \"facebook\" , \"FB\" )) and ( Note . Note . isNotNullOrEmpty ()) } . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" ), Account ( \"john.doe@myspace.com\" , \"com.myspace\" ), ) . include { setOf ( Contact . Id , Contact . DisplayNamePrimary , Phone . Number ) } . orderBy ( ContactsFields . DisplayNamePrimary . desc ()) . offset ( 0 ) . limit ( 5 ) . find () Fore more info, read Query contacts (advanced) . Once you have the contacts, you now have access to all of their data! val contact : Contact Log . d ( \"Contact\" , \"\"\" ID: ${ contact . id } Lookup Key: ${ contact . lookupKey } Display name: ${ contact . displayNamePrimary } Display name alt: ${ contact . displayNameAlt } Photo Uri: ${ contact . photoUri } Thumbnail Uri: ${ contact . photoThumbnailUri } Last updated: ${ contact . lastUpdatedTimestamp } Starred?: ${ contact . options ?. starred } Send to voicemail?: ${ contact . options ?. sendToVoicemail } Ringtone: ${ contact . options ?. customRingtone } Aggregate data from all RawContacts of the contact ----------------------------------- Addresses: ${ contact . addressList () } Emails: ${ contact . emailList () } Events: ${ contact . eventList () } Group memberships: ${ contact . groupMembershipList () } IMs: ${ contact . imList () } Names: ${ contact . nameList () } Nicknames: ${ contact . nicknameList () } Notes: ${ contact . noteList () } Organizations: ${ contact . organizationList () } Phones: ${ contact . phoneList () } Relations: ${ contact . relationList () } SipAddresses: ${ contact . sipAddressList () } Websites: ${ contact . websiteList () } ----------------------------------- \"\"\" . trimIndent () // There are also aggregate data functions that return a sequence instead of a list. ) For more info, read about API Entities .","title":"Quick Start"},{"location":"#more-than-enough-apis-that-will-allow-you-to-build-your-own-contacts-app","text":"This library is capable of doing more than just querying contacts. Actually, you can build your own full-fledged contacts app with it! Let's take a look at a few other APIs this library provides... To get the first 20 gmail emails ordered by email address in descending order, val emails = Contacts ( context ) . data () . query () . emails () . where { Email . Address endsWith \"gmail.com\" } . orderBy ( Fields . Email . Address . desc ( ignoreCase = true )) . offset ( 0 ) . limit ( 20 ) . find () It's not just for emails. It's for all data kinds (including custom data). For more info, read Query specific data kinds . To CREATE/INSERT a contact with a name of \"John Doe\" who works at Amazon with a work email of \"john.doe@amazon.com\" (in Kotlin), val insertResult = Contacts ( context ) . insert () . rawContacts ( NewRawContact (). apply { name = NewName (). apply { givenName = \"John\" familyName = \"Doe\" } organization = NewOrganization (). apply { company = \"Amazon\" title = \"Superstar\" } emails . add ( NewEmail (). apply { address = \"john.doe@amazon.com\" type = EmailEntity . Type . WORK }) }) . commit () Or alternatively, in a more Kotlinized style using named arguments, val insertResult = Contacts ( context ) . insert () . rawContacts ( NewRawContact ( name = NewName ( givenName = \"John\" , familyName = \"Doe\" ), organization = NewOrganization ( company = \"Amazon\" , title = \"Superstar\" ), emails = mutableListOf ( NewEmail ( address = \"john.doe@amazon.com\" , type = EmailEntity . Type . WORK )) )) . commit () Or alternatively, using extension functions, val insertResult = Contacts ( context ) . insert () . rawContact { setName { givenName = \"John\" familyName = \"Doe\" } setOrganization { company = \"Amazon\" title = \"Superstar\" } addEmail { address = \"john.doe@amazon.com\" type = EmailEntity . Type . WORK } } . commit () For more info, read Insert contacts . If John Doe switches jobs and heads over to Microsoft, we can UPDATE his data, Contacts ( context ) . update () . contacts ( johnDoe . mutableCopy { setOrganization { company = \"Microsoft\" title = \"Newb\" } emails (). first (). apply { address = \"john.doe@microsoft.com\" } }) . commit () For more info, read Update contacts . If we no longer like John Doe, we can DELETE him from our life, Contacts ( context ) . delete () . contacts ( johnDoe ) . commit () For more info, read Delete Contacts .","title":"More than enough APIs that will allow you to build your own contacts app!"},{"location":"#threading-and-permissions","text":"This library provides Kotlin coroutine extensions in the permissions module for all API functions to handle permissions and async module for executing work in background threads. launch { val contacts = Contacts ( context ) . queryWithPermission () ... . findWithContext () val deferredResult = Contacts ( context ) . insertWithPermission () ... . commitAsync () val result = deferredResult . await () } For more info, read Permissions handling using coroutines and Execute work outside of the UI thread using coroutines . So, if we call the above function and we don't yet have permission. The user will be prompted to give the appropriate permissions before the query proceeds. Then, the work is done in the coroutine context of choice (default is Dispatchers.IO). If the user does not give permission, the query will return no results. Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes.","title":"Threading and permissions"},{"location":"#full-documentation-guides-and-samples","text":"The above examples barely scratches the surface of what this library provides. For more in-depth documentation, visit the GitHub Pages . For a sample app reference, take a look at and run the sample module.","title":"Full documentation, guides, and samples"},{"location":"#all-apis-in-the-library-are-optimized","text":"Some other APIs or util functions out there typically perform one internal database query per contact returned. They do this to fetch the data per contact. This means that if there are 1,000 matching contacts, then an extra 1,000 internal database queries are performed! This is not cool! To address this issue, the query APIs provided in the Contacts, Reborn library, perform only at least two and at most six or seven internal database queries no matter how many contacts are matched! Even if there are 100,000 contacts matched, the library will only perform two to seven internal database queries (depending on your query parameters). Of course, if you don't want to fetch all hundreds of thousands of contacts, the query APIs support pagination with limit and offset functions :sunglasses: Cancellations are also supported! To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val contacts = query . find { ! isActive } } // Or, using the coroutine extensions in the async module... val contacts = query . findWithContext () }","title":"All APIs in the library are optimized!"},{"location":"#all-core-apis-are-framework-agnostic-and-works-well-with-java-and-kotlin","text":"The API does not and will not force you to use any frameworks (e.g. RxJava or Coroutines/Flow)! All core functions of the API live in the core module, which you can import to your project all by itself. Don't believe me? Take a look at the dependencies in the core/build.gradle :D So, feel free to use the core API however you want with whatever libraries or frameworks you want, such as Reactive, Coroutines/Flow, AsyncTask (hope not), WorkManager, and whatever permissions handling APIs you want to use. All other modules in this library are optional and are just there for your convenience or for reference. I also made sure that all core functions and entities are interoperable with Java. So, if you were wondering why I\u2019m using a semi-builder pattern instead of using named arguments with default values, that is why. I\u2019ve also made some other intentional decisions about API design to ensure the best possible experience for both Kotlin and Java consumers without sacrificing Kotlin language standards. It is Kotlin-first, Java-second (with love and care). Modules other than the core module are not guaranteed to be compatible with Java.","title":"All core APIs are framework-agnostic and works well with Java and Kotlin"},{"location":"#requirements","text":"Min SDK 19+","title":"Requirements"},{"location":"#proguard","text":"If you use Proguard and the async and/or permissions , you may need to add rules for Coroutines .","title":"Proguard"},{"location":"#license","text":"Copyright 2022 Contacts Contributors Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.","title":"License"},{"location":"contributing/","text":"Contributing \u00b6 There are only a few loose guidelines to follow. Read the Setup and Guidelines sections. Setup \u00b6 To open, build, and run this project, you will need to use Android Studio Bumblebee 2021.1.1 and later versions. If you run into any build issues, open up Android Studio preferences and make sure the following is set correctly... Build, Execution, Deployment -> Build Tools -> Gradle Use Gradle from: 'gradlew-wrapper.properties' file Gradle JDK: Embedded JDK version 11.x.x Language & Frameworks -> Kotlin Current Kotlin plugin version: 211-1.6.10-release-923-AS7442.40 Restart Android Studio, clean, build, and invalidate caches & restart. Guidelines \u00b6 Simple is better. Over-engineering is not welcome here. Don't over complicate function implementations unnecessarily, especially the public API. Less is more. If you have not noticed yet, the dependency list of this project is almost non-existent. The core module only depends on Kotlin's standard library. Not even the support annotations are included (though this is questionable and may change quickly). All modules only have dependencies on essentials. Nice-to-haves are excluded. Contacts have been here since API 1. In that spirit, we should not need to import tons of unnecessary dependencies to deliver the most basic Android API. Java compatibility is a must. Java is not dead even in Android, though it may seem like it. There are still probably a lot of people that have not migrated over to Kotlin. This is especially true for larger organizations with large code bases and unable to afford migrating to Kotlin. The API must be usable in Java, with exceptions to Kotlin-specific modules (e.g. async, permissions). Be patient. Early on, I (Vandolf) will be the only one to approve incoming code. It may take a few days for me to review code and decline/approve. I have a full time job after all =) As time passes, I'm hoping to give the power of approvals to others in the community. Uphold the spirit of Contacts, Reborn! Don't deviate from the existing API design. New code should follow existing API design to promote uniformity. It'll be easier to maintain and cross-pollinate.","title":"Contributing"},{"location":"contributing/#contributing","text":"There are only a few loose guidelines to follow. Read the Setup and Guidelines sections.","title":"Contributing"},{"location":"contributing/#setup","text":"To open, build, and run this project, you will need to use Android Studio Bumblebee 2021.1.1 and later versions. If you run into any build issues, open up Android Studio preferences and make sure the following is set correctly... Build, Execution, Deployment -> Build Tools -> Gradle Use Gradle from: 'gradlew-wrapper.properties' file Gradle JDK: Embedded JDK version 11.x.x Language & Frameworks -> Kotlin Current Kotlin plugin version: 211-1.6.10-release-923-AS7442.40 Restart Android Studio, clean, build, and invalidate caches & restart.","title":"Setup"},{"location":"contributing/#guidelines","text":"Simple is better. Over-engineering is not welcome here. Don't over complicate function implementations unnecessarily, especially the public API. Less is more. If you have not noticed yet, the dependency list of this project is almost non-existent. The core module only depends on Kotlin's standard library. Not even the support annotations are included (though this is questionable and may change quickly). All modules only have dependencies on essentials. Nice-to-haves are excluded. Contacts have been here since API 1. In that spirit, we should not need to import tons of unnecessary dependencies to deliver the most basic Android API. Java compatibility is a must. Java is not dead even in Android, though it may seem like it. There are still probably a lot of people that have not migrated over to Kotlin. This is especially true for larger organizations with large code bases and unable to afford migrating to Kotlin. The API must be usable in Java, with exceptions to Kotlin-specific modules (e.g. async, permissions). Be patient. Early on, I (Vandolf) will be the only one to approve incoming code. It may take a few days for me to review code and decline/approve. I have a full time job after all =) As time passes, I'm hoping to give the power of approvals to others in the community. Uphold the spirit of Contacts, Reborn! Don't deviate from the existing API design. New code should follow existing API design to promote uniformity. It'll be easier to maintain and cross-pollinate.","title":"Guidelines"},{"location":"dev-notes/","text":"Developer Notes \u00b6 This document contains useful developer notes that should be kept in mind during development. It serves as a memory of all the quirks and gotcha's of things like Android's ContactsContract . This is only meant to be read by contributors of this library, not consumers! Contacts Provider / ContactsContract \u00b6 It is important to know about the ins and outs of Android's Contacts Provider. After all, this API is just a wrapper around it. A very sweet, sugary wrapper! Sugar. Spice. And everything nice. :D It is important to get familiar with the official documentation of the Contact's Provider . Here is a summary; There are 3 main database tables used in dealing with contacts; Contacts RawContacts Data There are more but that is covered later. All of these tables and their fields are enumerated and documented in android.provider.ContactsContract . Each table serves a different purpose; Contacts Rows representing different people. RawContacts Rows that link Contacts rows to specific Accounts. Data Rows containing data (e.g. name, email) for a RawContacts row. These tables contain the following (notable) information (columns); Contacts _ID DISPLAY_NAME_PRIMARY RawContacts _ID : the Contacts._ID ACCOUNT_NAME : the Account.name ACCOUNT_TYPE the Account.type Data RAW_CONTACT_ID : the RawContacts._ID CONTACT_ID : the Contacts._ID DATA_1 to DATA_15 : contains a piece of contact data (e.g. first and last name, email address and type) determined by the MIMETYPE MIMETYPE : the type of data that this row's DATA_X columns contain (e.g. name and email data) The tables are connected the following way; RawContacts contains a reference to the Contacts row Id. Data contains a reference to the RawContacts row Id and Contacts row Id. Contacts; Display Name \u00b6 The Contacts.DISPLAY_NAME name may be different than the Data StructuredName display name! If a structured name in the Data table is not provided, then other kinds of data will be used as the Contacts row display name. For example, if an email is provided but no structured name then the display name will be the email. When a structured name is inserted, the Contacts Provider automatically updates the Contacts row display name. In the case of StructuredName , the Contacts.DISPLAY_NAME is made up of the prefix, given, middle, family name, and suffix and not the unstructured display name. If no data rows suitable to be a display name are available, then the Contacts row display name will be null. Data suitable to be a Contacts row display name are enumerated in DisplayNameSources ; email nickname organization phone number structured name Data not suitable to be display names are; address event group im note relation sip website The kind of data used as the display for the Contact is set in ContactNameColumns.DISPLAY_NAME_SOURCE . A note about StructuredName There may be a scenario where the unstructured StructuredName.DISPLAY_NAME does not match the structured components. Such scenarios are possible but is considered incorrect. For example, it is possible to programmatically set the display name to \"Ice Cold\" but set the given and family name to \"Hot Fire\". The Contacts.DISPLAY_NAME is made up of the prefix, given, middle, family name, and suffix (\"Hot Fire\") and not the unstructured display name. The Contacts Provider's general matching algorithm does not include the Contacts.DISPLAY_NAME . However, the StructuredName.DISPLAY_NAME is included in the matching process but not the rest of the structured components (e.g. given and family name). The native Contacts app displays the Contacts.DISPLAY_NAME . So, here comes the unusual scenario that looks like a bug. The general matching algorithm will match the text \"Ice\" or \"Cold\" but not \"Hot\" or \"Fire\". The end result is that searching for the Contact \"Ice Cold\" will show a Contact called \"Hot Fire\"! Contact Display Name and Default Name Rows \u00b6 If available, the \"default\" (isPrimary and isSuperPrimary set to 1) name row for a Contact is automatically set as the Contact display name by the Contacts Provider. Otherwise, the Contacts Provider chooses from any of the other suitable data from the aggregate Contact. The ContactsColumns.NAME_RAW_CONTACT_ID is automatically updated by the Contacts Provider along with the display name. The default status of other sources (e.g. email) does not affect the Contact display name. The native Contacts app also sets the most recently updated name as the default at every update. This results in the Contact display name changing to the most recently updated name from one of the associated RawContacts. The \"most recently updated name\" is the name field that was last updated by the user when editing in the Contacts app, which is irrelevant to its value. It does not matter if the user deleted the last character of the name, added the same character back, and then saved. It still counts as the most recently updated. All of the above only applies to API 21 and above. Display name resolution is different for APIs below 21 (pre-Lollipop)! The ContactsColumns.NAME_RAW_CONTACT_ID was added in API 21. It changed the way display names are resolved for Contacts with more than one constituent RawContacts, which is what has been described so far. Before this change (APIs 20 and below), the native Contacts app is still able to set the Contact display name somehow. I'm not sure how. If someone figures it out, please let me know. I tried updating the Contact DISPLAY_NAME directly but it does not work. Setting a name row as default also does not affect the Contact DISPLAY_NAME . Contacts; ID vs LOOKUP_KEY \u00b6 The Contacts._ID is the unique identifier for the row in the Contacts table. The Contacts.LOOKUP_KEY is the unique identifier for an aggregate Contact (a person). The _ID may change due to aggregation and sync. The same goes for the LOOKUP_KEY but unlike the ID it may still be used to find the aggregate contact. Unlike the Contact ID, the lookup key is the same across devices (for contacts that are associated with an Account and are synced). Note that I did the following investigation with a much larger data set. I simplified it here for brevity. Let's take a look at the following Contacts and RawContacts table rows, #### Contacts table Contact id: 55, lookupKey: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50, displayNamePrimary: Contact With Local RawContact Contact id: 56, lookupKey: 2059i6f5de8460f7f227e, displayNamePrimary: Contact With Synced RawContact #### RawContacts table RawContact id: 55, contactId: 55, displayNamePrimary: Contact With Local RawContact RawContact id: 56, contactId: 56, displayNamePrimary: Contact With Synced RawContact There are two Contacts each having one RawContact. Notice that the lookup keys are a bit different. Contact With Local RawContact: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 Contact With Synced RawContact: 2059i6f5de8460f7f227e The Contact with unsynced, device-only, local RawContact has a much longer (or shorter e.g. 0r62-2A2C2E) lookup key and starts with \"0r -\" and all characters after it are in uppercase. The other thing to notice is that the \"55\" in \"0r55-\" seems to be the same as the RawContact ID (I did a bit more experiments than what is written in these notes to confirm that it is indeed the RawContact ID and not the Contact ID). We probably don't need to worry about these details though the Contacts Provider probably uses these things internally. We also should not rely on it. However, it may be safe to assume that the Contact lookup key is a reference to a RawContact (or reference to more than one constituent RawContact when multiple RawContacts are linked). Again, an internal Contacts Provider detail we should not rely on BUT is probably relevant when implementing sync adapters. When we link the two, we get... Contact id: 55, lookupKey: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50.2059i6f5de8460f7f227e, displayNamePrimary: Contact With Synced RawContact #### RawContacts table RawContact id: 55, contactId: 55, displayNamePrimary: Contact With Local RawContact RawContact id: 56, contactId: 55, displayNamePrimary: Contact With Synced RawContact Notice, Contact with ID 56 has been deleted. Contact with ID 55 still exist with the lookup keys of both Contact 55 and 56 combined separated by a \".\" . This holds true in cases where two or more local-only or non-local-only RawContacts are linked. RawContacts remain unchanged except reference to Contact 56 has been replaced with 55. The primary display name of Contact 55 came from RawContact 55 prior to the link and now comes from RawContact 56 after the link. This primary name resolution is probably irrelevant so pay no attention to it. The most important part to notice is that the lookup keys get combined. The lookup uri is required to build a Contacts.CONTENT_LOOKUP_URI ... /** * A content:// style URI for this table that should be used to create * shortcuts or otherwise create long-term links to contacts. This URI * should always be followed by a \"/\" and the contact's {@link #LOOKUP_KEY}. * It can optionally also have a \"/\" and last known contact ID appended after * that. This \"complete\" format is an important optimization and is highly recommended. *

        * As long as the contact's row ID remains the same, this URI is * equivalent to {@link #CONTENT_URI}. If the contact's row ID changes * as a result of a sync or aggregation, this URI will look up the * contact using indirect information (sync IDs or constituent raw * contacts). *

        * Lookup key should be appended unencoded - it is stored in the encoded * form, ready for use in a URI. */ public static final Uri CONTENT_LOOKUP_URI = Uri . withAppendedPath ( CONTENT_URI , \"lookup\" ); /** * Build a {@link #CONTENT_LOOKUP_URI} lookup {@link Uri} using the * given {@link ContactsContract.Contacts#_ID} and {@link #LOOKUP_KEY}. *

        * Returns null if unable to construct a valid lookup URI from the * provided parameters. */ public static Uri getLookupUri ( long contactId , String lookupKey ) { if ( TextUtils . isEmpty ( lookupKey )) { return null ; } return ContentUris . withAppendedId ( Uri . withAppendedPath ( Contacts . CONTENT_LOOKUP_URI , lookupKey ), contactId ); } From the lookup uri, we can lookup the Contact row... public static Uri lookupContact ( ContentResolver resolver , Uri lookupUri ) { ... } Or simply get the Contact ID... // code inside `public static Uri lookupContact` resolver . query ( lookupUri , new String [] { Contacts . _ID }, null , null , null ) However, given that the lookup key of the deleted Contact 56 still lives on, it is possible to get the linked Contact 55 using the lookup key of Contact 56 using our standard query APIs! . where { Contact . LookupKey contains lookupKey } The above is correct as long as these assumptions hold true; the lookup key is unique there is no lookup key that can contain a shorter lookup key the Contact ID fails this test because a smaller number is contained in a larger number synced contacts have shorter lookup keys than local contacts. However, local contacts' lookup keys are capitalized whereas synced contact are not. Also, there seems to be other differences in pattern between long and short lookup keys. It should be safe to make this assumption. Until the community finds that this assumption is flawed, we'll assume that it is true! For now, we can avoid having to create another API or extensions just for using lookup keys . When we unlink , we get... #### Contacts table Contact id: 55, lookupKey: 2059i6f5de8460f7f227e, displayNamePrimary: Contact With Synced RawContact Contact id: 58, lookupKey: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50, displayNamePrimary: Contact With Local RawContact #### RawContacts table RawContact id: 55, contactId: 58, displayNamePrimary: Contact With Local RawContact RawContact id: 56, contactId: 55, displayNamePrimary: Contact With Synced RawContact Notice, A new Contact row with ID of 58 is created. The lookup keys are separated and distributed between Contact 55 and 58. RawContact 55 Contact reference has been set to Contact 58. Let's compare the Contact-RawContact relationship before and after linking and then unlinking. Contact ID Lookup Key RawContact.Contact ID Before 55, 56 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50, 2059i6f5de8460f7f227e 55, 56 After 55, 58 2059i6f5de8460f7f227e, 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 58, 55 Notice, Contact ID 55 swapped lookup keys with the former Contact 56 (now 58). RawContact ID 55 swapped Contact reference with RawContact 56. The Contact IDs and lookup keys got shuffled BUT the Contact-RawContact relationship remains the same if using the lookup keys as point of reference! Here is another way to look at the table, using the lookup key as the constant... Lookup Key Before After 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 Contact 55, RawContact 55 Contact 58, RawContact 55 2059i6f5de8460f7f227e Contact 56, RawContact 56 Contact 55, RawContact 56 Notice that the indirect relationship between the lookup key and RawContacts remains the same before and after the link-unlink even though the Contact IDs changed. As mentioned earlier in this section, the \"55\" in \"0r55-\" seems to be referencing the RawContact ID. In other words, since local RawContacts are not synced or tracked in a remote database where Contacts -> RawContacts mappings exist, the Contacts Provider most likely uses this \"0r -\" pattern to make the connection. This is not really relevant for us as we are not relying on this mechanism. I'm just pointing out my observations, which could be incorrect. This means that... If users of this library saved a reference Contact ID 55, then a link-unlink (or sync adapter functions) occur. Getting Contact by ID 55 will result in the RawContact-Data of the former Contact 56 to be returned. This is a bug! Same goes if users saved a reference to Contact ID 56. If users of this library saved a reference to the lookup keys, then a link-unlink (or sync adapter functions) occur. Getting Contact by lookup key will result in the correct RawContact-Data to be returned. So when to use Contact ID vs lookup key? Lookup key: for a reference to a Contact that needs to be loaded after some period of time. Saving/restoring activity/fragment instance state. Saving to an external database, preferences, or files. Creating shortcuts. ID: for everything else. Performing read/write operations in the same function call or session in your app. Performing read/write operations that require ID (e.g. Contact photo and options). Another thing to check is what happens when associating a local RawContact to an Account (move from device to Account) and vice versa. Is the lookup key of the Contact affected? After associating the local RawContact to an Account... #### Contacts table Contact id: 58, lookupKey: 2059i4abd4a8f8ff89642 #### RawContacts table RawContact id: 55, contactId: 58 The lookup key changed but the Contact ID remained the same! In this case, loading a reference to the previously local Contact will fail! I verified that this is indeed the behavior of the native (AOSP) Contacts app. Moving the RawContact from device to Google using Google Contacts app while having Contact details activity opened in the AOSP Contacts app will result in \"error Contact does not exist\" message in the AOSP Contacts app! The RawContact and its Data also remained the same in this case. Removing the account from it results in... #### Contacts table Contact id: 59, lookupKey: 0r58-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 #### RawContacts table RawContact id: 58, contactId: 59 The Contact and RawContacts row have been deleted and new rows have been created to replace them! I also verified that the Data rows have also been deleted and new rows have been created to replace them! This stuff is not really relevant for lookup key but still good to know for implementing moving between accounts in the future. RawContacts; Accounts + Contacts \u00b6 The RawContacts table associates a person to an android.accounts.Account that it belongs to. Each new RawContacts row created results in; a new row in the Contacts table (unless the RawContact is associated to another existing Contact) a new row in the RawContacts with account name and type set to null 0 or more rows in the Data table with a reference to the new Contacts and RawContacts Ids It is possible to create RawContacts without any rows in the Data table. See the Data required section for more details. For example, creating 4 new contacts using the native Android Contacts app results in; Contact id: 4, displayName: First Local Contact Contact id: 5, displayName: Second Local Contact Contact id: 6, displayName: Third Local Contact Contact id: 7, displayName: Third Local Contact RawContact id: 4, accountName: null, accountType: null RawContact id: 5, accountName: null, accountType: null RawContact id: 6, accountName: null, accountType: null RawContact id: 7, accountName: null, accountType: null Data id: 15, rawContactId: 4, contactId: 4, data: First Local Contact Data id: 16, rawContactId: 5, contactId: 5, data: Second Local Contact Data id: 17, rawContactId: 6, contactId: 6, data: Third Local Contact Data id: 18, rawContactId: 7, contactId: 7, data: Third Local Contact Local Contacts / RawContacts RawContacts inserted without an associated account are considered local or device-only raw contacts, which are not synced. The native Contacts app hides the following UI fields when inserting or updating local raw contacts; - Event - Relation - Group memberships To enforce this behavior, this library ignores all of the above during inserts and updates for local raw contacts. Lollipop (API 22) and below When an Account is added, from a state where no accounts have yet been added to the system, the Contacts Provider automatically sets all of the null accountName and accountType in the RawContacts table to that Account's name and type; RawContact id: 4, accountName: vestrel00@gmail.com, accountType: com.google RawContact id: 5, accountName: vestrel00@gmail.com, accountType: com.google RawContact id: 6, accountName: vestrel00@gmail.com, accountType: com.google RawContact id: 7, accountName: vestrel00@gmail.com, accountType: com.google RawContacts inserted without an associated account will automatically get assigned to an account if there are any available. This may take a few seconds, whenever the Contacts Provider decides to do it. Dissociating RawContacts from Accounts will result in the Contacts Provider associating those back to an Account. Marshmallow (API 23) and above The Contacts Provider no longer associates local contacts to an account when an account is or becomes available. Local contacts remain local. Account removal Removing the Account will remove all of the associated rows in the local RawContact, Data, and Groups tables. This includes user Profile data in those tables. SyncColumns modifications This library supports modifying the SyncColumns.ACCOUNT_NAME and SyncColumns.ACCOUNT_TYPE of the RawContacts table in some cases only. In some cases does not work as intended and produces unwanted side-effects. It probably has something to do with syncing with remote servers and local Account / sync data not matching up similar to errors on network requests if the system time does not match network time. The motivation behind changing the Account columns of the RawContacts table rows is that it would allow users to; Associate local RawContacts (those that are not associated with an Account) to an Account, allowing syncing between devices. Dissociate RawContacts from their Account such that they remain local to the device and not synced between devices. Transfer RawContacts from one Account to another. When modifying the SyncColumns directly, the first works as intended. The second works with some unwanted side-effects. The third does not work at all and produces unwanted side-effects. These are the behaviors that I have found; Associating local RawContact A to Account X. Works as intended. RawContact A is now associated with Account X and is synced across devices. Dissociating RawContact A (setting the SyncColumns' Account name and type to null) from Account X. Partially works with some unwanted-side effects. Dissociates RawContact A from the device but not other devices. RawContact A is no longer visible in the native Contacts app UNLESS it retains the group membership to at least the default group from an Account. At this point, RawContact A is a local contact. Changes to this local RawContact A will not be synced across devices. If RawContact A is updated in another device and synced up to the server, then a syncing side-effect occurs because the RawContact A in the device is different from the RawContact A in the server. This causes the Contacts Provider to create another RawContact, resulting in a \"duplicate\". The two RawContact As may get aggregated to the same Contact depending on how similar they are. If local RawContact A is re-associated back to Account X, it will still no longer be synced. Associating RawContact A from original Account X to Account Y. Does not work and have bad side-effects. No change in other devices. For Lollipop (API 22) and below, RawContact A is no longer visible in the native Contacts app and syncing Account Y in system settings fails. For Marshmallow (API 23) and above, RawContact A is no longer visible in the native Contacts app. RawContact A is automatically deleted locally at some point by the Contacts Provider. Syncing Account Y in system settings succeeds. Given that associating originally local RawContacts to an Account is the only thing that actually works, it is the only function that will be exposed to consumers. If consumers want to transfer RawContacts from one Account to another, they can create a copy of a RawContact associated with the desired Account and then delete the original RawContact. Same idea can be used to transform an Account-associated RawContact to a local RawContact. Perhaps we can implement some functions in this library that does these things? We won't for now because the native Contacts app does not support these functions anyways. It can always be implemented later if the community really wants. Here are some other things to note. The Contacts Provider automatically creates a group membership to the default group of the target Account when the account changes. This occurs even if the group membership already exists resulting in duplicates. The Contacts Provider DOES NOT delete existing group memberships when the account changes. This has to be done manually to prevent duplicates. RawContacts; Deletion \u00b6 Deleting a contact's Contacts row, RawContacts row(s), and associated Data row(s) are best explained in the documentation in ContactsContract.RawContacts ; When a raw contact is deleted, all of its Data rows as well as StatusUpdates, AggregationExceptions, PhoneLookup rows are deleted automatically. When all raw contacts associated with a Contacts row are deleted, the Contacts row itself is also deleted automatically. The invocation of resolver.delete(...), does not immediately delete a raw contacts row. Instead, it sets the ContactsContract.RawContactsColumns.DELETED flag on the raw contact and removes the raw contact from its aggregate contact. The sync adapter then deletes the raw contact from the server and finalizes phone-side deletion by calling resolver.delete(...) again and passing the ContactsContract#CALLER_IS_SYNCADAPTER query parameter. Some sync adapters are read-only, meaning that they only sync server-side changes to the phone, but not the reverse. If one of those raw contacts is marked for deletion, it will remain on the phone. However it will be effectively invisible, because it will not be part of any aggregate contact. TLDR To delete a contacts and all associated rows, simply delete all RawContact rows with the desired Contacts id. Deletion of the Contacts row and associated Data row(s) will be done automatically by the Contacts Provider. Note that deleting a RawContacts row may not immediately delete the RawContacts row. In this case, it is marked as deleted and its reference to a contact id is nulled. The Contact may still exist if it still has at least one constituent RawContact that is not marked for deletion. A RawContact is marked for deletion as specified by RawContactsColumns.DELETED . Typically, deleting RawContacts immediately removes the row from the RawContacts table. However, RawContacts row remains and is simply marked for deletion UNTIL the sync adapters syncs the changes. One of the reasons syncs do not occur is when the system sync settings are turned off for the Account or there is no network connection. Such RawContacts should not be included in query results for Contacts. The AOSP and Google Contacts app also does not show them. Note that local RawContacts rows (not associated with an Account) are deleted immediately as no sync needs to occur. Multiple RawContacts Per Contact \u00b6 Each row in the Contacts table may be associated with more than one row in the RawContacts table. The Contacts Provider may consolidate multiple contacts belonging to different accounts and combine them into a single entry in the Contacts table whilst maintaining the separate entries in the RawContacts table. A more likely scenario that causes multiple RawContacts per Contact is when two or more Contacts are \"linked\" (or \"merged\" for API 23 and below, or \"joined\" for API 22 and below). Behavior of linking/merging/joining contacts (AggregationExceptions) \u00b6 The native Contacts app terminology has changed over time; API 22 and below; join / separate API 23; merge / unmerge API 24 and above; link / unlink However, the internals have not changed; KEEP_TOGETHER / KEEP_SEPARATE . These operations are supported by the ContactsContract.AggregationExceptions . For example, given the following tables, ### Contacts table Contact id: 32, displayName: X, starred: 0, timesContacted: 1, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 Contact id: 33, displayName: Y, starred: 1, timesContacted: 2, lastTimeContacted: 1573071750624, customRingtone: content://media/internal/audio/media/115, sendToVoicemail: 1 ### RawContacts table RawContact id: 30, contactId: 32, displayName: X, accountName: x@x.com, accountType: com.google, starred: 0, timesContacted: 1, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 RawContact id: 31, contactId: 33, displayName: Y, accountName: y@y.com, accountType: com.google, starred: 1, timesContacted: 2, lastTimeContacted: 1573071750624, customRingtone: content://media/internal/audio/media/115, sendToVoicemail: 1 ### Data table Data id: 57, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/group_membership, data1: 18 Data id: 58, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/name, data1: X, isPrimary: 1, isSuperPrimary: 1 Data id: 59, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: x@x.com Data id: 60, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: xx@x.com, isPrimary: 1, isSuperPrimary: 1 Data id: 63, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/group_membership, data1: 6 Data id: 64, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/name, data1: Y, isPrimary: 1, isSuperPrimary: 1 Data id: 65, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/email_v2, data1: y@y.com Data id: 66, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/email_v2, data1: yy@y.com, isPrimary: 1, isSuperPrimary: 1 When Contact X links/merges/joins Contact Y , the tables becomes; ### Contacts table Contact id: 32, displayName: X, starred: 1, timesContacted: 2, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 ### RawContacts table RawContact id: 30, contactId: 32, displayName: X, accountName: x@x.com, accountType: com.google, starred: 0, timesContacted: 1, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 RawContact id: 31, contactId: 32, displayName: Y, accountName: y@y.com, accountType: com.google, starred: 1, timesContacted: 2, lastTimeContacted: 1573071750624, customRingtone: content://media/internal/audio/media/115, sendToVoicemail: 1 ### Data table Data id: 57, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/group_membership, data1: 18 Data id: 58, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/name, data1: X, isPrimary: 1, isSuperPrimary: 1 Data id: 59, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: x@x.com Data id: 60, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: xx@x.com, isPrimary: 1, isSuperPrimary: 0 Data id: 63, rawContactId: 31, contactId: 32, mimeType: vnd.android.cursor.item/group_membership, data1: 6 Data id: 64, rawContactId: 31, contactId: 32, mimeType: vnd.android.cursor.item/name, data1: Y, isPrimary: 1, isSuperPrimary: 0 Data id: 65, rawContactId: 31, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: y@y.com Data id: 66, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/email_v2, data1: yy@y.com, isPrimary: 1, isSuperPrimary: 0 What changed? Contact Y's row has been deleted and its column values have been merged into Contact X row. If the reverse occurred (Contact Y merged with Contact X), Contact Y's row would still be deleted. The difference is that Contact X's display name will be set to Contact Y's display name, which is done by the native Contacts app manually by setting Contact Y's Data name row to be the \"default\" (isPrimary and isSuperPrimary both set to 1). The AggregationExceptions table records the linked RawContacts's IDs in ascending order regardless of the order used in RAW_CONTACT_ID1 and RAW_CONTACT_ID2 at the time of merging. The RawContacts and Data table remains the same except the joined contactId column values have now been changed to the id of Contact X. All Data rows' isSuperPrimary value has been set to 0 though the isPrimary columns remain the same. In other words, this clears any \"default\" set before the link. These are done automatically by the Contacts Provider during the link operation. What is not done automatically by the Contacts Provider is that the name row of former Contact X is set as the default. The native Contacts app does this manually. The Contacts Providers automatically sets the Contact display name to whatever the default name row is for the Contact, if available. For more info on Contact display name resolution, read the Contact Display Name and Default Name Rows section. Note that display name resolution is different for APIs below 21 (pre-lollipop). The display name of the RawContacts remain the same. The Groups table remains unmodified. Options updates Changes to the options (starred, timesContacted, lastTimeContacted, customRingtone, and sendToVoicemail) of a RawContact may affect the options of the parent Contact. On the other hand, changes to the options of the parent Contact will be propagated to all child RawContact options. Photo updates A RawContact may have a full-sized photo saved as a file and a thumbnail version of that saved in the Data table in a photo mimetype row. A Contact's full-sized photo and thumbnail are simply references to the \"chosen\" RawContact's full-sized photo and thumbnail (though the URIs may differ). Note that when removing the photo in the native contacts app, the photo data row is not immediately deleted, though the PHOTO_FILE_ID is immediately set to null. This may result in the PHOTO_URI and PHOTO_THUMBNAIL_URI to still have a valid image uri even though the photo has been \"removed\". This library immediately deletes the photo data row, which seems to work perfectly. Data inserts In the native Contacts app, Data inserted in combined (raw) contacts mode will be associated to the first RawContact in the list sorted by the RawContact ID. This may not be the same as the RawContact referenced by ContactsColumns.NAME_RAW_CONTACT_ID . UI changes? The native Contacts App does not display the groups field when displaying / editing Contacts that have multiple RawContacts (linked/merged/joined) in combined mode. However, it does allow editing individual RawContact Data rows in which case the groups field is displayed and editable. In the native Contacts app, the name attribute used comes from the name row with IS_SUPER_PRIMARY set to true. This and all other \"unique\" mimetypes (organization) and non-unique mimetypes (email) per RawContact are shown only if they are not blank. Showing multiple RawContact's data in the same edit screen (combined mode) In older version of the native, Android Open Source Project (AOSP) Contacts app, data from multiple RawContacts was being shown in the same edit screen. This caused a lot of confusion about which data belonged to which RawContact. Newer versions of AOSP Contacts only allow editing one RawContact at a time to avoid confusion. Though, several RawContacts' data are still shown (not-editable) in the same screen. AggregationExceptions table \u00b6 Given the following Contacts and their RawContacts; Contact A RawContact 1 Contact B RawContact 2 Contact C RawContact 3 Contact D RawContact 4 Linking one by one in this order; Contact B link Contact A Contact C link Contact D Contact C link Contact B Results in the following AggregationExceptions rows respectively; Aggregation exception id: 430, type: 1, rawContactId1: 1, rawContactId2: 2 Aggregation exception id: 430, type: 1, rawContactId1: 1, rawContactId2: 2 Aggregation exception id: 432, type: 1, rawContactId1: 3, rawContactId2: 4 Aggregation exception id: 436, type: 1, rawContactId1: 1, rawContactId2: 2 Aggregation exception id: 439, type: 1, rawContactId1: 1, rawContactId2: 3 Aggregation exception id: 442, type: 1, rawContactId1: 1, rawContactId2: 4 Aggregation exception id: 440, type: 1, rawContactId1: 2, rawContactId2: 3 Aggregation exception id: 443, type: 1, rawContactId1: 2, rawContactId2: 4 Aggregation exception id: 444, type: 1, rawContactId1: 3, rawContactId2: 4 There is a pattern here. RawContact ids are sorted in ascending order and linked from least to greatest exhaustively but no double links (1-2 is the same as 2-1). RawContact 1 has a row with RawContact 2, 3, and 4. RawContact 2 has a row with RawContact 3 and 4. RawContact 3 has a row with RawContact 4. Linking all in one go; Contact C link Contact A, B, D Results in the same AggregationExceptions rows. Unlinking results in the same AggregationExceptions rows except the type is 2 (TYPE_KEEP_SEPARATE). Data Table \u00b6 The Data table uses generic column names (e.g. \"data1\", \"data2\", ...) using the column \"mimetype\" to distinguish the type of data in that generic column. For example, the column name of StructuredName.DISPLAY_NAME is the same as Email.ADDRESS , which is \"data1\". Each row in the Data table consists of a piece of RawContact data (e.g. a phone number), its \"mimetype\", and the associated RawContact and Contact id. A row does not contain all of the data for a contact. RawContacts may only have one row of certain mimetypes and may have multiple rows of other mimetypes. Here is the list. Unique mimetype per RawContact Name (StructuredName) Nickname Note Organization Photo SipAddress Non-unique mimetype per Raw Contact Address (StructuredPostal) Email Event GroupMembership Im Phone Relation Website Although some mimetypes are unique per RawContact, none of those mimetypes are unique per Contact because a Contact is an aggregate of one or more RawContacts! Data Primary and Super Primary Rows \u00b6 As per documentation, for a set of data rows with the same mimetype (e.g. a set of emails), there should only be one primary data row (e.g. email) per RawContact and one super primary data row per Contact. Furthermore, a data row that is super primary must also be primary. Unfortunately, the Contacts Provider does not do any data set validation for the Data columns IS_PRIMARY and IS_SUPER_PRIMARY . This means that it is possible to set more than one data row of the same mimetype as primary for the same RawContact and super primary for the same aggregate Contact. It is also possible to set a data row as super primary but not primary. Upholding the the contract is left to us... For example, given this relationship; Contact RawContact X Email A Email B RawContact Y Email C Email D When Emails A, B, C, and D are inserted with the RawContacts or after the RawContacts have been created, we get the following state; Email Primary Super Primary A 0 0 B 0 0 C 0 0 D 0 0 The state does not change when RawContact X is linked with RawContact Y. After setting Email A as the \"default\" email, it becomes primary and super primary; Email Primary Super Primary A 1 1 B 0 0 C 0 0 D 0 0 Then setting Email B as the default email, it becomes primary and super primary. Email A is no longer primary or super primary. Email Primary Super Primary A 0 0 B 1 1 C 0 0 D 0 0 Then setting Email C as the default email, it becomes primary and super primary. Email B is still primary because it belongs to a different RawContact than Email C. However, Email B is no longer the super primary as there can only be one per aggregate Contact. Email Primary Super Primary A 0 0 B 1 0 C 1 1 D 0 0 Then setting Email D as the default email, it becomes primary and super primary. Email C is no longer primary or super primary. Email Primary Super Primary A 0 0 B 1 0 C 0 0 D 1 1 Then clearing the default email D, removes its primary and super primary status. However, email B remains a primary but not a super primary. Email Primary Super Primary A 0 0 B 1 0 C 0 0 D 0 0 The above behavior is observed from the native Contacts app. The \"super primary\" data of an aggregate Contact is referred to as the \"default\". At this point, the native Contacts app still shows email B as the first email in the list even though it isn't the \"default\" (super primary) because it is still a primary. This adds a bit of confusion in my opinion, especially when more than 2, 3, or 4 RawContacts are linked. A \"fix\" would be to only order the list of emails using \"super primary\" instead of \"super primary\" and \"primary\". OR to remove the primary status of the data set of all linked RawContacts. One benefit of the native Contacts implementation of this is that it retains the primary status when unlinking RawContacts. This library should follow what the native Contacts app is doing in spirit of recreating the native experience as closely as possible, even if it seems like a lesser experience. Data Table Joins \u00b6 All columns accessible via cursors returned from Data table queries are specified in DataColumnsWithJoins , which includes the DataColumns , ContactsColumns , and ContactOptionsColumns . In code, mentions of the \"Data table\" typically refers to the joined table. The DataColumns gives us access to all of the columns in the Data table. All other joined columns, including the ContactsColumns are appended to each row in the query. This means that the ContactsColumns ; DISPLAY_NAME , PHOTO_URI , and PHOTO_THUMBNAIL_URI are repeated for all Data rows belonging to the same Contact. The ContactOptionsColumns values joined with the Data table are the values of the Contact, not the RawContact that the Data row belongs to! The same applies to the \"display_name\". Data Updates \u00b6 A new row in the Data table is created for each new piece of data (e.g. email address) entered for the contact. Removing a piece of existing data results in the deletion of the row in the Data table if that row no longer contains any meaningful data (no meaningful non-null \"datax\" columns left). This is the behavior of the native Android Contacts app. Therefore, querying for null fields is not possible. For example, there may be no Data rows that exist where the email address is null. Thus, a query to search for all contacts with null email address may return 0 contacts even if there are some contacts without email addresses. Data Required \u00b6 Creating blank RawContacts without email address (or other fields), results in no rows in the Data table for the email address, and all other fields. There are a few exceptions. The following Data rows are automatically created for all contacts, if not provided; Group membership, underlying value defaults to the account's default system group Name, underlying value defaults to null Nickname, underlying value defaults to null Note, underlying value defaults to null Note that all of the above rows are only automatically created for RawContacts that are associated with an Account. If a valid account is provided, the default (auto add) system group membership row is automatically created immediately by the Contacts Provider at the time of contact insertion. The name, nickname, and note are automatically created at a later time. If a valid account is not provided, none of the above data rows are automatically created. Blank RawContacts The Contacts Providers allows for RawContacts that have no rows in the Data table (let's call them \"blanks\") to exist. The native Contacts app does not allow insertion of new RawContacts without at least one data row. It also deletes blanks on update. Despite seemingly not allowing blanks, the native Contacts app shows them. There are two scenarios where blanks may exist. Contact with RawContact(s) with no Data row(s). In this case, the Contact is blank as well as its RawContact(s). Contact that has RawContact with Data row(s) and a RawContact with no Data row. In this case, the Contact and the RawContact with Data row(s) are not blank but the RawContact with no Data row is blank. Data StructuredName \u00b6 The DISPLAY_NAME is the unstructured representation of the name. It is made up of structured components; PREFIX , GIVEN_NAME , MIDDLE_NAME , FAMILY_NAME , and SUFFIX . When updating or inserting a row; If the display name is null and there are non-null structured components provided (e.g. given and family name), the Contacts Provider will automatically set the display name by combining the structured components. If the display name is not null and all structured components are null, the Contacts Provider automatically (to the best of its ability) derive the values for all the structured components. If the display name and structured components are not null, the Contacts Provider does nothing automatically. Data StructuredPostal \u00b6 The FORMATTED_ADDRESS is the unstructured representation of the postal address. It is made up of structured components; STREET , POBOX , NEIGHBORHOOD , CITY , REGION , POSTCODE , and COUNTRY . When updating or inserting a row; If the formatted address is null and there are non-null structured components provided (e.g. street and city), the Contacts Provider will automatically set the formatted address by combining the structured components. If the formatted address is not null and all structured components are null, the Contacts Provider automatically sets the street value to the formatted address. If the formatted address and structured components are not null, the Contacts Provider does nothing automatically. Groups Table & Accounts \u00b6 Contacts are assigned to one or more groups via the GroupMembership . It typically looks like this; Group id: 1, systemId: Contacts, readOnly: 1, title: My Contacts, favorites: 0, autoAdd: 1, accountName: vestrel00@gmail.com, accountType: com.google Group id: 2, systemId: null, readOnly: 1, title: Starred in Android, favorites: 1, autoAdd: 0, accountName: vestrel00@gmail.com, accountType: com.google Group id: 3, systemId: Friends, readOnly: 1, title: Friends, favorites: 0, autoAdd: 0, accountName: vestrel00@gmail.com, accountType: com.google Group id: 4, systemId: Family, readOnly: 1, title: Family, favorites: 0, autoAdd: 0, accountName: vestrel00@gmail.com, accountType: com.google Group id: 5, systemId: Coworkers, readOnly: 1, title: Coworkers, favorites: 0, autoAdd: 0, accountName: vestrel00@gmail.com, accountType: com.google Group id: 6, systemId: null, readOnly: 0, title: Custom Group, favorites: 0, autoAdd: 0, accountName: vestrel00@gmail.com, accountType: com.google The actual groups are in a separate table; Groups. Each group is associated with an Account. No group can exist without an account. It is account-exclusive. Each account will have its own set of the above system groups. This means that there may be multiple groups with the same title belonging to different accounts. System ids are typically Contacts, Friends, Family, and Coworkers. These ids are typically the same across all copies of Android. Notes; - The Contacts system group is the default group in which all raw contacts of an account belongs to. Therefore, it is typically hidden when showing the list of groups in the UI. - The starred (favorites) group is not a system group as it has null system id. However, it behaves like one in that it is read only and it comes with most (if not all) copies of the native app. Removing the Account will delete all of the associated rows in the Groups table. Groups, duplicate titles The Contacts Provider allows multiple groups with the same title (case-sensitive comparison) belonging to the same account to exist. In older versions of Android, the native Contacts app allows the creation of new groups with existing titles. In newer versions, duplicate titles are not allowed. Therefore, this library does not allow for duplicate titles. In newer versions, the group with the duplicate title gets deleted either automatically by the Contacts Provider or when viewing groups in the native Contacts app. It's not an immediate failure on insert or update. This could lead to bugs! Groups Table & GroupMemberships (Data Table) \u00b6 There may be multiple groups with the same title from different accounts. Therefore, the group membership should point to the group belonging to the same account as the raw contact. The native Contacts app displays only the groups belonging to the selected account. Updating group memberships of existing raw contacts seem to be almost instant. All raw contacts must be a part of at least the default group (system id is \"Contacts\"). Raw contacts with no group membership will be asynchronously added to the Account's default group by the Contacts Provider. Membership to the default group should never be deleted! Starred in Android (Favorites) \u00b6 When the ContactOptionsColumns.STARRED column of a Contact in the Contacts table is set to true, the Contacts Provider automatically adds a group membership to the favorites group for all RawContacts linked to the Contact. Setting STARRED to false removes all group memberships to the favorites group. The STARRED is interdependent with group memberships to the favorites group. Adding a group membership to the favorites group results in STARRED being set to true. Removing the membership sets it to false. Raw contacts that are not associated with an account do not have any group memberships. Even though these raw contacts may not have a membership to the favorites group, they may still be \"starred\" (favorited) via the ContactOptionsColumns.STARRED column in the Contacts table, which is not dependent on the existence of a favorites group membership. Refresh RawContact instances after changing the starred value. Otherwise, performing an update on the RawContact with a stale set of group memberships may revert the star/unstar operation. For example, query returns a starred RawContact -> set starred to false -> update RawContact (still containing a group membership to the favorites group) -> starred will be set back to true. Group memberships & Local RawContacts \u00b6 Local RawContacts may have a group membership to the default system group of an Account without being associated with the Account... The native Contacts app may not have an edit-RawContact option for newly inserted RawContacts that have no group membership to the default group when an Account is available. Though, edits can still be made in other ways. Instead, an option to \"Add to contacts\" is shown that adds a membership to the default group but does not associate the raw contact to the Account that owns the group. The edit UI does not show the group membership field. Weirdly, this only occurs when there is exactly only one Account. If there are no Accounts or there are two or more Accounts, then this does not occur. Also, this does not occur for a Contact with a RawContact that has a group membership AND a RawContact that has no group membership. Groups; Deletion \u00b6 Similar to deleting RawContacts, deleting a Groups row may not immediately delete the Groups row. In this case, it is marked as deleted. A Group is marked for deletion as specified by GroupsColumns.DELETED . Typically, deleting Groups immediately removes the row from the Groups table. However, Groups row remains and is simply marked for deletion UNTIL the sync adapters syncs the changes. One of the reasons syncs do not occur is when the system sync settings are turned off for the Account or there is no network connection. Such Groups should not be included in query results for Contacts. The AOSP and Google Contacts app also does not show them. Note that local Groups rows (not associated with an Account) are deleted immediately as no sync needs to occur. Groups; UI \u00b6 In newer Android versions of the native Contacts app, \"groups\" are now being referred to as \"labels\". However, the underlying code still uses groups. Google is probably just trying to make it more user friendly by calling it label instead of group. User Profile \u00b6 There exist one (profile) Contacts row that identifies the user; ContactsColumns.IS_USER_PROFILE . There is at least one RawContacts row that is associated with the user profile; RawContactsColumns.RAW_CONTACT_IS_USER_PROFILE . Associated RawContacts may or may not be associated with an Account. The RawContacts row(s) may have rows in the Data table as usual. These profile table rows have special IDs that differ from regular rows. See ContactsContract.isProfileId . Note that the Contacts Provider will throw an IllegalArgument exception when attempting to include ContactsColumns.IS_USER_PROFILE and RawContactsColumns.RAW_CONTACT_IS_USER_PROFILE columns in Data table queries. I have not yet tried including these columns in the Contacts or RawContacts table queries. The profile Contact row may not be merged / linked with other contacts and do not belong to any group (favorites / starred). Profile rows in the Contacts, RawContacts, and Data table are not visible via queries in the respective tables. They will not be in the resulting cursor. To get the profile Contacts table rows, query the Profile.CONTENT_URI . To get profile RawContacts table rows, query the Profile.CONTENT_RAW_CONTACTS_URI . To get the profile Data table rows, query the Profile.CONTENT_RAW_CONTACTS_URI appended with the RawContact id and RawContacts.Data.CONTENT_DIRECTORY . To insert a new profile RawContact, use Profile.CONTENT_RAW_CONTACTS_URI . It will automatically be associated with the profile Contact. If the profile Contact does not yet exist, it will be created automatically. To insert a new profile Data row, either; insert to the Profile.CONTENT_RAW_CONTACTS_URI appended with the RawContact id and RawContacts.Data.CONTENT_DIRECTORY insert to the Data table directly, referencing the RawContact id Same rules apply to all table rows. If all profile RawContacts table rows have been deleted, then associated Contacts and Data table rows will automatically be deleted. Profile aggregation The RawContacts of a (Contact) Profile are linked via the indexed rows; Profile.CONTENT_RAW_CONTACTS_URI . Therefore, the AggregationsExceptions table is not used here. Profile and users Note that as of Android 5 Lollipop, there may exist multiple users in a device. Each user has a separate list of accounts and contact data. This also means that each user has a separate (local) profile contact. Profile and Accounts According to the Profile documentation; \"... each account (including data set, if applicable) on the device may contribute a single raw contact representing the user's personal profile data from that source.\" In other words, one account can have one profile RawContact. Whether or not profile RawContacts associated to an Account can be carried over and synced across devices and users is up to the Contacts Provider / Sync provider for that Account. From my experience, profile RawContacts associated to an Account is not carried over / synced across devices or users. Despite the documentation of \"one profile RawContact per one Account\", the Contacts Provider allows for multiple RawContacts per Account, including multiple local RawContacts (no Account). Thus, we should let consumers exploit this but set defaults to be one-for-one. Creating / setting up the profile in the native Contacts app results in the creation of a local RawContact (not associated with an Account) even if there are available Accounts. The Contacts Provider does not associate local contacts to an account when an account is or becomes available (regardless of API level). Removing the Account will delete all of the associated rows in the Contact, RawContact, Data, and Groups tables. This includes user Profile data in those tables. Profile permissions Profile permissions (READ_PROFILE and WRITE_PROFILE) have been removed since API 23. However, they are still required for API 22 and below. Reading and writing the profile is included in the Contacts permissions. There is no need to ask for profile permissions at runtime because prior to API 23, permissions in the AndroidManifest have to be accepted prior to installation. Syncing Data / Sync Adapters \u00b6 First, it\u2019s good to know the official documentation of sync adapters; https://developer.android.com/guide/topics/providers/contacts-provider#SyncAdapters Now, let\u2019s ingest the official docs\u2026 Data belonging to a RawContact that is associated with a Google account will be available anywhere the Google account is used; in any Android or iOS device, a web browser, etc\u2026 Data is synced by Google\u2019s sync adapters to and from their remote servers. Syncing depends on the account sync settings, which can be configured in the native system settings app and possibly through some remote configuration. This library does not provide any sync adapters. Instead, it relies on existing sync adapters to do the syncing. Sync adapters and syncing are really out of scope of this library. Syncing is its own thing that typically happens outside of an application UI. This library is focused on reading and writing native and custom data to and from the local database. Syncing the local database to and from a remote service is a different story altogether =) Custom Data / MimeTypes \u00b6 First, it\u2019s good to know the official documentation of custom data rows; https://developer.android.com/guide/topics/providers/contacts-provider#CustomData Now, let\u2019s ingest the official docs\u2026 Custom mimetypes do not belong to the native Contacts Provider mimetype set (e.g. address, email, phone, etc). The Contacts Provider allows for the creation of new / custom mimetypes. This is especially useful for other apps (Google Contacts, Facebook, Twitter, WhatsApp, etc) that want to attach extra pieces of data to a particular RawContact. Custom data are NOT synced, including those that belong to RawContacts that are associated with an Account. Custom sync adapters are required to sync custom data. This library currently does NOT provide custom sync adapters to sync custom data! Custom data from other apps such as Facebook, Twitter, WhatsApp, etc may or may not be synced. It all depends on those applications and their custom sync adapters (if they have any) and sync settings. For insight on how aforementioned social media services may be syncing their data, read through the official documentation; https://developer.android.com/guide/topics/providers/contacts-provider#SocialStream Unused ContactsContract Stuff \u00b6 We are currently not utilizing these things because I haven't found usages of them while using the native Contacts app. They are probably working behind the scenes but until we find uses for these, let's leave it out because YAGNI . Settings . Contacts-specific settings for various Accounts (settings for an Account). Might be useful to add this for SHOULD_SYNC and UNGROUPED_VISIBLE . ContactsColumns.IN_VISIBLE_GROUP + Groups.GROUP_VISIBLE . Flag indicating if the contacts belonging to this group should be visible in any user interface. Java Support \u00b6 This library is intended to be Java-friendly. The policy is that we should attempt to write Java-friendly code that does not increase lines of code by much or add external dependencies to cater exclusively to Java users. Creating Entities & data class \u00b6 First, consumers are not allowed to create immutable entities. Those must come from the API itself to ensure data integrity. Whether or not we will change this in the future is debatable =) Consumers are able to set read-only and private or internal variables though because all Entity implementations are data classes. Data classes provide a copy function that allows for setting any property no matter their visibility and even if the constructor is private. As a matter of fact, setting the constructor of a data class as private gives this warning by Android Studio: \"Private data class constructor is exposed via the 'copy' method. There is currently no way to disable the copy function of data classes (that I know of). The only thing we can do is to provide documentation to consumers, insisting against the use of the copy method as it may lead to unwanted side effects when updating and deleting contacts. We could just use regular classes instead of data classes but entities should be data classes because it is what they are (know what I mean?!). Also, I'd hate to have to generate equals and hashcode functions for them, which will make the code harder to maintain. Though, we might do this anyways at some point if we want to make it possible for a mutable entity to equal an immutable entity. Time will tell =) FIXME? Hide / disable data class copy function if kotlin ever allows it. https://discuss.kotlinlang.org/t/data-class-copy-visibility-modifier/19746 Immutable vs Mutable Entities \u00b6 This library provides true immutability for immutable entities. Take a look at the current (simplified) hierarchy; 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 > ) : RawContactEntity data class Address ( val formattedAddress : String? ) data class MutableAddress ( var formattedAddress : String? ) Note the use of sealed class is to prevent consumers from defining their own entities. This restriction may or may not change in the future. Notice that there is nothing mutable in the immutable Contact . Everything are val s and the data structures used (i.e. RawContact , Address , and List ) are all immutable. This provides consumers 100% confidence that immutable entities are not mutable. They will not change or mutate in any way. Once they are constructed, they will always remain the same. Why immutability is so important will not be covered in this dev notes because it would be too big (that's what she said) and there are blogs and books written about this. One of the most important advantages of immutability is that it is thread-safe. Immutable instances can be used in several different threads without the need for synchronization and worries about deadlocks. In other words, they are thread-safe and faster than the mutable version. The current structure also allows consumers to be able to distinguish between immutable and mutable entities exhaustively. E.G. fun doSomethingAndReturn ( contact : ContactEntity ) = when ( contact ) { is Contact -> {} is MutableContact -> {} } Note that the mutable entities provided in this library are NOT thread-safe . Consumers will have to perform their own synchronizations if they want to use and mutate mutable entities in multi-threaded scenarios. The cost of the current immutability implementation \u00b6 The cost of implementing true immutability is more lines of code. Notice that the MutableContact does not inherit from Contact . The same goes for the other entities. This leads to having to write seemingly duplicate code when writing functions and extensions. // FIXME? Furthermore, equality between immutable and mutable entities are not yet implemented. This means that Contact(\"john\") == MutableContact(\"john\") will return false even though their underlying contents are the same. This can be fixed by overriding the equals and hashcode functions of all entities. However, that is a lot more code that I would like to avoid, which is why I'm using data class for all entities in the first place! This may change in the future if the community really wants to change it =) On a side note, the same cost is incurred by Kotlin's standard libs. For example, notice that AbstractMutableList does not inherit from and is completely separate from AbstractList . I'm sure stdlib devs also had to write seemingly duplicate code in implementations of the List interface. Avoiding the cost... Shortcuts and pitfalls. \u00b6 One thing that may come to mind in attempts to reduce lines of seemingly duplicate code is to have just a mutable implementation of an immutable declaration. For example, we can restructure the hierarchy to; sealed interface Contact { val rawContacts : List < RawContact > } data class MutableContact ( override val rawContacts : List < MutableRawContact > ) : Contact sealed interface RawContact { val addresses : List < Address > } data class MutableRawContact ( override var addresses : MutableList < MutableAddress > ) : RawContact sealed interface Address { val formattedAddress : String? } data class MutableAddress ( override var formattedAddress : String? ) : Address Notice that there is a non-concrete declaration (i.e. Contact , RawContact , and Address ) and just one concrete implementation (i.e. MutableContact , MutableRawContact , and MutableAddress ). Note that a val declaration can be overridden by a var . Keep in mind that val only requires getters whereas var requires both getters and setters. Therefore, a var cannot be overridden by a val . Or maybe there is a different reason Kotlin imposes this restriction =) On a similar note, the List interface can be overridden to a MutableList . We, as API contributors, can avoid having to write seemingly duplicate functions and extensions! However! Can you see what's wrong with this setup? If we do this, we would either be deceiving consumers to think that the instances of \"immutable\" class signatures (i.e. Contact , RawContact , and Address ) are actually immutable OR we would have to let consumers know that the API does not really provide true immutability. Neither option is ideal (nor is it acceptable IMO). Consumers would have a reference to a Contact , which they may assume is immutable because of the usage of val instead of var , but in actuality the underlying implementation is mutable... This could be a cause of really hard to find bugs in multi-threaded usage. Consumers may use Contact with the assumption that it is immutable only to find that it can actually be mutated! We could fix this by just making the mutable implementation thread-safe but since that is the only implementation, consumers will be forced to use thread-safe code when they don't have to thereby negatively affecting performance. Keep in mind that thread safety is only one of several reasons for immutability. Those other reasons will be violated too. Consumers will be shocked if they ever do the following or something similar. fun x ( contact : Contact ) = when ( contact ) { is MutableContact -> {} // this is always true is Contact -> {} // this is always true } In any case, I have to admit, it is a nice trick that would save API contributors time. But that's just it! It's just a trick. A shortcut. A nice little time save at the cost of integrity. It is not worth it (IMO). Why Not Add Android X / Support Library Dependencies? \u00b6 I want to keep the dependency list of this library to a minimum. The Contacts Provider is native to Android since the beginning. I want to honor that fact by avoiding adding dependencies here. I made a bit of an exception by adding the Dexter library for permissions handling for the permissions modules (not in the core modules). I'm tempted to remove the Dexter dependency and implement permissions handling myself because Dexter brings in a lot of other dependencies with it. However, it is not part of the core module so I'm able to live with this. TODO Remove/replace Dexter. It is no longer being maintained. Keeping dependencies to a minimum is just a small challenge I made up. We will see how long it can last! I left comments all over the code on when an androidx dependency may be useful. The most glaring example of this is @WorkerThread. Even with that, I'll hold off on adding the androidx annotation lib. I think we can all be consenting adults =) If the community strongly desires the addition of these support libs, then the community will win =)","title":"Developer notes"},{"location":"dev-notes/#developer-notes","text":"This document contains useful developer notes that should be kept in mind during development. It serves as a memory of all the quirks and gotcha's of things like Android's ContactsContract . This is only meant to be read by contributors of this library, not consumers!","title":"Developer Notes"},{"location":"dev-notes/#contacts-provider-contactscontract","text":"It is important to know about the ins and outs of Android's Contacts Provider. After all, this API is just a wrapper around it. A very sweet, sugary wrapper! Sugar. Spice. And everything nice. :D It is important to get familiar with the official documentation of the Contact's Provider . Here is a summary; There are 3 main database tables used in dealing with contacts; Contacts RawContacts Data There are more but that is covered later. All of these tables and their fields are enumerated and documented in android.provider.ContactsContract . Each table serves a different purpose; Contacts Rows representing different people. RawContacts Rows that link Contacts rows to specific Accounts. Data Rows containing data (e.g. name, email) for a RawContacts row. These tables contain the following (notable) information (columns); Contacts _ID DISPLAY_NAME_PRIMARY RawContacts _ID : the Contacts._ID ACCOUNT_NAME : the Account.name ACCOUNT_TYPE the Account.type Data RAW_CONTACT_ID : the RawContacts._ID CONTACT_ID : the Contacts._ID DATA_1 to DATA_15 : contains a piece of contact data (e.g. first and last name, email address and type) determined by the MIMETYPE MIMETYPE : the type of data that this row's DATA_X columns contain (e.g. name and email data) The tables are connected the following way; RawContacts contains a reference to the Contacts row Id. Data contains a reference to the RawContacts row Id and Contacts row Id.","title":"Contacts Provider / ContactsContract"},{"location":"dev-notes/#contacts-display-name","text":"The Contacts.DISPLAY_NAME name may be different than the Data StructuredName display name! If a structured name in the Data table is not provided, then other kinds of data will be used as the Contacts row display name. For example, if an email is provided but no structured name then the display name will be the email. When a structured name is inserted, the Contacts Provider automatically updates the Contacts row display name. In the case of StructuredName , the Contacts.DISPLAY_NAME is made up of the prefix, given, middle, family name, and suffix and not the unstructured display name. If no data rows suitable to be a display name are available, then the Contacts row display name will be null. Data suitable to be a Contacts row display name are enumerated in DisplayNameSources ; email nickname organization phone number structured name Data not suitable to be display names are; address event group im note relation sip website The kind of data used as the display for the Contact is set in ContactNameColumns.DISPLAY_NAME_SOURCE . A note about StructuredName There may be a scenario where the unstructured StructuredName.DISPLAY_NAME does not match the structured components. Such scenarios are possible but is considered incorrect. For example, it is possible to programmatically set the display name to \"Ice Cold\" but set the given and family name to \"Hot Fire\". The Contacts.DISPLAY_NAME is made up of the prefix, given, middle, family name, and suffix (\"Hot Fire\") and not the unstructured display name. The Contacts Provider's general matching algorithm does not include the Contacts.DISPLAY_NAME . However, the StructuredName.DISPLAY_NAME is included in the matching process but not the rest of the structured components (e.g. given and family name). The native Contacts app displays the Contacts.DISPLAY_NAME . So, here comes the unusual scenario that looks like a bug. The general matching algorithm will match the text \"Ice\" or \"Cold\" but not \"Hot\" or \"Fire\". The end result is that searching for the Contact \"Ice Cold\" will show a Contact called \"Hot Fire\"!","title":"Contacts; Display Name"},{"location":"dev-notes/#contact-display-name-and-default-name-rows","text":"If available, the \"default\" (isPrimary and isSuperPrimary set to 1) name row for a Contact is automatically set as the Contact display name by the Contacts Provider. Otherwise, the Contacts Provider chooses from any of the other suitable data from the aggregate Contact. The ContactsColumns.NAME_RAW_CONTACT_ID is automatically updated by the Contacts Provider along with the display name. The default status of other sources (e.g. email) does not affect the Contact display name. The native Contacts app also sets the most recently updated name as the default at every update. This results in the Contact display name changing to the most recently updated name from one of the associated RawContacts. The \"most recently updated name\" is the name field that was last updated by the user when editing in the Contacts app, which is irrelevant to its value. It does not matter if the user deleted the last character of the name, added the same character back, and then saved. It still counts as the most recently updated. All of the above only applies to API 21 and above. Display name resolution is different for APIs below 21 (pre-Lollipop)! The ContactsColumns.NAME_RAW_CONTACT_ID was added in API 21. It changed the way display names are resolved for Contacts with more than one constituent RawContacts, which is what has been described so far. Before this change (APIs 20 and below), the native Contacts app is still able to set the Contact display name somehow. I'm not sure how. If someone figures it out, please let me know. I tried updating the Contact DISPLAY_NAME directly but it does not work. Setting a name row as default also does not affect the Contact DISPLAY_NAME .","title":"Contact Display Name and Default Name Rows"},{"location":"dev-notes/#contacts-id-vs-lookup_key","text":"The Contacts._ID is the unique identifier for the row in the Contacts table. The Contacts.LOOKUP_KEY is the unique identifier for an aggregate Contact (a person). The _ID may change due to aggregation and sync. The same goes for the LOOKUP_KEY but unlike the ID it may still be used to find the aggregate contact. Unlike the Contact ID, the lookup key is the same across devices (for contacts that are associated with an Account and are synced). Note that I did the following investigation with a much larger data set. I simplified it here for brevity. Let's take a look at the following Contacts and RawContacts table rows, #### Contacts table Contact id: 55, lookupKey: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50, displayNamePrimary: Contact With Local RawContact Contact id: 56, lookupKey: 2059i6f5de8460f7f227e, displayNamePrimary: Contact With Synced RawContact #### RawContacts table RawContact id: 55, contactId: 55, displayNamePrimary: Contact With Local RawContact RawContact id: 56, contactId: 56, displayNamePrimary: Contact With Synced RawContact There are two Contacts each having one RawContact. Notice that the lookup keys are a bit different. Contact With Local RawContact: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 Contact With Synced RawContact: 2059i6f5de8460f7f227e The Contact with unsynced, device-only, local RawContact has a much longer (or shorter e.g. 0r62-2A2C2E) lookup key and starts with \"0r -\" and all characters after it are in uppercase. The other thing to notice is that the \"55\" in \"0r55-\" seems to be the same as the RawContact ID (I did a bit more experiments than what is written in these notes to confirm that it is indeed the RawContact ID and not the Contact ID). We probably don't need to worry about these details though the Contacts Provider probably uses these things internally. We also should not rely on it. However, it may be safe to assume that the Contact lookup key is a reference to a RawContact (or reference to more than one constituent RawContact when multiple RawContacts are linked). Again, an internal Contacts Provider detail we should not rely on BUT is probably relevant when implementing sync adapters. When we link the two, we get... Contact id: 55, lookupKey: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50.2059i6f5de8460f7f227e, displayNamePrimary: Contact With Synced RawContact #### RawContacts table RawContact id: 55, contactId: 55, displayNamePrimary: Contact With Local RawContact RawContact id: 56, contactId: 55, displayNamePrimary: Contact With Synced RawContact Notice, Contact with ID 56 has been deleted. Contact with ID 55 still exist with the lookup keys of both Contact 55 and 56 combined separated by a \".\" . This holds true in cases where two or more local-only or non-local-only RawContacts are linked. RawContacts remain unchanged except reference to Contact 56 has been replaced with 55. The primary display name of Contact 55 came from RawContact 55 prior to the link and now comes from RawContact 56 after the link. This primary name resolution is probably irrelevant so pay no attention to it. The most important part to notice is that the lookup keys get combined. The lookup uri is required to build a Contacts.CONTENT_LOOKUP_URI ... /** * A content:// style URI for this table that should be used to create * shortcuts or otherwise create long-term links to contacts. This URI * should always be followed by a \"/\" and the contact's {@link #LOOKUP_KEY}. * It can optionally also have a \"/\" and last known contact ID appended after * that. This \"complete\" format is an important optimization and is highly recommended. *

        * As long as the contact's row ID remains the same, this URI is * equivalent to {@link #CONTENT_URI}. If the contact's row ID changes * as a result of a sync or aggregation, this URI will look up the * contact using indirect information (sync IDs or constituent raw * contacts). *

        * Lookup key should be appended unencoded - it is stored in the encoded * form, ready for use in a URI. */ public static final Uri CONTENT_LOOKUP_URI = Uri . withAppendedPath ( CONTENT_URI , \"lookup\" ); /** * Build a {@link #CONTENT_LOOKUP_URI} lookup {@link Uri} using the * given {@link ContactsContract.Contacts#_ID} and {@link #LOOKUP_KEY}. *

        * Returns null if unable to construct a valid lookup URI from the * provided parameters. */ public static Uri getLookupUri ( long contactId , String lookupKey ) { if ( TextUtils . isEmpty ( lookupKey )) { return null ; } return ContentUris . withAppendedId ( Uri . withAppendedPath ( Contacts . CONTENT_LOOKUP_URI , lookupKey ), contactId ); } From the lookup uri, we can lookup the Contact row... public static Uri lookupContact ( ContentResolver resolver , Uri lookupUri ) { ... } Or simply get the Contact ID... // code inside `public static Uri lookupContact` resolver . query ( lookupUri , new String [] { Contacts . _ID }, null , null , null ) However, given that the lookup key of the deleted Contact 56 still lives on, it is possible to get the linked Contact 55 using the lookup key of Contact 56 using our standard query APIs! . where { Contact . LookupKey contains lookupKey } The above is correct as long as these assumptions hold true; the lookup key is unique there is no lookup key that can contain a shorter lookup key the Contact ID fails this test because a smaller number is contained in a larger number synced contacts have shorter lookup keys than local contacts. However, local contacts' lookup keys are capitalized whereas synced contact are not. Also, there seems to be other differences in pattern between long and short lookup keys. It should be safe to make this assumption. Until the community finds that this assumption is flawed, we'll assume that it is true! For now, we can avoid having to create another API or extensions just for using lookup keys . When we unlink , we get... #### Contacts table Contact id: 55, lookupKey: 2059i6f5de8460f7f227e, displayNamePrimary: Contact With Synced RawContact Contact id: 58, lookupKey: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50, displayNamePrimary: Contact With Local RawContact #### RawContacts table RawContact id: 55, contactId: 58, displayNamePrimary: Contact With Local RawContact RawContact id: 56, contactId: 55, displayNamePrimary: Contact With Synced RawContact Notice, A new Contact row with ID of 58 is created. The lookup keys are separated and distributed between Contact 55 and 58. RawContact 55 Contact reference has been set to Contact 58. Let's compare the Contact-RawContact relationship before and after linking and then unlinking. Contact ID Lookup Key RawContact.Contact ID Before 55, 56 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50, 2059i6f5de8460f7f227e 55, 56 After 55, 58 2059i6f5de8460f7f227e, 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 58, 55 Notice, Contact ID 55 swapped lookup keys with the former Contact 56 (now 58). RawContact ID 55 swapped Contact reference with RawContact 56. The Contact IDs and lookup keys got shuffled BUT the Contact-RawContact relationship remains the same if using the lookup keys as point of reference! Here is another way to look at the table, using the lookup key as the constant... Lookup Key Before After 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 Contact 55, RawContact 55 Contact 58, RawContact 55 2059i6f5de8460f7f227e Contact 56, RawContact 56 Contact 55, RawContact 56 Notice that the indirect relationship between the lookup key and RawContacts remains the same before and after the link-unlink even though the Contact IDs changed. As mentioned earlier in this section, the \"55\" in \"0r55-\" seems to be referencing the RawContact ID. In other words, since local RawContacts are not synced or tracked in a remote database where Contacts -> RawContacts mappings exist, the Contacts Provider most likely uses this \"0r -\" pattern to make the connection. This is not really relevant for us as we are not relying on this mechanism. I'm just pointing out my observations, which could be incorrect. This means that... If users of this library saved a reference Contact ID 55, then a link-unlink (or sync adapter functions) occur. Getting Contact by ID 55 will result in the RawContact-Data of the former Contact 56 to be returned. This is a bug! Same goes if users saved a reference to Contact ID 56. If users of this library saved a reference to the lookup keys, then a link-unlink (or sync adapter functions) occur. Getting Contact by lookup key will result in the correct RawContact-Data to be returned. So when to use Contact ID vs lookup key? Lookup key: for a reference to a Contact that needs to be loaded after some period of time. Saving/restoring activity/fragment instance state. Saving to an external database, preferences, or files. Creating shortcuts. ID: for everything else. Performing read/write operations in the same function call or session in your app. Performing read/write operations that require ID (e.g. Contact photo and options). Another thing to check is what happens when associating a local RawContact to an Account (move from device to Account) and vice versa. Is the lookup key of the Contact affected? After associating the local RawContact to an Account... #### Contacts table Contact id: 58, lookupKey: 2059i4abd4a8f8ff89642 #### RawContacts table RawContact id: 55, contactId: 58 The lookup key changed but the Contact ID remained the same! In this case, loading a reference to the previously local Contact will fail! I verified that this is indeed the behavior of the native (AOSP) Contacts app. Moving the RawContact from device to Google using Google Contacts app while having Contact details activity opened in the AOSP Contacts app will result in \"error Contact does not exist\" message in the AOSP Contacts app! The RawContact and its Data also remained the same in this case. Removing the account from it results in... #### Contacts table Contact id: 59, lookupKey: 0r58-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 #### RawContacts table RawContact id: 58, contactId: 59 The Contact and RawContacts row have been deleted and new rows have been created to replace them! I also verified that the Data rows have also been deleted and new rows have been created to replace them! This stuff is not really relevant for lookup key but still good to know for implementing moving between accounts in the future.","title":"Contacts; ID vs LOOKUP_KEY"},{"location":"dev-notes/#rawcontacts-accounts-contacts","text":"The RawContacts table associates a person to an android.accounts.Account that it belongs to. Each new RawContacts row created results in; a new row in the Contacts table (unless the RawContact is associated to another existing Contact) a new row in the RawContacts with account name and type set to null 0 or more rows in the Data table with a reference to the new Contacts and RawContacts Ids It is possible to create RawContacts without any rows in the Data table. See the Data required section for more details. For example, creating 4 new contacts using the native Android Contacts app results in; Contact id: 4, displayName: First Local Contact Contact id: 5, displayName: Second Local Contact Contact id: 6, displayName: Third Local Contact Contact id: 7, displayName: Third Local Contact RawContact id: 4, accountName: null, accountType: null RawContact id: 5, accountName: null, accountType: null RawContact id: 6, accountName: null, accountType: null RawContact id: 7, accountName: null, accountType: null Data id: 15, rawContactId: 4, contactId: 4, data: First Local Contact Data id: 16, rawContactId: 5, contactId: 5, data: Second Local Contact Data id: 17, rawContactId: 6, contactId: 6, data: Third Local Contact Data id: 18, rawContactId: 7, contactId: 7, data: Third Local Contact Local Contacts / RawContacts RawContacts inserted without an associated account are considered local or device-only raw contacts, which are not synced. The native Contacts app hides the following UI fields when inserting or updating local raw contacts; - Event - Relation - Group memberships To enforce this behavior, this library ignores all of the above during inserts and updates for local raw contacts. Lollipop (API 22) and below When an Account is added, from a state where no accounts have yet been added to the system, the Contacts Provider automatically sets all of the null accountName and accountType in the RawContacts table to that Account's name and type; RawContact id: 4, accountName: vestrel00@gmail.com, accountType: com.google RawContact id: 5, accountName: vestrel00@gmail.com, accountType: com.google RawContact id: 6, accountName: vestrel00@gmail.com, accountType: com.google RawContact id: 7, accountName: vestrel00@gmail.com, accountType: com.google RawContacts inserted without an associated account will automatically get assigned to an account if there are any available. This may take a few seconds, whenever the Contacts Provider decides to do it. Dissociating RawContacts from Accounts will result in the Contacts Provider associating those back to an Account. Marshmallow (API 23) and above The Contacts Provider no longer associates local contacts to an account when an account is or becomes available. Local contacts remain local. Account removal Removing the Account will remove all of the associated rows in the local RawContact, Data, and Groups tables. This includes user Profile data in those tables. SyncColumns modifications This library supports modifying the SyncColumns.ACCOUNT_NAME and SyncColumns.ACCOUNT_TYPE of the RawContacts table in some cases only. In some cases does not work as intended and produces unwanted side-effects. It probably has something to do with syncing with remote servers and local Account / sync data not matching up similar to errors on network requests if the system time does not match network time. The motivation behind changing the Account columns of the RawContacts table rows is that it would allow users to; Associate local RawContacts (those that are not associated with an Account) to an Account, allowing syncing between devices. Dissociate RawContacts from their Account such that they remain local to the device and not synced between devices. Transfer RawContacts from one Account to another. When modifying the SyncColumns directly, the first works as intended. The second works with some unwanted side-effects. The third does not work at all and produces unwanted side-effects. These are the behaviors that I have found; Associating local RawContact A to Account X. Works as intended. RawContact A is now associated with Account X and is synced across devices. Dissociating RawContact A (setting the SyncColumns' Account name and type to null) from Account X. Partially works with some unwanted-side effects. Dissociates RawContact A from the device but not other devices. RawContact A is no longer visible in the native Contacts app UNLESS it retains the group membership to at least the default group from an Account. At this point, RawContact A is a local contact. Changes to this local RawContact A will not be synced across devices. If RawContact A is updated in another device and synced up to the server, then a syncing side-effect occurs because the RawContact A in the device is different from the RawContact A in the server. This causes the Contacts Provider to create another RawContact, resulting in a \"duplicate\". The two RawContact As may get aggregated to the same Contact depending on how similar they are. If local RawContact A is re-associated back to Account X, it will still no longer be synced. Associating RawContact A from original Account X to Account Y. Does not work and have bad side-effects. No change in other devices. For Lollipop (API 22) and below, RawContact A is no longer visible in the native Contacts app and syncing Account Y in system settings fails. For Marshmallow (API 23) and above, RawContact A is no longer visible in the native Contacts app. RawContact A is automatically deleted locally at some point by the Contacts Provider. Syncing Account Y in system settings succeeds. Given that associating originally local RawContacts to an Account is the only thing that actually works, it is the only function that will be exposed to consumers. If consumers want to transfer RawContacts from one Account to another, they can create a copy of a RawContact associated with the desired Account and then delete the original RawContact. Same idea can be used to transform an Account-associated RawContact to a local RawContact. Perhaps we can implement some functions in this library that does these things? We won't for now because the native Contacts app does not support these functions anyways. It can always be implemented later if the community really wants. Here are some other things to note. The Contacts Provider automatically creates a group membership to the default group of the target Account when the account changes. This occurs even if the group membership already exists resulting in duplicates. The Contacts Provider DOES NOT delete existing group memberships when the account changes. This has to be done manually to prevent duplicates.","title":"RawContacts; Accounts + Contacts"},{"location":"dev-notes/#rawcontacts-deletion","text":"Deleting a contact's Contacts row, RawContacts row(s), and associated Data row(s) are best explained in the documentation in ContactsContract.RawContacts ; When a raw contact is deleted, all of its Data rows as well as StatusUpdates, AggregationExceptions, PhoneLookup rows are deleted automatically. When all raw contacts associated with a Contacts row are deleted, the Contacts row itself is also deleted automatically. The invocation of resolver.delete(...), does not immediately delete a raw contacts row. Instead, it sets the ContactsContract.RawContactsColumns.DELETED flag on the raw contact and removes the raw contact from its aggregate contact. The sync adapter then deletes the raw contact from the server and finalizes phone-side deletion by calling resolver.delete(...) again and passing the ContactsContract#CALLER_IS_SYNCADAPTER query parameter. Some sync adapters are read-only, meaning that they only sync server-side changes to the phone, but not the reverse. If one of those raw contacts is marked for deletion, it will remain on the phone. However it will be effectively invisible, because it will not be part of any aggregate contact. TLDR To delete a contacts and all associated rows, simply delete all RawContact rows with the desired Contacts id. Deletion of the Contacts row and associated Data row(s) will be done automatically by the Contacts Provider. Note that deleting a RawContacts row may not immediately delete the RawContacts row. In this case, it is marked as deleted and its reference to a contact id is nulled. The Contact may still exist if it still has at least one constituent RawContact that is not marked for deletion. A RawContact is marked for deletion as specified by RawContactsColumns.DELETED . Typically, deleting RawContacts immediately removes the row from the RawContacts table. However, RawContacts row remains and is simply marked for deletion UNTIL the sync adapters syncs the changes. One of the reasons syncs do not occur is when the system sync settings are turned off for the Account or there is no network connection. Such RawContacts should not be included in query results for Contacts. The AOSP and Google Contacts app also does not show them. Note that local RawContacts rows (not associated with an Account) are deleted immediately as no sync needs to occur.","title":"RawContacts; Deletion"},{"location":"dev-notes/#multiple-rawcontacts-per-contact","text":"Each row in the Contacts table may be associated with more than one row in the RawContacts table. The Contacts Provider may consolidate multiple contacts belonging to different accounts and combine them into a single entry in the Contacts table whilst maintaining the separate entries in the RawContacts table. A more likely scenario that causes multiple RawContacts per Contact is when two or more Contacts are \"linked\" (or \"merged\" for API 23 and below, or \"joined\" for API 22 and below).","title":"Multiple RawContacts Per Contact"},{"location":"dev-notes/#behavior-of-linkingmergingjoining-contacts-aggregationexceptions","text":"The native Contacts app terminology has changed over time; API 22 and below; join / separate API 23; merge / unmerge API 24 and above; link / unlink However, the internals have not changed; KEEP_TOGETHER / KEEP_SEPARATE . These operations are supported by the ContactsContract.AggregationExceptions . For example, given the following tables, ### Contacts table Contact id: 32, displayName: X, starred: 0, timesContacted: 1, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 Contact id: 33, displayName: Y, starred: 1, timesContacted: 2, lastTimeContacted: 1573071750624, customRingtone: content://media/internal/audio/media/115, sendToVoicemail: 1 ### RawContacts table RawContact id: 30, contactId: 32, displayName: X, accountName: x@x.com, accountType: com.google, starred: 0, timesContacted: 1, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 RawContact id: 31, contactId: 33, displayName: Y, accountName: y@y.com, accountType: com.google, starred: 1, timesContacted: 2, lastTimeContacted: 1573071750624, customRingtone: content://media/internal/audio/media/115, sendToVoicemail: 1 ### Data table Data id: 57, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/group_membership, data1: 18 Data id: 58, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/name, data1: X, isPrimary: 1, isSuperPrimary: 1 Data id: 59, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: x@x.com Data id: 60, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: xx@x.com, isPrimary: 1, isSuperPrimary: 1 Data id: 63, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/group_membership, data1: 6 Data id: 64, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/name, data1: Y, isPrimary: 1, isSuperPrimary: 1 Data id: 65, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/email_v2, data1: y@y.com Data id: 66, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/email_v2, data1: yy@y.com, isPrimary: 1, isSuperPrimary: 1 When Contact X links/merges/joins Contact Y , the tables becomes; ### Contacts table Contact id: 32, displayName: X, starred: 1, timesContacted: 2, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 ### RawContacts table RawContact id: 30, contactId: 32, displayName: X, accountName: x@x.com, accountType: com.google, starred: 0, timesContacted: 1, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 RawContact id: 31, contactId: 32, displayName: Y, accountName: y@y.com, accountType: com.google, starred: 1, timesContacted: 2, lastTimeContacted: 1573071750624, customRingtone: content://media/internal/audio/media/115, sendToVoicemail: 1 ### Data table Data id: 57, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/group_membership, data1: 18 Data id: 58, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/name, data1: X, isPrimary: 1, isSuperPrimary: 1 Data id: 59, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: x@x.com Data id: 60, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: xx@x.com, isPrimary: 1, isSuperPrimary: 0 Data id: 63, rawContactId: 31, contactId: 32, mimeType: vnd.android.cursor.item/group_membership, data1: 6 Data id: 64, rawContactId: 31, contactId: 32, mimeType: vnd.android.cursor.item/name, data1: Y, isPrimary: 1, isSuperPrimary: 0 Data id: 65, rawContactId: 31, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: y@y.com Data id: 66, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/email_v2, data1: yy@y.com, isPrimary: 1, isSuperPrimary: 0 What changed? Contact Y's row has been deleted and its column values have been merged into Contact X row. If the reverse occurred (Contact Y merged with Contact X), Contact Y's row would still be deleted. The difference is that Contact X's display name will be set to Contact Y's display name, which is done by the native Contacts app manually by setting Contact Y's Data name row to be the \"default\" (isPrimary and isSuperPrimary both set to 1). The AggregationExceptions table records the linked RawContacts's IDs in ascending order regardless of the order used in RAW_CONTACT_ID1 and RAW_CONTACT_ID2 at the time of merging. The RawContacts and Data table remains the same except the joined contactId column values have now been changed to the id of Contact X. All Data rows' isSuperPrimary value has been set to 0 though the isPrimary columns remain the same. In other words, this clears any \"default\" set before the link. These are done automatically by the Contacts Provider during the link operation. What is not done automatically by the Contacts Provider is that the name row of former Contact X is set as the default. The native Contacts app does this manually. The Contacts Providers automatically sets the Contact display name to whatever the default name row is for the Contact, if available. For more info on Contact display name resolution, read the Contact Display Name and Default Name Rows section. Note that display name resolution is different for APIs below 21 (pre-lollipop). The display name of the RawContacts remain the same. The Groups table remains unmodified. Options updates Changes to the options (starred, timesContacted, lastTimeContacted, customRingtone, and sendToVoicemail) of a RawContact may affect the options of the parent Contact. On the other hand, changes to the options of the parent Contact will be propagated to all child RawContact options. Photo updates A RawContact may have a full-sized photo saved as a file and a thumbnail version of that saved in the Data table in a photo mimetype row. A Contact's full-sized photo and thumbnail are simply references to the \"chosen\" RawContact's full-sized photo and thumbnail (though the URIs may differ). Note that when removing the photo in the native contacts app, the photo data row is not immediately deleted, though the PHOTO_FILE_ID is immediately set to null. This may result in the PHOTO_URI and PHOTO_THUMBNAIL_URI to still have a valid image uri even though the photo has been \"removed\". This library immediately deletes the photo data row, which seems to work perfectly. Data inserts In the native Contacts app, Data inserted in combined (raw) contacts mode will be associated to the first RawContact in the list sorted by the RawContact ID. This may not be the same as the RawContact referenced by ContactsColumns.NAME_RAW_CONTACT_ID . UI changes? The native Contacts App does not display the groups field when displaying / editing Contacts that have multiple RawContacts (linked/merged/joined) in combined mode. However, it does allow editing individual RawContact Data rows in which case the groups field is displayed and editable. In the native Contacts app, the name attribute used comes from the name row with IS_SUPER_PRIMARY set to true. This and all other \"unique\" mimetypes (organization) and non-unique mimetypes (email) per RawContact are shown only if they are not blank. Showing multiple RawContact's data in the same edit screen (combined mode) In older version of the native, Android Open Source Project (AOSP) Contacts app, data from multiple RawContacts was being shown in the same edit screen. This caused a lot of confusion about which data belonged to which RawContact. Newer versions of AOSP Contacts only allow editing one RawContact at a time to avoid confusion. Though, several RawContacts' data are still shown (not-editable) in the same screen.","title":"Behavior of linking/merging/joining contacts (AggregationExceptions)"},{"location":"dev-notes/#aggregationexceptions-table","text":"Given the following Contacts and their RawContacts; Contact A RawContact 1 Contact B RawContact 2 Contact C RawContact 3 Contact D RawContact 4 Linking one by one in this order; Contact B link Contact A Contact C link Contact D Contact C link Contact B Results in the following AggregationExceptions rows respectively; Aggregation exception id: 430, type: 1, rawContactId1: 1, rawContactId2: 2 Aggregation exception id: 430, type: 1, rawContactId1: 1, rawContactId2: 2 Aggregation exception id: 432, type: 1, rawContactId1: 3, rawContactId2: 4 Aggregation exception id: 436, type: 1, rawContactId1: 1, rawContactId2: 2 Aggregation exception id: 439, type: 1, rawContactId1: 1, rawContactId2: 3 Aggregation exception id: 442, type: 1, rawContactId1: 1, rawContactId2: 4 Aggregation exception id: 440, type: 1, rawContactId1: 2, rawContactId2: 3 Aggregation exception id: 443, type: 1, rawContactId1: 2, rawContactId2: 4 Aggregation exception id: 444, type: 1, rawContactId1: 3, rawContactId2: 4 There is a pattern here. RawContact ids are sorted in ascending order and linked from least to greatest exhaustively but no double links (1-2 is the same as 2-1). RawContact 1 has a row with RawContact 2, 3, and 4. RawContact 2 has a row with RawContact 3 and 4. RawContact 3 has a row with RawContact 4. Linking all in one go; Contact C link Contact A, B, D Results in the same AggregationExceptions rows. Unlinking results in the same AggregationExceptions rows except the type is 2 (TYPE_KEEP_SEPARATE).","title":"AggregationExceptions table"},{"location":"dev-notes/#data-table","text":"The Data table uses generic column names (e.g. \"data1\", \"data2\", ...) using the column \"mimetype\" to distinguish the type of data in that generic column. For example, the column name of StructuredName.DISPLAY_NAME is the same as Email.ADDRESS , which is \"data1\". Each row in the Data table consists of a piece of RawContact data (e.g. a phone number), its \"mimetype\", and the associated RawContact and Contact id. A row does not contain all of the data for a contact. RawContacts may only have one row of certain mimetypes and may have multiple rows of other mimetypes. Here is the list. Unique mimetype per RawContact Name (StructuredName) Nickname Note Organization Photo SipAddress Non-unique mimetype per Raw Contact Address (StructuredPostal) Email Event GroupMembership Im Phone Relation Website Although some mimetypes are unique per RawContact, none of those mimetypes are unique per Contact because a Contact is an aggregate of one or more RawContacts!","title":"Data Table"},{"location":"dev-notes/#data-primary-and-super-primary-rows","text":"As per documentation, for a set of data rows with the same mimetype (e.g. a set of emails), there should only be one primary data row (e.g. email) per RawContact and one super primary data row per Contact. Furthermore, a data row that is super primary must also be primary. Unfortunately, the Contacts Provider does not do any data set validation for the Data columns IS_PRIMARY and IS_SUPER_PRIMARY . This means that it is possible to set more than one data row of the same mimetype as primary for the same RawContact and super primary for the same aggregate Contact. It is also possible to set a data row as super primary but not primary. Upholding the the contract is left to us... For example, given this relationship; Contact RawContact X Email A Email B RawContact Y Email C Email D When Emails A, B, C, and D are inserted with the RawContacts or after the RawContacts have been created, we get the following state; Email Primary Super Primary A 0 0 B 0 0 C 0 0 D 0 0 The state does not change when RawContact X is linked with RawContact Y. After setting Email A as the \"default\" email, it becomes primary and super primary; Email Primary Super Primary A 1 1 B 0 0 C 0 0 D 0 0 Then setting Email B as the default email, it becomes primary and super primary. Email A is no longer primary or super primary. Email Primary Super Primary A 0 0 B 1 1 C 0 0 D 0 0 Then setting Email C as the default email, it becomes primary and super primary. Email B is still primary because it belongs to a different RawContact than Email C. However, Email B is no longer the super primary as there can only be one per aggregate Contact. Email Primary Super Primary A 0 0 B 1 0 C 1 1 D 0 0 Then setting Email D as the default email, it becomes primary and super primary. Email C is no longer primary or super primary. Email Primary Super Primary A 0 0 B 1 0 C 0 0 D 1 1 Then clearing the default email D, removes its primary and super primary status. However, email B remains a primary but not a super primary. Email Primary Super Primary A 0 0 B 1 0 C 0 0 D 0 0 The above behavior is observed from the native Contacts app. The \"super primary\" data of an aggregate Contact is referred to as the \"default\". At this point, the native Contacts app still shows email B as the first email in the list even though it isn't the \"default\" (super primary) because it is still a primary. This adds a bit of confusion in my opinion, especially when more than 2, 3, or 4 RawContacts are linked. A \"fix\" would be to only order the list of emails using \"super primary\" instead of \"super primary\" and \"primary\". OR to remove the primary status of the data set of all linked RawContacts. One benefit of the native Contacts implementation of this is that it retains the primary status when unlinking RawContacts. This library should follow what the native Contacts app is doing in spirit of recreating the native experience as closely as possible, even if it seems like a lesser experience.","title":"Data Primary and Super Primary Rows"},{"location":"dev-notes/#data-table-joins","text":"All columns accessible via cursors returned from Data table queries are specified in DataColumnsWithJoins , which includes the DataColumns , ContactsColumns , and ContactOptionsColumns . In code, mentions of the \"Data table\" typically refers to the joined table. The DataColumns gives us access to all of the columns in the Data table. All other joined columns, including the ContactsColumns are appended to each row in the query. This means that the ContactsColumns ; DISPLAY_NAME , PHOTO_URI , and PHOTO_THUMBNAIL_URI are repeated for all Data rows belonging to the same Contact. The ContactOptionsColumns values joined with the Data table are the values of the Contact, not the RawContact that the Data row belongs to! The same applies to the \"display_name\".","title":"Data Table Joins"},{"location":"dev-notes/#data-updates","text":"A new row in the Data table is created for each new piece of data (e.g. email address) entered for the contact. Removing a piece of existing data results in the deletion of the row in the Data table if that row no longer contains any meaningful data (no meaningful non-null \"datax\" columns left). This is the behavior of the native Android Contacts app. Therefore, querying for null fields is not possible. For example, there may be no Data rows that exist where the email address is null. Thus, a query to search for all contacts with null email address may return 0 contacts even if there are some contacts without email addresses.","title":"Data Updates"},{"location":"dev-notes/#data-required","text":"Creating blank RawContacts without email address (or other fields), results in no rows in the Data table for the email address, and all other fields. There are a few exceptions. The following Data rows are automatically created for all contacts, if not provided; Group membership, underlying value defaults to the account's default system group Name, underlying value defaults to null Nickname, underlying value defaults to null Note, underlying value defaults to null Note that all of the above rows are only automatically created for RawContacts that are associated with an Account. If a valid account is provided, the default (auto add) system group membership row is automatically created immediately by the Contacts Provider at the time of contact insertion. The name, nickname, and note are automatically created at a later time. If a valid account is not provided, none of the above data rows are automatically created. Blank RawContacts The Contacts Providers allows for RawContacts that have no rows in the Data table (let's call them \"blanks\") to exist. The native Contacts app does not allow insertion of new RawContacts without at least one data row. It also deletes blanks on update. Despite seemingly not allowing blanks, the native Contacts app shows them. There are two scenarios where blanks may exist. Contact with RawContact(s) with no Data row(s). In this case, the Contact is blank as well as its RawContact(s). Contact that has RawContact with Data row(s) and a RawContact with no Data row. In this case, the Contact and the RawContact with Data row(s) are not blank but the RawContact with no Data row is blank.","title":"Data Required"},{"location":"dev-notes/#data-structuredname","text":"The DISPLAY_NAME is the unstructured representation of the name. It is made up of structured components; PREFIX , GIVEN_NAME , MIDDLE_NAME , FAMILY_NAME , and SUFFIX . When updating or inserting a row; If the display name is null and there are non-null structured components provided (e.g. given and family name), the Contacts Provider will automatically set the display name by combining the structured components. If the display name is not null and all structured components are null, the Contacts Provider automatically (to the best of its ability) derive the values for all the structured components. If the display name and structured components are not null, the Contacts Provider does nothing automatically.","title":"Data StructuredName"},{"location":"dev-notes/#data-structuredpostal","text":"The FORMATTED_ADDRESS is the unstructured representation of the postal address. It is made up of structured components; STREET , POBOX , NEIGHBORHOOD , CITY , REGION , POSTCODE , and COUNTRY . When updating or inserting a row; If the formatted address is null and there are non-null structured components provided (e.g. street and city), the Contacts Provider will automatically set the formatted address by combining the structured components. If the formatted address is not null and all structured components are null, the Contacts Provider automatically sets the street value to the formatted address. If the formatted address and structured components are not null, the Contacts Provider does nothing automatically.","title":"Data StructuredPostal"},{"location":"dev-notes/#groups-table-accounts","text":"Contacts are assigned to one or more groups via the GroupMembership . It typically looks like this; Group id: 1, systemId: Contacts, readOnly: 1, title: My Contacts, favorites: 0, autoAdd: 1, accountName: vestrel00@gmail.com, accountType: com.google Group id: 2, systemId: null, readOnly: 1, title: Starred in Android, favorites: 1, autoAdd: 0, accountName: vestrel00@gmail.com, accountType: com.google Group id: 3, systemId: Friends, readOnly: 1, title: Friends, favorites: 0, autoAdd: 0, accountName: vestrel00@gmail.com, accountType: com.google Group id: 4, systemId: Family, readOnly: 1, title: Family, favorites: 0, autoAdd: 0, accountName: vestrel00@gmail.com, accountType: com.google Group id: 5, systemId: Coworkers, readOnly: 1, title: Coworkers, favorites: 0, autoAdd: 0, accountName: vestrel00@gmail.com, accountType: com.google Group id: 6, systemId: null, readOnly: 0, title: Custom Group, favorites: 0, autoAdd: 0, accountName: vestrel00@gmail.com, accountType: com.google The actual groups are in a separate table; Groups. Each group is associated with an Account. No group can exist without an account. It is account-exclusive. Each account will have its own set of the above system groups. This means that there may be multiple groups with the same title belonging to different accounts. System ids are typically Contacts, Friends, Family, and Coworkers. These ids are typically the same across all copies of Android. Notes; - The Contacts system group is the default group in which all raw contacts of an account belongs to. Therefore, it is typically hidden when showing the list of groups in the UI. - The starred (favorites) group is not a system group as it has null system id. However, it behaves like one in that it is read only and it comes with most (if not all) copies of the native app. Removing the Account will delete all of the associated rows in the Groups table. Groups, duplicate titles The Contacts Provider allows multiple groups with the same title (case-sensitive comparison) belonging to the same account to exist. In older versions of Android, the native Contacts app allows the creation of new groups with existing titles. In newer versions, duplicate titles are not allowed. Therefore, this library does not allow for duplicate titles. In newer versions, the group with the duplicate title gets deleted either automatically by the Contacts Provider or when viewing groups in the native Contacts app. It's not an immediate failure on insert or update. This could lead to bugs!","title":"Groups Table & Accounts"},{"location":"dev-notes/#groups-table-groupmemberships-data-table","text":"There may be multiple groups with the same title from different accounts. Therefore, the group membership should point to the group belonging to the same account as the raw contact. The native Contacts app displays only the groups belonging to the selected account. Updating group memberships of existing raw contacts seem to be almost instant. All raw contacts must be a part of at least the default group (system id is \"Contacts\"). Raw contacts with no group membership will be asynchronously added to the Account's default group by the Contacts Provider. Membership to the default group should never be deleted!","title":"Groups Table & GroupMemberships (Data Table)"},{"location":"dev-notes/#starred-in-android-favorites","text":"When the ContactOptionsColumns.STARRED column of a Contact in the Contacts table is set to true, the Contacts Provider automatically adds a group membership to the favorites group for all RawContacts linked to the Contact. Setting STARRED to false removes all group memberships to the favorites group. The STARRED is interdependent with group memberships to the favorites group. Adding a group membership to the favorites group results in STARRED being set to true. Removing the membership sets it to false. Raw contacts that are not associated with an account do not have any group memberships. Even though these raw contacts may not have a membership to the favorites group, they may still be \"starred\" (favorited) via the ContactOptionsColumns.STARRED column in the Contacts table, which is not dependent on the existence of a favorites group membership. Refresh RawContact instances after changing the starred value. Otherwise, performing an update on the RawContact with a stale set of group memberships may revert the star/unstar operation. For example, query returns a starred RawContact -> set starred to false -> update RawContact (still containing a group membership to the favorites group) -> starred will be set back to true.","title":"Starred in Android (Favorites)"},{"location":"dev-notes/#group-memberships-local-rawcontacts","text":"Local RawContacts may have a group membership to the default system group of an Account without being associated with the Account... The native Contacts app may not have an edit-RawContact option for newly inserted RawContacts that have no group membership to the default group when an Account is available. Though, edits can still be made in other ways. Instead, an option to \"Add to contacts\" is shown that adds a membership to the default group but does not associate the raw contact to the Account that owns the group. The edit UI does not show the group membership field. Weirdly, this only occurs when there is exactly only one Account. If there are no Accounts or there are two or more Accounts, then this does not occur. Also, this does not occur for a Contact with a RawContact that has a group membership AND a RawContact that has no group membership.","title":"Group memberships & Local RawContacts"},{"location":"dev-notes/#groups-deletion","text":"Similar to deleting RawContacts, deleting a Groups row may not immediately delete the Groups row. In this case, it is marked as deleted. A Group is marked for deletion as specified by GroupsColumns.DELETED . Typically, deleting Groups immediately removes the row from the Groups table. However, Groups row remains and is simply marked for deletion UNTIL the sync adapters syncs the changes. One of the reasons syncs do not occur is when the system sync settings are turned off for the Account or there is no network connection. Such Groups should not be included in query results for Contacts. The AOSP and Google Contacts app also does not show them. Note that local Groups rows (not associated with an Account) are deleted immediately as no sync needs to occur.","title":"Groups; Deletion"},{"location":"dev-notes/#groups-ui","text":"In newer Android versions of the native Contacts app, \"groups\" are now being referred to as \"labels\". However, the underlying code still uses groups. Google is probably just trying to make it more user friendly by calling it label instead of group.","title":"Groups; UI"},{"location":"dev-notes/#user-profile","text":"There exist one (profile) Contacts row that identifies the user; ContactsColumns.IS_USER_PROFILE . There is at least one RawContacts row that is associated with the user profile; RawContactsColumns.RAW_CONTACT_IS_USER_PROFILE . Associated RawContacts may or may not be associated with an Account. The RawContacts row(s) may have rows in the Data table as usual. These profile table rows have special IDs that differ from regular rows. See ContactsContract.isProfileId . Note that the Contacts Provider will throw an IllegalArgument exception when attempting to include ContactsColumns.IS_USER_PROFILE and RawContactsColumns.RAW_CONTACT_IS_USER_PROFILE columns in Data table queries. I have not yet tried including these columns in the Contacts or RawContacts table queries. The profile Contact row may not be merged / linked with other contacts and do not belong to any group (favorites / starred). Profile rows in the Contacts, RawContacts, and Data table are not visible via queries in the respective tables. They will not be in the resulting cursor. To get the profile Contacts table rows, query the Profile.CONTENT_URI . To get profile RawContacts table rows, query the Profile.CONTENT_RAW_CONTACTS_URI . To get the profile Data table rows, query the Profile.CONTENT_RAW_CONTACTS_URI appended with the RawContact id and RawContacts.Data.CONTENT_DIRECTORY . To insert a new profile RawContact, use Profile.CONTENT_RAW_CONTACTS_URI . It will automatically be associated with the profile Contact. If the profile Contact does not yet exist, it will be created automatically. To insert a new profile Data row, either; insert to the Profile.CONTENT_RAW_CONTACTS_URI appended with the RawContact id and RawContacts.Data.CONTENT_DIRECTORY insert to the Data table directly, referencing the RawContact id Same rules apply to all table rows. If all profile RawContacts table rows have been deleted, then associated Contacts and Data table rows will automatically be deleted. Profile aggregation The RawContacts of a (Contact) Profile are linked via the indexed rows; Profile.CONTENT_RAW_CONTACTS_URI . Therefore, the AggregationsExceptions table is not used here. Profile and users Note that as of Android 5 Lollipop, there may exist multiple users in a device. Each user has a separate list of accounts and contact data. This also means that each user has a separate (local) profile contact. Profile and Accounts According to the Profile documentation; \"... each account (including data set, if applicable) on the device may contribute a single raw contact representing the user's personal profile data from that source.\" In other words, one account can have one profile RawContact. Whether or not profile RawContacts associated to an Account can be carried over and synced across devices and users is up to the Contacts Provider / Sync provider for that Account. From my experience, profile RawContacts associated to an Account is not carried over / synced across devices or users. Despite the documentation of \"one profile RawContact per one Account\", the Contacts Provider allows for multiple RawContacts per Account, including multiple local RawContacts (no Account). Thus, we should let consumers exploit this but set defaults to be one-for-one. Creating / setting up the profile in the native Contacts app results in the creation of a local RawContact (not associated with an Account) even if there are available Accounts. The Contacts Provider does not associate local contacts to an account when an account is or becomes available (regardless of API level). Removing the Account will delete all of the associated rows in the Contact, RawContact, Data, and Groups tables. This includes user Profile data in those tables. Profile permissions Profile permissions (READ_PROFILE and WRITE_PROFILE) have been removed since API 23. However, they are still required for API 22 and below. Reading and writing the profile is included in the Contacts permissions. There is no need to ask for profile permissions at runtime because prior to API 23, permissions in the AndroidManifest have to be accepted prior to installation.","title":"User Profile"},{"location":"dev-notes/#syncing-data-sync-adapters","text":"First, it\u2019s good to know the official documentation of sync adapters; https://developer.android.com/guide/topics/providers/contacts-provider#SyncAdapters Now, let\u2019s ingest the official docs\u2026 Data belonging to a RawContact that is associated with a Google account will be available anywhere the Google account is used; in any Android or iOS device, a web browser, etc\u2026 Data is synced by Google\u2019s sync adapters to and from their remote servers. Syncing depends on the account sync settings, which can be configured in the native system settings app and possibly through some remote configuration. This library does not provide any sync adapters. Instead, it relies on existing sync adapters to do the syncing. Sync adapters and syncing are really out of scope of this library. Syncing is its own thing that typically happens outside of an application UI. This library is focused on reading and writing native and custom data to and from the local database. Syncing the local database to and from a remote service is a different story altogether =)","title":"Syncing Data / Sync Adapters"},{"location":"dev-notes/#custom-data-mimetypes","text":"First, it\u2019s good to know the official documentation of custom data rows; https://developer.android.com/guide/topics/providers/contacts-provider#CustomData Now, let\u2019s ingest the official docs\u2026 Custom mimetypes do not belong to the native Contacts Provider mimetype set (e.g. address, email, phone, etc). The Contacts Provider allows for the creation of new / custom mimetypes. This is especially useful for other apps (Google Contacts, Facebook, Twitter, WhatsApp, etc) that want to attach extra pieces of data to a particular RawContact. Custom data are NOT synced, including those that belong to RawContacts that are associated with an Account. Custom sync adapters are required to sync custom data. This library currently does NOT provide custom sync adapters to sync custom data! Custom data from other apps such as Facebook, Twitter, WhatsApp, etc may or may not be synced. It all depends on those applications and their custom sync adapters (if they have any) and sync settings. For insight on how aforementioned social media services may be syncing their data, read through the official documentation; https://developer.android.com/guide/topics/providers/contacts-provider#SocialStream","title":"Custom Data / MimeTypes"},{"location":"dev-notes/#unused-contactscontract-stuff","text":"We are currently not utilizing these things because I haven't found usages of them while using the native Contacts app. They are probably working behind the scenes but until we find uses for these, let's leave it out because YAGNI . Settings . Contacts-specific settings for various Accounts (settings for an Account). Might be useful to add this for SHOULD_SYNC and UNGROUPED_VISIBLE . ContactsColumns.IN_VISIBLE_GROUP + Groups.GROUP_VISIBLE . Flag indicating if the contacts belonging to this group should be visible in any user interface.","title":"Unused ContactsContract Stuff"},{"location":"dev-notes/#java-support","text":"This library is intended to be Java-friendly. The policy is that we should attempt to write Java-friendly code that does not increase lines of code by much or add external dependencies to cater exclusively to Java users.","title":"Java Support"},{"location":"dev-notes/#creating-entities-data-class","text":"First, consumers are not allowed to create immutable entities. Those must come from the API itself to ensure data integrity. Whether or not we will change this in the future is debatable =) Consumers are able to set read-only and private or internal variables though because all Entity implementations are data classes. Data classes provide a copy function that allows for setting any property no matter their visibility and even if the constructor is private. As a matter of fact, setting the constructor of a data class as private gives this warning by Android Studio: \"Private data class constructor is exposed via the 'copy' method. There is currently no way to disable the copy function of data classes (that I know of). The only thing we can do is to provide documentation to consumers, insisting against the use of the copy method as it may lead to unwanted side effects when updating and deleting contacts. We could just use regular classes instead of data classes but entities should be data classes because it is what they are (know what I mean?!). Also, I'd hate to have to generate equals and hashcode functions for them, which will make the code harder to maintain. Though, we might do this anyways at some point if we want to make it possible for a mutable entity to equal an immutable entity. Time will tell =) FIXME? Hide / disable data class copy function if kotlin ever allows it. https://discuss.kotlinlang.org/t/data-class-copy-visibility-modifier/19746","title":"Creating Entities & data class"},{"location":"dev-notes/#immutable-vs-mutable-entities","text":"This library provides true immutability for immutable entities. Take a look at the current (simplified) hierarchy; 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 > ) : RawContactEntity data class Address ( val formattedAddress : String? ) data class MutableAddress ( var formattedAddress : String? ) Note the use of sealed class is to prevent consumers from defining their own entities. This restriction may or may not change in the future. Notice that there is nothing mutable in the immutable Contact . Everything are val s and the data structures used (i.e. RawContact , Address , and List ) are all immutable. This provides consumers 100% confidence that immutable entities are not mutable. They will not change or mutate in any way. Once they are constructed, they will always remain the same. Why immutability is so important will not be covered in this dev notes because it would be too big (that's what she said) and there are blogs and books written about this. One of the most important advantages of immutability is that it is thread-safe. Immutable instances can be used in several different threads without the need for synchronization and worries about deadlocks. In other words, they are thread-safe and faster than the mutable version. The current structure also allows consumers to be able to distinguish between immutable and mutable entities exhaustively. E.G. fun doSomethingAndReturn ( contact : ContactEntity ) = when ( contact ) { is Contact -> {} is MutableContact -> {} } Note that the mutable entities provided in this library are NOT thread-safe . Consumers will have to perform their own synchronizations if they want to use and mutate mutable entities in multi-threaded scenarios.","title":"Immutable vs Mutable Entities"},{"location":"dev-notes/#the-cost-of-the-current-immutability-implementation","text":"The cost of implementing true immutability is more lines of code. Notice that the MutableContact does not inherit from Contact . The same goes for the other entities. This leads to having to write seemingly duplicate code when writing functions and extensions. // FIXME? Furthermore, equality between immutable and mutable entities are not yet implemented. This means that Contact(\"john\") == MutableContact(\"john\") will return false even though their underlying contents are the same. This can be fixed by overriding the equals and hashcode functions of all entities. However, that is a lot more code that I would like to avoid, which is why I'm using data class for all entities in the first place! This may change in the future if the community really wants to change it =) On a side note, the same cost is incurred by Kotlin's standard libs. For example, notice that AbstractMutableList does not inherit from and is completely separate from AbstractList . I'm sure stdlib devs also had to write seemingly duplicate code in implementations of the List interface.","title":"The cost of the current immutability implementation"},{"location":"dev-notes/#avoiding-the-cost-shortcuts-and-pitfalls","text":"One thing that may come to mind in attempts to reduce lines of seemingly duplicate code is to have just a mutable implementation of an immutable declaration. For example, we can restructure the hierarchy to; sealed interface Contact { val rawContacts : List < RawContact > } data class MutableContact ( override val rawContacts : List < MutableRawContact > ) : Contact sealed interface RawContact { val addresses : List < Address > } data class MutableRawContact ( override var addresses : MutableList < MutableAddress > ) : RawContact sealed interface Address { val formattedAddress : String? } data class MutableAddress ( override var formattedAddress : String? ) : Address Notice that there is a non-concrete declaration (i.e. Contact , RawContact , and Address ) and just one concrete implementation (i.e. MutableContact , MutableRawContact , and MutableAddress ). Note that a val declaration can be overridden by a var . Keep in mind that val only requires getters whereas var requires both getters and setters. Therefore, a var cannot be overridden by a val . Or maybe there is a different reason Kotlin imposes this restriction =) On a similar note, the List interface can be overridden to a MutableList . We, as API contributors, can avoid having to write seemingly duplicate functions and extensions! However! Can you see what's wrong with this setup? If we do this, we would either be deceiving consumers to think that the instances of \"immutable\" class signatures (i.e. Contact , RawContact , and Address ) are actually immutable OR we would have to let consumers know that the API does not really provide true immutability. Neither option is ideal (nor is it acceptable IMO). Consumers would have a reference to a Contact , which they may assume is immutable because of the usage of val instead of var , but in actuality the underlying implementation is mutable... This could be a cause of really hard to find bugs in multi-threaded usage. Consumers may use Contact with the assumption that it is immutable only to find that it can actually be mutated! We could fix this by just making the mutable implementation thread-safe but since that is the only implementation, consumers will be forced to use thread-safe code when they don't have to thereby negatively affecting performance. Keep in mind that thread safety is only one of several reasons for immutability. Those other reasons will be violated too. Consumers will be shocked if they ever do the following or something similar. fun x ( contact : Contact ) = when ( contact ) { is MutableContact -> {} // this is always true is Contact -> {} // this is always true } In any case, I have to admit, it is a nice trick that would save API contributors time. But that's just it! It's just a trick. A shortcut. A nice little time save at the cost of integrity. It is not worth it (IMO).","title":"Avoiding the cost... Shortcuts and pitfalls."},{"location":"dev-notes/#why-not-add-android-x-support-library-dependencies","text":"I want to keep the dependency list of this library to a minimum. The Contacts Provider is native to Android since the beginning. I want to honor that fact by avoiding adding dependencies here. I made a bit of an exception by adding the Dexter library for permissions handling for the permissions modules (not in the core modules). I'm tempted to remove the Dexter dependency and implement permissions handling myself because Dexter brings in a lot of other dependencies with it. However, it is not part of the core module so I'm able to live with this. TODO Remove/replace Dexter. It is no longer being maintained. Keeping dependencies to a minimum is just a small challenge I made up. We will see how long it can last! I left comments all over the code on when an androidx dependency may be useful. The most glaring example of this is @WorkerThread. Even with that, I'll hold off on adding the androidx annotation lib. I think we can all be consenting adults =) If the community strongly desires the addition of these support libs, then the community will win =)","title":"Why Not Add Android X / Support Library Dependencies?"},{"location":"accounts/associate-device-local-raw-contacts-to-an-account/","text":"Associate a local RawContact to an Account \u00b6 This library provides the AccountsLocalRawContactsUpdate API, which allows you to associate local RawContacts (those that are not associated with an Account) to an Account in order to enable syncing. An instance of the AccountsLocalRawContactsUpdate API is obtained by, val accountsLocalRawContactsUpdate = Contacts ( context ). accounts (). updateLocalRawContactsAccount () For more info on local RawContacts, read about Local (device-only) contacts . For more info on syncing, read Sync contact data across devices . Basic usage \u00b6 To associate/add the given local RawContacts to the given account, val updateResult = accountsLocalRawContactsUpdate . addToAccount ( account ) . localRawContacts ( rawContacts ) . commit () Executing the update \u00b6 To execute the update, . commit () Handling the update result \u00b6 The commit function returns a Result , To check if all updates succeeded, val allUpdatesSuccessful = updateResult . isSuccessful To check if a particular update succeeded, val firstUpdateSuccessful = updateResult . isSuccessful ( rawContact1 ) Handling update failure \u00b6 The update may fail for a particular RawContact for various reasons, updateResult . failureReason ( rawContact1 ) ?. let { when ( it ) { INVALID_ACCOUNT -> handleInvalidAccount () RAW_CONTACT_IS_NOT_LOCAL -> handleRawContactIsNotLocal () UNKNOWN -> handleUnknownFailure () } } Cancelling the update \u00b6 To cancel an update amid execution, . commit { returnTrueIfUpdateShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel update processing as soon as possible. The function is called numerous times during update processing to check if processing should stop or continue. This gives you the option to cancel the update. For example, to automatically cancel the update inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val updateResult = update . commit { ! isActive } } } Performing the update and result processing asynchronously \u00b6 Updates are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the update with permission \u00b6 These updates require the android.permission.GET_ACCOUNTS and android.permission.WRITE_CONTACTS . If not granted, the update will do nothing and return a failed result. To perform the update with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Profile data \u00b6 The AccountsLocalRawContactsUpdate API also supports updating the Profile (device owner) RawContacts. To get an instance of this API for Profile data updates, val profileDataUpdate = Contacts ( context ). accounts (). profile (). updateLocalRawContactsAccount () All updates will be limited to the Profile RawContacts, whether it exists or not. Developer notes (or for advanced users) \u00b6 The following section are note from developers of this library for other developers. It is copied from the DEV_NOTES . You may still read the following as a consumer of the library in case you need deeper insight. Due to certain limitations and behaviors imposed by the Contacts Provider, this library only provides an API to support; Associate local RawContacts (those that are not associated with an Account) to an Account, allowing syncing between devices. The library does not provide an API that supports; Dissociate RawContacts from their Account such that they remain local to the device and not synced between devices. Transfer RawContacts from one Account to another. SyncColumns modifications \u00b6 This library supports modifying the SyncColumns.ACCOUNT_NAME and SyncColumns.ACCOUNT_TYPE of the RawContacts table in some cases only. In some cases does not work as intended and produces unwanted side-effects. It probably has something to do with syncing with remote servers and local Account / sync data not matching up similar to errors on network requests if the system time does not match network time. The motivation behind changing the Account columns of the RawContacts table rows is that it would allow users to; Associate local RawContacts (those that are not associated with an Account) to an Account, allowing syncing between devices. Dissociate RawContacts from their Account such that they remain local to the device and not synced between devices. Transfer RawContacts from one Account to another. When modifying the SyncColumns directly, the first works as intended. The second works with some unwanted side-effects. The third does not work at all and produces unwanted side-effects. These are the behaviors that I have found; Associating local RawContact A to Account X. Works as intended. RawContact A is now associated with Account X and is synced across devices. Dissociating RawContact A (setting the SyncColumns' Account name and type to null) from Account X. Partially works with some unwanted-side effects. Dissociates RawContact A from the device but not other devices. RawContact A is no longer visible in the native Contacts app UNLESS it retains the group membership to at least the default group from an Account. At this point, RawContact A is a local contact. Changes to this local RawContact A will not be synced across devices. If RawContact A is updated in another device and synced up to the server, then a syncing side-effect occurs because the RawContact A in the device is different from the RawContact A in the server. This causes the Contacts Provider to create another RawContact, resulting in a \"duplicate\". The two RawContact As may get aggregated to the same Contact depending on how similar they are. If local RawContact A is re-associated back to Account X, it will still no longer be synced. Associating RawContact A from original Account X to Account Y. Does not work and have bad side-effects. No change in other devices. For Lollipop (API 22) and below, RawContact A is no longer visible in the native Contacts app and syncing Account Y in system settings fails. For Marshmallow (API 23) and above, RawContact A is no longer visible in the native Contacts app. RawContact A is automatically deleted locally at some point by the Contacts Provider. Syncing Account Y in system settings succeeds. Given that associating originally local RawContacts to an Account is the only thing that actually works, it is the only function that will be exposed to consumers. If consumers want to transfer RawContacts from one Account to another, they can create a copy of a RawContact associated with the desired Account and then delete the original RawContact. Same idea can be used to transform an Account-associated RawContact to a local RawContact. Perhaps we can implement some functions in this library that does these things? We won't for now because the native Contacts app does not support these functions anyways. It can always be implemented later if the community really wants. Here are some other things to note. The Contacts Provider automatically creates a group membership to the default group of the target Account when the account changes. This occurs even if the group membership already exists resulting in duplicates. The Contacts Provider DOES NOT delete existing group memberships when the account changes. This has to be done manually to prevent duplicates.","title":"Associate a local RawContact to an Account"},{"location":"accounts/associate-device-local-raw-contacts-to-an-account/#associate-a-local-rawcontact-to-an-account","text":"This library provides the AccountsLocalRawContactsUpdate API, which allows you to associate local RawContacts (those that are not associated with an Account) to an Account in order to enable syncing. An instance of the AccountsLocalRawContactsUpdate API is obtained by, val accountsLocalRawContactsUpdate = Contacts ( context ). accounts (). updateLocalRawContactsAccount () For more info on local RawContacts, read about Local (device-only) contacts . For more info on syncing, read Sync contact data across devices .","title":"Associate a local RawContact to an Account"},{"location":"accounts/associate-device-local-raw-contacts-to-an-account/#basic-usage","text":"To associate/add the given local RawContacts to the given account, val updateResult = accountsLocalRawContactsUpdate . addToAccount ( account ) . localRawContacts ( rawContacts ) . commit ()","title":"Basic usage"},{"location":"accounts/associate-device-local-raw-contacts-to-an-account/#executing-the-update","text":"To execute the update, . commit ()","title":"Executing the update"},{"location":"accounts/associate-device-local-raw-contacts-to-an-account/#handling-the-update-result","text":"The commit function returns a Result , To check if all updates succeeded, val allUpdatesSuccessful = updateResult . isSuccessful To check if a particular update succeeded, val firstUpdateSuccessful = updateResult . isSuccessful ( rawContact1 )","title":"Handling the update result"},{"location":"accounts/associate-device-local-raw-contacts-to-an-account/#handling-update-failure","text":"The update may fail for a particular RawContact for various reasons, updateResult . failureReason ( rawContact1 ) ?. let { when ( it ) { INVALID_ACCOUNT -> handleInvalidAccount () RAW_CONTACT_IS_NOT_LOCAL -> handleRawContactIsNotLocal () UNKNOWN -> handleUnknownFailure () } }","title":"Handling update failure"},{"location":"accounts/associate-device-local-raw-contacts-to-an-account/#cancelling-the-update","text":"To cancel an update amid execution, . commit { returnTrueIfUpdateShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel update processing as soon as possible. The function is called numerous times during update processing to check if processing should stop or continue. This gives you the option to cancel the update. For example, to automatically cancel the update inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val updateResult = update . commit { ! isActive } } }","title":"Cancelling the update"},{"location":"accounts/associate-device-local-raw-contacts-to-an-account/#performing-the-update-and-result-processing-asynchronously","text":"Updates are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the update and result processing asynchronously"},{"location":"accounts/associate-device-local-raw-contacts-to-an-account/#performing-the-update-with-permission","text":"These updates require the android.permission.GET_ACCOUNTS and android.permission.WRITE_CONTACTS . If not granted, the update will do nothing and return a failed result. To perform the update with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the update with permission"},{"location":"accounts/associate-device-local-raw-contacts-to-an-account/#profile-data","text":"The AccountsLocalRawContactsUpdate API also supports updating the Profile (device owner) RawContacts. To get an instance of this API for Profile data updates, val profileDataUpdate = Contacts ( context ). accounts (). profile (). updateLocalRawContactsAccount () All updates will be limited to the Profile RawContacts, whether it exists or not.","title":"Profile data"},{"location":"accounts/associate-device-local-raw-contacts-to-an-account/#developer-notes-or-for-advanced-users","text":"The following section are note from developers of this library for other developers. It is copied from the DEV_NOTES . You may still read the following as a consumer of the library in case you need deeper insight. Due to certain limitations and behaviors imposed by the Contacts Provider, this library only provides an API to support; Associate local RawContacts (those that are not associated with an Account) to an Account, allowing syncing between devices. The library does not provide an API that supports; Dissociate RawContacts from their Account such that they remain local to the device and not synced between devices. Transfer RawContacts from one Account to another.","title":"Developer notes (or for advanced users)"},{"location":"accounts/associate-device-local-raw-contacts-to-an-account/#synccolumns-modifications","text":"This library supports modifying the SyncColumns.ACCOUNT_NAME and SyncColumns.ACCOUNT_TYPE of the RawContacts table in some cases only. In some cases does not work as intended and produces unwanted side-effects. It probably has something to do with syncing with remote servers and local Account / sync data not matching up similar to errors on network requests if the system time does not match network time. The motivation behind changing the Account columns of the RawContacts table rows is that it would allow users to; Associate local RawContacts (those that are not associated with an Account) to an Account, allowing syncing between devices. Dissociate RawContacts from their Account such that they remain local to the device and not synced between devices. Transfer RawContacts from one Account to another. When modifying the SyncColumns directly, the first works as intended. The second works with some unwanted side-effects. The third does not work at all and produces unwanted side-effects. These are the behaviors that I have found; Associating local RawContact A to Account X. Works as intended. RawContact A is now associated with Account X and is synced across devices. Dissociating RawContact A (setting the SyncColumns' Account name and type to null) from Account X. Partially works with some unwanted-side effects. Dissociates RawContact A from the device but not other devices. RawContact A is no longer visible in the native Contacts app UNLESS it retains the group membership to at least the default group from an Account. At this point, RawContact A is a local contact. Changes to this local RawContact A will not be synced across devices. If RawContact A is updated in another device and synced up to the server, then a syncing side-effect occurs because the RawContact A in the device is different from the RawContact A in the server. This causes the Contacts Provider to create another RawContact, resulting in a \"duplicate\". The two RawContact As may get aggregated to the same Contact depending on how similar they are. If local RawContact A is re-associated back to Account X, it will still no longer be synced. Associating RawContact A from original Account X to Account Y. Does not work and have bad side-effects. No change in other devices. For Lollipop (API 22) and below, RawContact A is no longer visible in the native Contacts app and syncing Account Y in system settings fails. For Marshmallow (API 23) and above, RawContact A is no longer visible in the native Contacts app. RawContact A is automatically deleted locally at some point by the Contacts Provider. Syncing Account Y in system settings succeeds. Given that associating originally local RawContacts to an Account is the only thing that actually works, it is the only function that will be exposed to consumers. If consumers want to transfer RawContacts from one Account to another, they can create a copy of a RawContact associated with the desired Account and then delete the original RawContact. Same idea can be used to transform an Account-associated RawContact to a local RawContact. Perhaps we can implement some functions in this library that does these things? We won't for now because the native Contacts app does not support these functions anyways. It can always be implemented later if the community really wants. Here are some other things to note. The Contacts Provider automatically creates a group membership to the default group of the target Account when the account changes. This occurs even if the group membership already exists resulting in duplicates. The Contacts Provider DOES NOT delete existing group memberships when the account changes. This has to be done manually to prevent duplicates.","title":"SyncColumns modifications"},{"location":"accounts/query-accounts/","text":"Query for Accounts \u00b6 This library provides the AccountsQuery API that allows you to retrieve Account s from the AccountManager . An instance of the AccountsQuery API is obtained by, val query = Contacts ( context ). accounts (). query () A basic query \u00b6 To get all available accounts in the system, val accounts = Contacts ( context ). accounts (). query () . find () To get all available accounts in the system with an account type of \"com.google\" or \"com.yahoo\", val accounts = Contacts ( context ). accounts (). query () . withTypes ( \"com.google\" , \"com.yahoo\" ) . find () To get the account for a set of RawContacts, val account = Contacts ( context ). accounts (). query () . associatedWith ( rawContacts ) . find () To get all available accounts in the system with an account type of \"com.google\" or \"com.yahoo\" AND is associated with at least one of the given RawContacts, val accounts = Contacts ( context ). accounts (). query () . withTypes ( \"com.google\" , \"com.yahoo\" ) . associatedWith ( rawContacts ) . find () RawContacts that are not associated with an Account are local to the device. For more info, read about Local (device-only) contacts . Account for each specified RawContact \u00b6 When you perform a query that uses associatedWith without using withTypes , you are able to get the Account for each of the RawContact specified. val rawContactAccount = accounts . accountFor ( rawContact ) This allows you to get the accounts for multiple RawContacts in one API call =) Cancelling the query \u00b6 To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val contacts = query . find { ! isActive } } } Performing the query asynchronously \u00b6 Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the query with permission \u00b6 Queries require the android.permission.READ_CONTACTS and android.permission.GET_ACCOUNTS permissions. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Profile accounts \u00b6 The AccountsQuery API also supports querying the Profile (device owner) contact data. To get an instance of this API for Profile queries, val query = Contacts ( context ). accounts (). profile (). query () All queries will be limited to the Profile, whether it exists or not.","title":"Query for Accounts"},{"location":"accounts/query-accounts/#query-for-accounts","text":"This library provides the AccountsQuery API that allows you to retrieve Account s from the AccountManager . An instance of the AccountsQuery API is obtained by, val query = Contacts ( context ). accounts (). query ()","title":"Query for Accounts"},{"location":"accounts/query-accounts/#a-basic-query","text":"To get all available accounts in the system, val accounts = Contacts ( context ). accounts (). query () . find () To get all available accounts in the system with an account type of \"com.google\" or \"com.yahoo\", val accounts = Contacts ( context ). accounts (). query () . withTypes ( \"com.google\" , \"com.yahoo\" ) . find () To get the account for a set of RawContacts, val account = Contacts ( context ). accounts (). query () . associatedWith ( rawContacts ) . find () To get all available accounts in the system with an account type of \"com.google\" or \"com.yahoo\" AND is associated with at least one of the given RawContacts, val accounts = Contacts ( context ). accounts (). query () . withTypes ( \"com.google\" , \"com.yahoo\" ) . associatedWith ( rawContacts ) . find () RawContacts that are not associated with an Account are local to the device. For more info, read about Local (device-only) contacts .","title":"A basic query"},{"location":"accounts/query-accounts/#account-for-each-specified-rawcontact","text":"When you perform a query that uses associatedWith without using withTypes , you are able to get the Account for each of the RawContact specified. val rawContactAccount = accounts . accountFor ( rawContact ) This allows you to get the accounts for multiple RawContacts in one API call =)","title":"Account for each specified RawContact"},{"location":"accounts/query-accounts/#cancelling-the-query","text":"To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val contacts = query . find { ! isActive } } }","title":"Cancelling the query"},{"location":"accounts/query-accounts/#performing-the-query-asynchronously","text":"Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the query asynchronously"},{"location":"accounts/query-accounts/#performing-the-query-with-permission","text":"Queries require the android.permission.READ_CONTACTS and android.permission.GET_ACCOUNTS permissions. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the query with permission"},{"location":"accounts/query-accounts/#profile-accounts","text":"The AccountsQuery API also supports querying the Profile (device owner) contact data. To get an instance of this API for Profile queries, val query = Contacts ( context ). accounts (). profile (). query () All queries will be limited to the Profile, whether it exists or not.","title":"Profile accounts"},{"location":"accounts/query-raw-contacts/","text":"Query RawContacts \u00b6 This library provides the AccountsRawContactsQuery API that allows you to get a list of RawContacts matching a specific search criteria. More specifically, this query returns BlankRawContact s, which are RawContacts that contains no data (e.g. email, phone). It only contains critical information required for performing RawContact operations such as associating local RawContacts to an Account. For more info, read Associate local RawContacts to an Account . An instance of the AccountsRawContactsQuery API is obtained by, val query = Contacts ( context ). accounts (). queryRawContacts () A basic query \u00b6 To get all RawContacts as blanks, val rawContacts = Contacts ( context ). accounts (). queryRawContacts (). find () Specifying Accounts \u00b6 To limit the search to only those RawContacts associated with one of the given accounts, . accounts ( accounts ) For example, to limit the search to contacts belonging to only one account, . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) For more info, read Query for Accounts . If no accounts are specified (this function is not called or called with no Accounts), then all RawContacts of Contacts are included in the search. A null Account may be provided here, which results in RawContacts with no associated Account to be included in the search. RawContacts without an associated account are considered local contacts or device-only contacts, which are not synced. For more info, read about Local (device-only) contacts . Note that this may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it. Ordering \u00b6 To order resulting RawContacts using one or more fields, . orderBy ( fieldOrder ) For example, to order RawContacts by account type, . orderBy ( RawContactsFields . AccountType . asc ()) String comparisons ignores case by default. Each orderBys provides ignoreCase as an optional parameter. Use RawContactsFields to construct the orderBys. Limiting and offsetting \u00b6 To limit the amount of RawContacts returned and/or offset (skip) a specified number of RawContacts, . limit ( limit ) . offset ( offset ) For example, to only get a maximum 20 RawContacts, skipping the first 20, . limit ( 20 ) . offset ( 20 ) This is useful for pagination =) Note that it is recommended to limit the number of RawContacts when querying to increase performance and decrease memory cost. Executing the query \u00b6 To execute the query, . find () Cancelling the query \u00b6 To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val rawContacts = query . find { ! isActive } } } Performing the query asynchronously \u00b6 Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes. Performing the query with permission \u00b6 Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) RawContacts from more than one account in the same list \u00b6 When you perform a query that returns groups from more than one account, you will get everything in the same BlankRawContactsList . This list is just like any other List except it also provides an extra function that allows you to get a sublist with RawContacts belonging only to a particular account. val rawContactsFromAccount = blankRawContactsList . from ( account ) Getting Contacts and RawContacts from BlankRawContacts \u00b6 If you want to get the Contacts and all associated RawContacts and Data from a set of BlankRawContact s, val contacts = Contacts ( context ) . query () . where { RawContact . Id `in` blankRawContactIds } . find () For more info, read Query contacts (advanced) . If you need a more convenient way to convert the BlankRawContact s to RawContacts , use BlankRawContactToRawContact extensions. For more info, read Convenience functions . Profile RawContacts \u00b6 The AccountsRawContactsQuery API also supports querying the Profile (device owner) RawContacts. To get an instance of this API for Profile queries, val query = Contacts ( context ). accounts (). profile (). queryRawContacts () All queries will be limited to the Profile, whether it exists or not. Using the where function to specify matching criteria \u00b6 Use the contacts.core.RawContactsField combined with the extensions from contacts.core.Where to form WHERE clauses. This docs page will not provide a tutorial on database where clauses. It assumes that you know the basics. If you don't know the basics, then search for sqlite where clause . For example, to get a list of RawContacts with the given IDs, val favoriteRawContacts = Contacts ( context ) . accounts () . queryRawContacts () . where { Id `in` rawContactIds } . find () Limitations \u00b6 This library only provides basic WHERE functions. It does not cover the entirety of SQLite, though the community may add more over time <3 Furthermore, this library is constrained by rules and limitations set by the Contacts Provider and the behavior of the native Contacts app. One such rule/limitation has resulted in this library not providing WHERE functions such as isNull or isNullOrEmpty to prevent making misleading queries.","title":"Query RawContacts"},{"location":"accounts/query-raw-contacts/#query-rawcontacts","text":"This library provides the AccountsRawContactsQuery API that allows you to get a list of RawContacts matching a specific search criteria. More specifically, this query returns BlankRawContact s, which are RawContacts that contains no data (e.g. email, phone). It only contains critical information required for performing RawContact operations such as associating local RawContacts to an Account. For more info, read Associate local RawContacts to an Account . An instance of the AccountsRawContactsQuery API is obtained by, val query = Contacts ( context ). accounts (). queryRawContacts ()","title":"Query RawContacts"},{"location":"accounts/query-raw-contacts/#a-basic-query","text":"To get all RawContacts as blanks, val rawContacts = Contacts ( context ). accounts (). queryRawContacts (). find ()","title":"A basic query"},{"location":"accounts/query-raw-contacts/#specifying-accounts","text":"To limit the search to only those RawContacts associated with one of the given accounts, . accounts ( accounts ) For example, to limit the search to contacts belonging to only one account, . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) For more info, read Query for Accounts . If no accounts are specified (this function is not called or called with no Accounts), then all RawContacts of Contacts are included in the search. A null Account may be provided here, which results in RawContacts with no associated Account to be included in the search. RawContacts without an associated account are considered local contacts or device-only contacts, which are not synced. For more info, read about Local (device-only) contacts . Note that this may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it.","title":"Specifying Accounts"},{"location":"accounts/query-raw-contacts/#ordering","text":"To order resulting RawContacts using one or more fields, . orderBy ( fieldOrder ) For example, to order RawContacts by account type, . orderBy ( RawContactsFields . AccountType . asc ()) String comparisons ignores case by default. Each orderBys provides ignoreCase as an optional parameter. Use RawContactsFields to construct the orderBys.","title":"Ordering"},{"location":"accounts/query-raw-contacts/#limiting-and-offsetting","text":"To limit the amount of RawContacts returned and/or offset (skip) a specified number of RawContacts, . limit ( limit ) . offset ( offset ) For example, to only get a maximum 20 RawContacts, skipping the first 20, . limit ( 20 ) . offset ( 20 ) This is useful for pagination =) Note that it is recommended to limit the number of RawContacts when querying to increase performance and decrease memory cost.","title":"Limiting and offsetting"},{"location":"accounts/query-raw-contacts/#executing-the-query","text":"To execute the query, . find ()","title":"Executing the query"},{"location":"accounts/query-raw-contacts/#cancelling-the-query","text":"To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val rawContacts = query . find { ! isActive } } }","title":"Cancelling the query"},{"location":"accounts/query-raw-contacts/#performing-the-query-asynchronously","text":"Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes.","title":"Performing the query asynchronously"},{"location":"accounts/query-raw-contacts/#performing-the-query-with-permission","text":"Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the query with permission"},{"location":"accounts/query-raw-contacts/#rawcontacts-from-more-than-one-account-in-the-same-list","text":"When you perform a query that returns groups from more than one account, you will get everything in the same BlankRawContactsList . This list is just like any other List except it also provides an extra function that allows you to get a sublist with RawContacts belonging only to a particular account. val rawContactsFromAccount = blankRawContactsList . from ( account )","title":"RawContacts from more than one account in the same list"},{"location":"accounts/query-raw-contacts/#getting-contacts-and-rawcontacts-from-blankrawcontacts","text":"If you want to get the Contacts and all associated RawContacts and Data from a set of BlankRawContact s, val contacts = Contacts ( context ) . query () . where { RawContact . Id `in` blankRawContactIds } . find () For more info, read Query contacts (advanced) . If you need a more convenient way to convert the BlankRawContact s to RawContacts , use BlankRawContactToRawContact extensions. For more info, read Convenience functions .","title":"Getting Contacts and RawContacts from BlankRawContacts"},{"location":"accounts/query-raw-contacts/#profile-rawcontacts","text":"The AccountsRawContactsQuery API also supports querying the Profile (device owner) RawContacts. To get an instance of this API for Profile queries, val query = Contacts ( context ). accounts (). profile (). queryRawContacts () All queries will be limited to the Profile, whether it exists or not.","title":"Profile RawContacts"},{"location":"accounts/query-raw-contacts/#using-the-where-function-to-specify-matching-criteria","text":"Use the contacts.core.RawContactsField combined with the extensions from contacts.core.Where to form WHERE clauses. This docs page will not provide a tutorial on database where clauses. It assumes that you know the basics. If you don't know the basics, then search for sqlite where clause . For example, to get a list of RawContacts with the given IDs, val favoriteRawContacts = Contacts ( context ) . accounts () . queryRawContacts () . where { Id `in` rawContactIds } . find ()","title":"Using the where function to specify matching criteria"},{"location":"accounts/query-raw-contacts/#limitations","text":"This library only provides basic WHERE functions. It does not cover the entirety of SQLite, though the community may add more over time <3 Furthermore, this library is constrained by rules and limitations set by the Contacts Provider and the behavior of the native Contacts app. One such rule/limitation has resulted in this library not providing WHERE functions such as isNull or isNullOrEmpty to prevent making misleading queries.","title":"Limitations"},{"location":"async/async-execution-coroutines/","text":"Execute work outside of the UI thread using coroutines \u00b6 This library provides extensions in the async module that allow you to execute all core API functions outside of the main, UI thread. These extensions use Kotlin Coroutines . The extension functions are lightweight and mostly exist for Coroutine user's convenience. The extensions can be generalized in two categories; withContext and async . These use, you guessed it, Kotlin Coroutine's withContext and async functions respectively. For all core API functions that does blocking work in the call-site thread (e.g. query, insert, update, and deletes), there is a corresponding xxxWithContext and xxxAsync extension function. Using withContext extensions \u00b6 To perform an query, insert, update, and delete in order (sequential) outside the main UI thread, launch { val queryResult = query . findWithContext () val insertResult = insert . commitWithContext () val updateResult = update . commitWithContext () val deleteResult = delete . commitWithContext () } For each invocation of xxxWithContext , the current coroutine suspends, performs the operation in the given CoroutineContext (default is Dispatchers.IO if not specified), then returns the result. Computations automatically stops if the parent coroutine scope / job is cancelled. Using async extensions \u00b6 To perform an query, insert, update, and delete in parallel outside the main UI thread, launch { val deferredQueryResult = query . findAsynct () val deferredInsertResult = insert . commitAsync () val deferredUpdateResult = update . commitAsync () val deferredDeleteResult = delete . commitAsync () awaitAll ( deferredQueryResult , deferredInsertResult , deferredUpdateResult , deferredDeleteResult ) } For each invocation of xxxAsync , a CoroutineScope is created with the given CoroutineContext (default is Dispatchers.IO if not specified), performs the operation in that scope, then returns the Deferred result. Computations automatically stops if the parent coroutine scope / job is cancelled. Cancellations are supported \u00b6 To cancel a query amid execution, query . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val contacts = query . find { ! isActive } } } Or, using the coroutine extensions in the async module, launch { val contacts = query . findWithContext () } Most core API functions support cancellations, not just queries! Not compatible with Java \u00b6 Unlike the core module, the async module is not compatible with Java because it requires Kotlin Coroutines. These extensions are optional \u00b6 You are free to use the core APIs however you want with whatever libraries or frameworks you want that works with Java such as Reactive, AsyncTask (hope not), WorkManager, or your own DIY solution. Extensions for RxJava and Flow are in the roadmap \u00b6 If you prefer not to use Kotlin Coroutines and would rather use your own multi-threading mechanism, then you are free to use the core module without using the async module functions. However, if you prefer to use something that comes with the library to ensure first-class support, then you might be interested in waiting for extensions for RxJava and Kotlin Flow !","title":"Execute work outside of the UI thread using coroutines"},{"location":"async/async-execution-coroutines/#execute-work-outside-of-the-ui-thread-using-coroutines","text":"This library provides extensions in the async module that allow you to execute all core API functions outside of the main, UI thread. These extensions use Kotlin Coroutines . The extension functions are lightweight and mostly exist for Coroutine user's convenience. The extensions can be generalized in two categories; withContext and async . These use, you guessed it, Kotlin Coroutine's withContext and async functions respectively. For all core API functions that does blocking work in the call-site thread (e.g. query, insert, update, and deletes), there is a corresponding xxxWithContext and xxxAsync extension function.","title":"Execute work outside of the UI thread using coroutines"},{"location":"async/async-execution-coroutines/#using-withcontext-extensions","text":"To perform an query, insert, update, and delete in order (sequential) outside the main UI thread, launch { val queryResult = query . findWithContext () val insertResult = insert . commitWithContext () val updateResult = update . commitWithContext () val deleteResult = delete . commitWithContext () } For each invocation of xxxWithContext , the current coroutine suspends, performs the operation in the given CoroutineContext (default is Dispatchers.IO if not specified), then returns the result. Computations automatically stops if the parent coroutine scope / job is cancelled.","title":"Using withContext extensions"},{"location":"async/async-execution-coroutines/#using-async-extensions","text":"To perform an query, insert, update, and delete in parallel outside the main UI thread, launch { val deferredQueryResult = query . findAsynct () val deferredInsertResult = insert . commitAsync () val deferredUpdateResult = update . commitAsync () val deferredDeleteResult = delete . commitAsync () awaitAll ( deferredQueryResult , deferredInsertResult , deferredUpdateResult , deferredDeleteResult ) } For each invocation of xxxAsync , a CoroutineScope is created with the given CoroutineContext (default is Dispatchers.IO if not specified), performs the operation in that scope, then returns the Deferred result. Computations automatically stops if the parent coroutine scope / job is cancelled.","title":"Using async extensions"},{"location":"async/async-execution-coroutines/#cancellations-are-supported","text":"To cancel a query amid execution, query . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val contacts = query . find { ! isActive } } } Or, using the coroutine extensions in the async module, launch { val contacts = query . findWithContext () } Most core API functions support cancellations, not just queries!","title":"Cancellations are supported"},{"location":"async/async-execution-coroutines/#not-compatible-with-java","text":"Unlike the core module, the async module is not compatible with Java because it requires Kotlin Coroutines.","title":"Not compatible with Java"},{"location":"async/async-execution-coroutines/#these-extensions-are-optional","text":"You are free to use the core APIs however you want with whatever libraries or frameworks you want that works with Java such as Reactive, AsyncTask (hope not), WorkManager, or your own DIY solution.","title":"These extensions are optional"},{"location":"async/async-execution-coroutines/#extensions-for-rxjava-and-flow-are-in-the-roadmap","text":"If you prefer not to use Kotlin Coroutines and would rather use your own multi-threading mechanism, then you are free to use the core module without using the async module functions. However, if you prefer to use something that comes with the library to ensure first-class support, then you might be interested in waiting for extensions for RxJava and Kotlin Flow !","title":"Extensions for RxJava and Flow are in the roadmap"},{"location":"basics/delete-contacts/","text":"Delete Contacts \u00b6 This library provides the Delete API, which allows you to delete one or more Contacts or RawContacts. An instance of the Delete API is obtained by, val delete = Contacts ( context ). delete () If you want to delete the device owner Contact Profile, read Delete device owner Contact profile . If you want to delete a set of Data, read Delete existing sets of data . A basic delete \u00b6 To delete a set of Contact and all of its RawContacts, val deleteResult = delete . contacts ( contactToDelete ) . commit () If you want to delete a set of RawContacts, val deleteResult = delete . rawContacts ( contactToDelete ) . commit () You may specify contacts and rawContacts in the same delete operation. Note that Contacts are deleted automatically when all constituent RawContacts are deleted. Executing the delete \u00b6 To execute the delete, . commit () If you want to delete all given Contacts and RawContacts in a single atomic transaction, . commitInOneTransaction () The call to commitInOneTransaction will only succeed if ALL given Contacts and RawContacts are successfully deleted. If one delete fails, the entire operation will fail and everything will be reverted prior to the delete operation. In contrast, commit allows for some deletes to succeed and some to fail. Handling the delete result \u00b6 The commit and commitInOneTransaction functions returns a Result , To check if all deletes succeeded, val allDeletesSuccessful = deleteResult . isSuccessful To check if a particular delete succeeded, val firstDeleteSuccessful = deleteResult . isSuccessful ( mutableContact1 ) Performing the delete and result processing asynchronously \u00b6 Deletes are executed when the commit or commitInOneTransaction function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the delete with permission \u00b6 Deletes require the android.permission.WRITE_CONTACTS permissions. If not granted, the delete will do nothing and return a failed result. To perform the delete with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Custom data support \u00b6 The Delete API supports custom data. For more info, read Delete custom data . Data belonging to RawContacts/Contact are deleted \u00b6 When a RawContact is deleted, all of its data are also deleted. Contacts are deleted automatically when all constituent RawContacts are deleted \u00b6 Deleting a contact's Contacts row, RawContacts row(s), and associated Data row(s) are best explained in the documentation in ContactsContract.RawContacts ; When a raw contact is deleted, all of its Data rows as well as StatusUpdates, AggregationExceptions, PhoneLookup rows are deleted automatically. When all raw contacts associated with a Contacts row are deleted, the Contacts row itself is also deleted automatically. The invocation of resolver.delete(...), does not immediately delete a raw contacts row. Instead, it sets the ContactsContract.RawContactsColumns.DELETED flag on the raw contact and removes the raw contact from its aggregate contact. The sync adapter then deletes the raw contact from the server and finalizes phone-side deletion by calling resolver.delete(...) again and passing the ContactsContract#CALLER_IS_SYNCADAPTER query parameter. Some sync adapters are read-only, meaning that they only sync server-side changes to the phone, but not the reverse. If one of those raw contacts is marked for deletion, it will remain on the phone. However it will be effectively invisible, because it will not be part of any aggregate contact. TLDR To delete a contacts and all associated rows, simply delete all RawContact rows with the desired Contacts id. Deletion of the Contacts row and associated Data row(s) will be done automatically by the Contacts Provider.","title":"Delete contacts"},{"location":"basics/delete-contacts/#delete-contacts","text":"This library provides the Delete API, which allows you to delete one or more Contacts or RawContacts. An instance of the Delete API is obtained by, val delete = Contacts ( context ). delete () If you want to delete the device owner Contact Profile, read Delete device owner Contact profile . If you want to delete a set of Data, read Delete existing sets of data .","title":"Delete Contacts"},{"location":"basics/delete-contacts/#a-basic-delete","text":"To delete a set of Contact and all of its RawContacts, val deleteResult = delete . contacts ( contactToDelete ) . commit () If you want to delete a set of RawContacts, val deleteResult = delete . rawContacts ( contactToDelete ) . commit () You may specify contacts and rawContacts in the same delete operation. Note that Contacts are deleted automatically when all constituent RawContacts are deleted.","title":"A basic delete"},{"location":"basics/delete-contacts/#executing-the-delete","text":"To execute the delete, . commit () If you want to delete all given Contacts and RawContacts in a single atomic transaction, . commitInOneTransaction () The call to commitInOneTransaction will only succeed if ALL given Contacts and RawContacts are successfully deleted. If one delete fails, the entire operation will fail and everything will be reverted prior to the delete operation. In contrast, commit allows for some deletes to succeed and some to fail.","title":"Executing the delete"},{"location":"basics/delete-contacts/#handling-the-delete-result","text":"The commit and commitInOneTransaction functions returns a Result , To check if all deletes succeeded, val allDeletesSuccessful = deleteResult . isSuccessful To check if a particular delete succeeded, val firstDeleteSuccessful = deleteResult . isSuccessful ( mutableContact1 )","title":"Handling the delete result"},{"location":"basics/delete-contacts/#performing-the-delete-and-result-processing-asynchronously","text":"Deletes are executed when the commit or commitInOneTransaction function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the delete and result processing asynchronously"},{"location":"basics/delete-contacts/#performing-the-delete-with-permission","text":"Deletes require the android.permission.WRITE_CONTACTS permissions. If not granted, the delete will do nothing and return a failed result. To perform the delete with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the delete with permission"},{"location":"basics/delete-contacts/#custom-data-support","text":"The Delete API supports custom data. For more info, read Delete custom data .","title":"Custom data support"},{"location":"basics/delete-contacts/#data-belonging-to-rawcontactscontact-are-deleted","text":"When a RawContact is deleted, all of its data are also deleted.","title":"Data belonging to RawContacts/Contact are deleted"},{"location":"basics/delete-contacts/#contacts-are-deleted-automatically-when-all-constituent-rawcontacts-are-deleted","text":"Deleting a contact's Contacts row, RawContacts row(s), and associated Data row(s) are best explained in the documentation in ContactsContract.RawContacts ; When a raw contact is deleted, all of its Data rows as well as StatusUpdates, AggregationExceptions, PhoneLookup rows are deleted automatically. When all raw contacts associated with a Contacts row are deleted, the Contacts row itself is also deleted automatically. The invocation of resolver.delete(...), does not immediately delete a raw contacts row. Instead, it sets the ContactsContract.RawContactsColumns.DELETED flag on the raw contact and removes the raw contact from its aggregate contact. The sync adapter then deletes the raw contact from the server and finalizes phone-side deletion by calling resolver.delete(...) again and passing the ContactsContract#CALLER_IS_SYNCADAPTER query parameter. Some sync adapters are read-only, meaning that they only sync server-side changes to the phone, but not the reverse. If one of those raw contacts is marked for deletion, it will remain on the phone. However it will be effectively invisible, because it will not be part of any aggregate contact. TLDR To delete a contacts and all associated rows, simply delete all RawContact rows with the desired Contacts id. Deletion of the Contacts row and associated Data row(s) will be done automatically by the Contacts Provider.","title":"Contacts are deleted automatically when all constituent RawContacts are deleted"},{"location":"basics/insert-contacts/","text":"Insert contacts \u00b6 This library provides the Insert API that allows you to insert one or more RawContacts and Data. An instance of the Insert API is obtained by, val insert = Contacts ( context ). insert () If you want to create/insert the device owner Contact Profile, read Insert device owner Contact profile . If you want to insert Data into a new or existing contact, read Insert data into new or existing contacts . A basic insert \u00b6 To create/insert a contact with a name of \"John Doe\" who works at Amazon with a work email of \"john.doe@amazon.com\" (in Kotlin), val insertResult = Contacts ( context ) . insert () . rawContacts ( NewRawContact (). apply { name = NewName (). apply { givenName = \"John\" familyName = \"Doe\" } organization = NewOrganization (). apply { company = \"Amazon\" title = \"Superstar\" } emails . add ( NewEmail (). apply { address = \"john.doe@amazon.com\" type = EmailEntity . Type . WORK }) }) . commit () Or alternatively, in a more Kotlinized style using named arguments, val insertResult = Contacts ( context ) . insert () . rawContacts ( NewRawContact ( name = NewName ( givenName = \"John\" , familyName = \"Doe\" ), organization = NewOrganization ( company = \"Amazon\" , title = \"Superstar\" ), emails = mutableListOf ( NewEmail ( address = \"john.doe@amazon.com\" , type = EmailEntity . Type . WORK )) )) . commit () Or alternatively, using extension functions, val insertResult = Contacts ( context ) . insert () . rawContact { setName { givenName = \"John\" familyName = \"Doe\" } setOrganization { company = \"Amazon\" title = \"Superstar\" } addEmail { address = \"john.doe@amazon.com\" type = EmailEntity . Type . WORK } } . commit () Allowing blanks \u00b6 The API allows you to specify if you want to be able to insert blank contacts or not, . allowBlanks ( true | false ) For more info, read about Blank contacts . Blank data are not inserted \u00b6 Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are ignored and are not inserted by insert APIs. For more info, read about Blank data . Associating an Account \u00b6 New RawContacts can be associated with an Account in order to enable syncing, . forAccount ( account ) For example, to associated the new RawContact to an account, . forAccount ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) For more info, read Query for Accounts . Local RawContacts \u00b6 If no Account is provided, or null is provided, or if an incorrect account is provided, the RawContacts inserted will not be associated with an Account. RawContacts inserted without an associated account are considered local or device-only contacts, which are not synced. For more info, read Sync contact data across devices . There are also certain data kinds that are ignored on insert or update if the RawContact is local. For more info, read about Local (device-only) contacts . Including only specific data \u00b6 To include only the given set of fields (data) in each of the insert operation, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations . Executing the insert \u00b6 To execute the insert, . commit () Handling the insert result \u00b6 The commit function returns a Result , val contactsApi = Contacts ( context ) val newRawContact1 = NewRawContact (...) val newRawContact2 = NewRawContact (...) val insertResult = contactsApi . insert () . rawContacts ( newRawContact1 , newRawContact2 ) . commit () To check if all inserts succeeded, val allInsertsSuccessful = insertResult . isSuccessful To check if a particular insert succeeded, val firstInsertSuccessful = insertResult . isSuccessful ( newRawContact1 ) To get the RawContact IDs of all the newly created RawContacts, val allRawContactIds = insertResult . rawContactIds To get the RawContact ID of a particular RawContact, val secondRawContactId = insertResult . rawContactId ( newRawContact2 ) Once you have the RawContact IDs, you can retrieve the newly created Contacts via the Query API, val contacts = contactsApi . query () . where { RawContact . Id `in` allRawContactIds } . find () For more info, read Query contacts (advanced) . Alternatively, you may use the extensions provided in InsertResult . To get all newly created Contacts, val contacts = insertResult . contacts ( contactsApi ) To get a particular contact, val contact = insertResult . contacts ( contactsApi , newRawContact1 ) To instead get the RawContacts directly, val rawContacts = insertResult . rawContacts ( contactsApi ) To get a particular RawContact, val rawContact = insertResult . rawContact ( contactsApi , newRawContact2 ) Cancelling the insert \u00b6 To cancel an insert amid execution, . commit { returnTrueIfInsertShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel insert processing as soon as possible. The function is called numerous times during insert processing to check if processing should stop or continue. This gives you the option to cancel the insert. For example, to automatically cancel the insert inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val insertResult = insert . commit { ! isActive } } } Performing the insert and result processing asynchronously \u00b6 Inserts are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the insert with permission \u00b6 Inserts require the android.permission.WRITE_CONTACTS and android.permission.GET_ACCOUNTS permissions. If not granted, the insert will do nothing and return a failed result. To perform the insert with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Custom data support \u00b6 The Insert API supports custom data. For more info, read Insert custom data into new or existing contacts . RawContact and Contacts aggregation \u00b6 As per documentation in android.provider.ContactsContract.Contacts , A Contact cannot be created explicitly. When a raw contact is inserted, the provider will first try to find a Contact representing the same person. If one is found, the raw contact's RawContacts#CONTACT_ID column gets the _ID of the aggregate Contact. If no match is found, the provider automatically inserts a new Contact and puts its _ID into the RawContacts#CONTACT_ID column of the newly inserted raw contact. Insert a new RawContact with data of every kind \u00b6 Unless you are allowing blanks, you only need to provide at least one data kind when inserting a new contact in order for the operation to succeed. If you want to provide data of every kind, which is useful when implementing a contact creation screen, val accountToAddContactTo = Account ( \"vestrel00@pixar.com\" , \"com.pixar\" ) val insertResult = Contacts ( context ) . insert () . forAccount () . rawContact { setName { givenName = \"Buzz\" familyName = \"Lightyear\" } setNickname { name = \"Buzz\" } setOrganization { title = \"Space Toy\" company = \"Pixar\" } addPhone { number = \"(555) 555-5555\" type = PhoneEntity . Type . CUSTOM label = \"Fake Number\" } setSipAddress { sipAddress = \"sip:buzz.lightyear@pixar.com\" } addEmail { address = \"buzz.lightyear@pixar.com\" type = EmailEntity . Type . WORK } addEmail { address = \"buzz@lightyear.net\" type = EmailEntity . Type . HOME } addAddress { formattedAddress = \"1200 Park Ave\" type = AddressEntity . Type . WORK } addIm { data = \"buzzlightyear@skype.com\" protocol = ImEntity . Protocol . SKYPE } addWebsite { url = \"https://www.pixar.com\" } addWebsite { url = \"https://www.disney.com\" } addEvent { date = EventDate . from ( year = 1995 , month = 10 , dayOfMonth = 22 ) type = EventEntity . Type . BIRTHDAY } addRelation { name = \"Childhood friend\" type = RelationEntity . Type . CUSTOM label = \"Imaginary Friend\" } groupMemberships . addAll ( contactsApi . groups () . query () . accounts ( accountToAddContactTo ) . where { ( Favorites equalTo true ) or ( Title contains \"friend\" ) } . find () . newMemberships () ) setNote { note = \"The best toy in the world!\" } } . commit () Inserting photos and thumbnails \u00b6 Full-sized photos (and by API design thumbnails) can only be inserted after inserting the contact. For more info, read Get set remove full-sized and thumbnail contact photos .","title":"Insert contacts"},{"location":"basics/insert-contacts/#insert-contacts","text":"This library provides the Insert API that allows you to insert one or more RawContacts and Data. An instance of the Insert API is obtained by, val insert = Contacts ( context ). insert () If you want to create/insert the device owner Contact Profile, read Insert device owner Contact profile . If you want to insert Data into a new or existing contact, read Insert data into new or existing contacts .","title":"Insert contacts"},{"location":"basics/insert-contacts/#a-basic-insert","text":"To create/insert a contact with a name of \"John Doe\" who works at Amazon with a work email of \"john.doe@amazon.com\" (in Kotlin), val insertResult = Contacts ( context ) . insert () . rawContacts ( NewRawContact (). apply { name = NewName (). apply { givenName = \"John\" familyName = \"Doe\" } organization = NewOrganization (). apply { company = \"Amazon\" title = \"Superstar\" } emails . add ( NewEmail (). apply { address = \"john.doe@amazon.com\" type = EmailEntity . Type . WORK }) }) . commit () Or alternatively, in a more Kotlinized style using named arguments, val insertResult = Contacts ( context ) . insert () . rawContacts ( NewRawContact ( name = NewName ( givenName = \"John\" , familyName = \"Doe\" ), organization = NewOrganization ( company = \"Amazon\" , title = \"Superstar\" ), emails = mutableListOf ( NewEmail ( address = \"john.doe@amazon.com\" , type = EmailEntity . Type . WORK )) )) . commit () Or alternatively, using extension functions, val insertResult = Contacts ( context ) . insert () . rawContact { setName { givenName = \"John\" familyName = \"Doe\" } setOrganization { company = \"Amazon\" title = \"Superstar\" } addEmail { address = \"john.doe@amazon.com\" type = EmailEntity . Type . WORK } } . commit ()","title":"A basic insert"},{"location":"basics/insert-contacts/#allowing-blanks","text":"The API allows you to specify if you want to be able to insert blank contacts or not, . allowBlanks ( true | false ) For more info, read about Blank contacts .","title":"Allowing blanks"},{"location":"basics/insert-contacts/#blank-data-are-not-inserted","text":"Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are ignored and are not inserted by insert APIs. For more info, read about Blank data .","title":"Blank data are not inserted"},{"location":"basics/insert-contacts/#associating-an-account","text":"New RawContacts can be associated with an Account in order to enable syncing, . forAccount ( account ) For example, to associated the new RawContact to an account, . forAccount ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) For more info, read Query for Accounts .","title":"Associating an Account"},{"location":"basics/insert-contacts/#local-rawcontacts","text":"If no Account is provided, or null is provided, or if an incorrect account is provided, the RawContacts inserted will not be associated with an Account. RawContacts inserted without an associated account are considered local or device-only contacts, which are not synced. For more info, read Sync contact data across devices . There are also certain data kinds that are ignored on insert or update if the RawContact is local. For more info, read about Local (device-only) contacts .","title":"Local RawContacts"},{"location":"basics/insert-contacts/#including-only-specific-data","text":"To include only the given set of fields (data) in each of the insert operation, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations .","title":"Including only specific data"},{"location":"basics/insert-contacts/#executing-the-insert","text":"To execute the insert, . commit ()","title":"Executing the insert"},{"location":"basics/insert-contacts/#handling-the-insert-result","text":"The commit function returns a Result , val contactsApi = Contacts ( context ) val newRawContact1 = NewRawContact (...) val newRawContact2 = NewRawContact (...) val insertResult = contactsApi . insert () . rawContacts ( newRawContact1 , newRawContact2 ) . commit () To check if all inserts succeeded, val allInsertsSuccessful = insertResult . isSuccessful To check if a particular insert succeeded, val firstInsertSuccessful = insertResult . isSuccessful ( newRawContact1 ) To get the RawContact IDs of all the newly created RawContacts, val allRawContactIds = insertResult . rawContactIds To get the RawContact ID of a particular RawContact, val secondRawContactId = insertResult . rawContactId ( newRawContact2 ) Once you have the RawContact IDs, you can retrieve the newly created Contacts via the Query API, val contacts = contactsApi . query () . where { RawContact . Id `in` allRawContactIds } . find () For more info, read Query contacts (advanced) . Alternatively, you may use the extensions provided in InsertResult . To get all newly created Contacts, val contacts = insertResult . contacts ( contactsApi ) To get a particular contact, val contact = insertResult . contacts ( contactsApi , newRawContact1 ) To instead get the RawContacts directly, val rawContacts = insertResult . rawContacts ( contactsApi ) To get a particular RawContact, val rawContact = insertResult . rawContact ( contactsApi , newRawContact2 )","title":"Handling the insert result"},{"location":"basics/insert-contacts/#cancelling-the-insert","text":"To cancel an insert amid execution, . commit { returnTrueIfInsertShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel insert processing as soon as possible. The function is called numerous times during insert processing to check if processing should stop or continue. This gives you the option to cancel the insert. For example, to automatically cancel the insert inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val insertResult = insert . commit { ! isActive } } }","title":"Cancelling the insert"},{"location":"basics/insert-contacts/#performing-the-insert-and-result-processing-asynchronously","text":"Inserts are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the insert and result processing asynchronously"},{"location":"basics/insert-contacts/#performing-the-insert-with-permission","text":"Inserts require the android.permission.WRITE_CONTACTS and android.permission.GET_ACCOUNTS permissions. If not granted, the insert will do nothing and return a failed result. To perform the insert with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the insert with permission"},{"location":"basics/insert-contacts/#custom-data-support","text":"The Insert API supports custom data. For more info, read Insert custom data into new or existing contacts .","title":"Custom data support"},{"location":"basics/insert-contacts/#rawcontact-and-contacts-aggregation","text":"As per documentation in android.provider.ContactsContract.Contacts , A Contact cannot be created explicitly. When a raw contact is inserted, the provider will first try to find a Contact representing the same person. If one is found, the raw contact's RawContacts#CONTACT_ID column gets the _ID of the aggregate Contact. If no match is found, the provider automatically inserts a new Contact and puts its _ID into the RawContacts#CONTACT_ID column of the newly inserted raw contact.","title":"RawContact and Contacts aggregation"},{"location":"basics/insert-contacts/#insert-a-new-rawcontact-with-data-of-every-kind","text":"Unless you are allowing blanks, you only need to provide at least one data kind when inserting a new contact in order for the operation to succeed. If you want to provide data of every kind, which is useful when implementing a contact creation screen, val accountToAddContactTo = Account ( \"vestrel00@pixar.com\" , \"com.pixar\" ) val insertResult = Contacts ( context ) . insert () . forAccount () . rawContact { setName { givenName = \"Buzz\" familyName = \"Lightyear\" } setNickname { name = \"Buzz\" } setOrganization { title = \"Space Toy\" company = \"Pixar\" } addPhone { number = \"(555) 555-5555\" type = PhoneEntity . Type . CUSTOM label = \"Fake Number\" } setSipAddress { sipAddress = \"sip:buzz.lightyear@pixar.com\" } addEmail { address = \"buzz.lightyear@pixar.com\" type = EmailEntity . Type . WORK } addEmail { address = \"buzz@lightyear.net\" type = EmailEntity . Type . HOME } addAddress { formattedAddress = \"1200 Park Ave\" type = AddressEntity . Type . WORK } addIm { data = \"buzzlightyear@skype.com\" protocol = ImEntity . Protocol . SKYPE } addWebsite { url = \"https://www.pixar.com\" } addWebsite { url = \"https://www.disney.com\" } addEvent { date = EventDate . from ( year = 1995 , month = 10 , dayOfMonth = 22 ) type = EventEntity . Type . BIRTHDAY } addRelation { name = \"Childhood friend\" type = RelationEntity . Type . CUSTOM label = \"Imaginary Friend\" } groupMemberships . addAll ( contactsApi . groups () . query () . accounts ( accountToAddContactTo ) . where { ( Favorites equalTo true ) or ( Title contains \"friend\" ) } . find () . newMemberships () ) setNote { note = \"The best toy in the world!\" } } . commit ()","title":"Insert a new RawContact with data of every kind"},{"location":"basics/insert-contacts/#inserting-photos-and-thumbnails","text":"Full-sized photos (and by API design thumbnails) can only be inserted after inserting the contact. For more info, read Get set remove full-sized and thumbnail contact photos .","title":"Inserting photos and thumbnails"},{"location":"basics/query-contacts-advanced/","text":"Query contacts (advanced) \u00b6 This library provides the Query API that allows you to get a list of Contacts matching a specific search criteria. All RawContacts of matching Contacts are included in the resulting Contact instances. This provides a great deal of granularity and customizations when providing matching criteria via the where function. An instance of the Query API is obtained by, val query = Contacts ( context ). query () For a broader, and more native Contacts app like query, use the BroadQuery API. For more info, read Query contacts . If you want to query Data directly instead of Contacts, read Query specific data kinds . If you want to get the device owner Contact Profile, read Query device owner Contact profile . An advanced query \u00b6 To retrieve the first 5 contacts (including only the contact id, display name, and phone numbers in the results) ordered by display name in descending order, matching ALL of these rules; a first name starting with \"leo\" has emails from gmail or hotmail lives in the US has been born prior to making this query is favorited (starred) has a nickname of \"DarEdEvil\" (case sensitive) works for Facebook has a note belongs to the account of \"john.doe@gmail.com\" or \"john.doe@myspace.com\" val contacts = Contacts ( context ) . query () . where { ( Name . GivenName startsWith \"leo\" ) and ( Email . Address { endsWith ( \"gmail.com\" ) or endsWith ( \"hotmail.com\" ) }) and ( Address . Country equalToIgnoreCase \"us\" ) and ( Event { ( Date lessThan Date (). toWhereString ()) and ( Type equalTo EventEntity . Type . BIRTHDAY ) }) and ( Contact . Options . Starred equalTo true ) and ( Nickname . Name equalTo \"DarEdEvil\" ) and ( Organization . Company `in` listOf ( \"facebook\" , \"FB\" )) and ( Note . Note . isNotNullOrEmpty ()) } . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" ), Account ( \"john.doe@myspace.com\" , \"com.myspace\" ), ) . include { setOf ( Contact . Id , Contact . DisplayNamePrimary , Phone . Number ) } . orderBy ( ContactsFields . DisplayNamePrimary . desc ()) . offset ( 0 ) . limit ( 5 ) . find () A basic query \u00b6 This query API may also be used to make basic, simpler queries. To get all contacts ordered by the primary display name, val contacts = Contacts ( context ) . query () . orderBy ( ContactsFields . DisplayNamePrimary . asc ()) . find () To get all contacts with a phone number AND email, val contacts = Contacts ( context ) . query () ... . where { Phone . Number . isNotNullOrEmpty () and Email . Address . isNotNullOrEmpty () } . find () Note that phone numbers are a special case because the Contacts Provider keeps track of the existence of a phone number for any given contact. Use Contact.HasPhoneNumber equalTo true instead for a more optimized query. To get a list of contacts with the given IDs, val contacts = Contacts ( context ) . query () ... . where { Contact . Id `in` contactIds } . find () To get a Contact by lookup key, read about Contact lookup key vs ID . Including blank contacts \u00b6 The API allows you to specify if you want to include blank contacts or not, . includeBlanks ( true | false ) For more info, read Blank contacts . Specifying Accounts \u00b6 To limit the search to only those contacts associated with one of the given accounts, . accounts ( accounts ) For example, to limit the search to contacts belonging to only one account, . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) For more info, read Query for Accounts . The Contacts returned may still contain RawContacts / data that belongs to other accounts not specified in the given accounts because Contacts may be made up of more than one RawContact from different Accounts. This is the same behavior as the native Contacts app. If no accounts are specified (this function is not called or called with no Accounts), then all RawContacts of Contacts are included in the search. A null Account may be provided here, which results in RawContacts with no associated Account to be included in the search. RawContacts without an associated account are considered local contacts or device-only contacts, which are not synced. For more info, read about Local (device-only) contacts . Note that this may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it. Including only specific data \u00b6 To include only the given set of fields (data) in each of the matching contacts, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations . Specifying Groups \u00b6 To limit the search to only those RawContacts associated with at least one of the given groups, . where { GroupMembership . GroupId `in` groups . mapNotNull { it . id } } For more info, read Query groups . Contacts returned may still contain RawContacts / data that belongs to other groups not specified in the given groups because Contacts may be made up of more than one RawContact from different Groups. This is the same behavior as the native Contacts app. If no groups are specified, then all RawContacts of Contacts are included in the search. Note that this may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it. Ordering \u00b6 To order resulting Contacts using one or more fields, . orderBy ( fieldOrder ) For example, to order contacts by favorite/starred status such that favorite/starred contacts appear first in the list AND order by display name primary in ascending order (from a to z ignoring case), . orderBy ( ContactsFields . Options . Starred . desc (), ContactsFields . DisplayNamePrimary . asc () ) String comparisons ignores case by default. Each orderBys provides ignoreCase as an optional parameter. Use ContactsFields to construct the orderBys. If you need to sort a collection of Contacts outside of a database query using any field (in addition to ContactsFields ), use contacts.core.util.ContactsComparator . For more info, read Convenience functions . Limiting and offsetting \u00b6 To limit the amount of contacts returned and/or offset (skip) a specified number of contacts, . limit ( limit ) . offset ( offset ) For example, to only get a maximum 20 contacts, skipping the first 20, . limit ( 20 ) . offset ( 20 ) This is useful for pagination =) Note that it is recommended to limit the number of contacts when querying to increase performance and decrease memory cost. Executing the query \u00b6 To execute the query, . find () Cancelling the query \u00b6 To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val contacts = query . find { ! isActive } } } Performing the query asynchronously \u00b6 Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes. Performing the query with permission \u00b6 Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Custom data support \u00b6 The Query API supports custom data. For more info, read Query custom data . Using the where function to specify matching criteria \u00b6 Use the contacts.core.Fields combined with the extensions from contacts.core.Where to form WHERE clauses. This docs page will not provide a tutorial on database where clauses. It assumes that you know the basics. If you don't know the basics, then search for sqlite where clause . For example, to get all contacts with a phone number AND email, val contacts = Contacts ( context ) . query () ... . where { Phone . Number . isNotNullOrEmpty () and Email . Address . isNotNullOrEmpty () } . find () To get a list of contacts with the given IDs, val contacts = Contacts ( context ) . query () ... . where { Contact . Id `in` contactIds } . find () Performance \u00b6 Using where may require one or more additional queries, internally performed by the API, which increases the time it takes for the query to complete. Therefore, you should only use where if you actually need it. For every usage of the and operator where the left-hand-side and right-hand-side are different data kinds, an internal database query is performed. This is due to the way the Data table is structured in relation to Contacts. For example, Email . Address . isNotNull () and Phone . Number . isNotNull () and Address . FormattedAddress . isNotNull () The above will require two additional internal database queries in order to simplify the query such that it can actually provide matching Contacts. Using the or operator does not have this performance hit. Limitations \u00b6 This library only provides basic WHERE functions. It does not cover the entirety of SQLite, though the community may add more over time <3 Furthermore, this library is constrained by rules and limitations set by the Contacts Provider and the behavior of the native Contacts app. One such rule/limitation has resulted in this library not providing WHERE functions such as isNull or isNullOrEmpty to prevent making misleading queries. Removing a piece of existing data results in the deletion of the row in the Data table if that row no longer contains any meaningful data. This is the behavior of the native Contacts app. Therefore, querying for null fields is not possible. For example, there may be no Data rows that exist where the email address is null. Thus, a query to search for all contacts with no email addresses may return 0 contacts even if there are some contacts that do not have at least one email address. If you want to match contacts that has no particular type of data, you will have to make two queries. One to get contacts that have that particular type of data and another to get contacts that were not part of the first query results. For example, val contactsWithEmails = query . include ( Fields . Contact . Id ) . where { Email . Address . isNotNullOrEmpty () } . find () val contactIdsWithEmails = contactsWithEmails . mapNotNull { it . id } val contactsWithoutEmails = query . where { Contact . Id notIn contactIdsWithEmails } . find () There is a special case with phone numbers. The ContactsContract provides a field that is true if the contact has at least one phone number; Fields.Contact.HasPhoneNumber . The phone number is the only kind of data that the ContactsContract provides with an indexed value such as this. The ContactsContract does NOT provide things like \"hasEmail\", \"hasWebsite\", etc. Regardless, this library provide functions to match contacts that \"has at least one instance of a kind of data\". The HasPhoneNumber field is not necessary to get contacts that have a phone number. However, this does provide an easy way to get contacts that have no phone numbers without having to make two queries. For example, val contactsWithNoPhoneNumbers = query . where { Contact . HasPhoneNumber notEqualTo true } . find () Blank Contacts and the where function \u00b6 The where function is only used to query the Data table. Some contacts do not have any Data table rows. However, this library exposes some fields that belong to other tables, accessible via the Data table with joins; Fields.Contact Fields.RawContact Using these fields in the where clause does not have any effect in matching blank Contacts or blank RawContacts simply because they have no Data rows containing these joined fields. For more info, read about Blank contacts .","title":"Query contacts (advanced)"},{"location":"basics/query-contacts-advanced/#query-contacts-advanced","text":"This library provides the Query API that allows you to get a list of Contacts matching a specific search criteria. All RawContacts of matching Contacts are included in the resulting Contact instances. This provides a great deal of granularity and customizations when providing matching criteria via the where function. An instance of the Query API is obtained by, val query = Contacts ( context ). query () For a broader, and more native Contacts app like query, use the BroadQuery API. For more info, read Query contacts . If you want to query Data directly instead of Contacts, read Query specific data kinds . If you want to get the device owner Contact Profile, read Query device owner Contact profile .","title":"Query contacts (advanced)"},{"location":"basics/query-contacts-advanced/#an-advanced-query","text":"To retrieve the first 5 contacts (including only the contact id, display name, and phone numbers in the results) ordered by display name in descending order, matching ALL of these rules; a first name starting with \"leo\" has emails from gmail or hotmail lives in the US has been born prior to making this query is favorited (starred) has a nickname of \"DarEdEvil\" (case sensitive) works for Facebook has a note belongs to the account of \"john.doe@gmail.com\" or \"john.doe@myspace.com\" val contacts = Contacts ( context ) . query () . where { ( Name . GivenName startsWith \"leo\" ) and ( Email . Address { endsWith ( \"gmail.com\" ) or endsWith ( \"hotmail.com\" ) }) and ( Address . Country equalToIgnoreCase \"us\" ) and ( Event { ( Date lessThan Date (). toWhereString ()) and ( Type equalTo EventEntity . Type . BIRTHDAY ) }) and ( Contact . Options . Starred equalTo true ) and ( Nickname . Name equalTo \"DarEdEvil\" ) and ( Organization . Company `in` listOf ( \"facebook\" , \"FB\" )) and ( Note . Note . isNotNullOrEmpty ()) } . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" ), Account ( \"john.doe@myspace.com\" , \"com.myspace\" ), ) . include { setOf ( Contact . Id , Contact . DisplayNamePrimary , Phone . Number ) } . orderBy ( ContactsFields . DisplayNamePrimary . desc ()) . offset ( 0 ) . limit ( 5 ) . find ()","title":"An advanced query"},{"location":"basics/query-contacts-advanced/#a-basic-query","text":"This query API may also be used to make basic, simpler queries. To get all contacts ordered by the primary display name, val contacts = Contacts ( context ) . query () . orderBy ( ContactsFields . DisplayNamePrimary . asc ()) . find () To get all contacts with a phone number AND email, val contacts = Contacts ( context ) . query () ... . where { Phone . Number . isNotNullOrEmpty () and Email . Address . isNotNullOrEmpty () } . find () Note that phone numbers are a special case because the Contacts Provider keeps track of the existence of a phone number for any given contact. Use Contact.HasPhoneNumber equalTo true instead for a more optimized query. To get a list of contacts with the given IDs, val contacts = Contacts ( context ) . query () ... . where { Contact . Id `in` contactIds } . find () To get a Contact by lookup key, read about Contact lookup key vs ID .","title":"A basic query"},{"location":"basics/query-contacts-advanced/#including-blank-contacts","text":"The API allows you to specify if you want to include blank contacts or not, . includeBlanks ( true | false ) For more info, read Blank contacts .","title":"Including blank contacts"},{"location":"basics/query-contacts-advanced/#specifying-accounts","text":"To limit the search to only those contacts associated with one of the given accounts, . accounts ( accounts ) For example, to limit the search to contacts belonging to only one account, . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) For more info, read Query for Accounts . The Contacts returned may still contain RawContacts / data that belongs to other accounts not specified in the given accounts because Contacts may be made up of more than one RawContact from different Accounts. This is the same behavior as the native Contacts app. If no accounts are specified (this function is not called or called with no Accounts), then all RawContacts of Contacts are included in the search. A null Account may be provided here, which results in RawContacts with no associated Account to be included in the search. RawContacts without an associated account are considered local contacts or device-only contacts, which are not synced. For more info, read about Local (device-only) contacts . Note that this may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it.","title":"Specifying Accounts"},{"location":"basics/query-contacts-advanced/#including-only-specific-data","text":"To include only the given set of fields (data) in each of the matching contacts, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations .","title":"Including only specific data"},{"location":"basics/query-contacts-advanced/#specifying-groups","text":"To limit the search to only those RawContacts associated with at least one of the given groups, . where { GroupMembership . GroupId `in` groups . mapNotNull { it . id } } For more info, read Query groups . Contacts returned may still contain RawContacts / data that belongs to other groups not specified in the given groups because Contacts may be made up of more than one RawContact from different Groups. This is the same behavior as the native Contacts app. If no groups are specified, then all RawContacts of Contacts are included in the search. Note that this may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it.","title":"Specifying Groups"},{"location":"basics/query-contacts-advanced/#ordering","text":"To order resulting Contacts using one or more fields, . orderBy ( fieldOrder ) For example, to order contacts by favorite/starred status such that favorite/starred contacts appear first in the list AND order by display name primary in ascending order (from a to z ignoring case), . orderBy ( ContactsFields . Options . Starred . desc (), ContactsFields . DisplayNamePrimary . asc () ) String comparisons ignores case by default. Each orderBys provides ignoreCase as an optional parameter. Use ContactsFields to construct the orderBys. If you need to sort a collection of Contacts outside of a database query using any field (in addition to ContactsFields ), use contacts.core.util.ContactsComparator . For more info, read Convenience functions .","title":"Ordering"},{"location":"basics/query-contacts-advanced/#limiting-and-offsetting","text":"To limit the amount of contacts returned and/or offset (skip) a specified number of contacts, . limit ( limit ) . offset ( offset ) For example, to only get a maximum 20 contacts, skipping the first 20, . limit ( 20 ) . offset ( 20 ) This is useful for pagination =) Note that it is recommended to limit the number of contacts when querying to increase performance and decrease memory cost.","title":"Limiting and offsetting"},{"location":"basics/query-contacts-advanced/#executing-the-query","text":"To execute the query, . find ()","title":"Executing the query"},{"location":"basics/query-contacts-advanced/#cancelling-the-query","text":"To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val contacts = query . find { ! isActive } } }","title":"Cancelling the query"},{"location":"basics/query-contacts-advanced/#performing-the-query-asynchronously","text":"Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes.","title":"Performing the query asynchronously"},{"location":"basics/query-contacts-advanced/#performing-the-query-with-permission","text":"Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the query with permission"},{"location":"basics/query-contacts-advanced/#custom-data-support","text":"The Query API supports custom data. For more info, read Query custom data .","title":"Custom data support"},{"location":"basics/query-contacts-advanced/#using-the-where-function-to-specify-matching-criteria","text":"Use the contacts.core.Fields combined with the extensions from contacts.core.Where to form WHERE clauses. This docs page will not provide a tutorial on database where clauses. It assumes that you know the basics. If you don't know the basics, then search for sqlite where clause . For example, to get all contacts with a phone number AND email, val contacts = Contacts ( context ) . query () ... . where { Phone . Number . isNotNullOrEmpty () and Email . Address . isNotNullOrEmpty () } . find () To get a list of contacts with the given IDs, val contacts = Contacts ( context ) . query () ... . where { Contact . Id `in` contactIds } . find ()","title":"Using the where function to specify matching criteria"},{"location":"basics/query-contacts-advanced/#performance","text":"Using where may require one or more additional queries, internally performed by the API, which increases the time it takes for the query to complete. Therefore, you should only use where if you actually need it. For every usage of the and operator where the left-hand-side and right-hand-side are different data kinds, an internal database query is performed. This is due to the way the Data table is structured in relation to Contacts. For example, Email . Address . isNotNull () and Phone . Number . isNotNull () and Address . FormattedAddress . isNotNull () The above will require two additional internal database queries in order to simplify the query such that it can actually provide matching Contacts. Using the or operator does not have this performance hit.","title":"Performance"},{"location":"basics/query-contacts-advanced/#limitations","text":"This library only provides basic WHERE functions. It does not cover the entirety of SQLite, though the community may add more over time <3 Furthermore, this library is constrained by rules and limitations set by the Contacts Provider and the behavior of the native Contacts app. One such rule/limitation has resulted in this library not providing WHERE functions such as isNull or isNullOrEmpty to prevent making misleading queries. Removing a piece of existing data results in the deletion of the row in the Data table if that row no longer contains any meaningful data. This is the behavior of the native Contacts app. Therefore, querying for null fields is not possible. For example, there may be no Data rows that exist where the email address is null. Thus, a query to search for all contacts with no email addresses may return 0 contacts even if there are some contacts that do not have at least one email address. If you want to match contacts that has no particular type of data, you will have to make two queries. One to get contacts that have that particular type of data and another to get contacts that were not part of the first query results. For example, val contactsWithEmails = query . include ( Fields . Contact . Id ) . where { Email . Address . isNotNullOrEmpty () } . find () val contactIdsWithEmails = contactsWithEmails . mapNotNull { it . id } val contactsWithoutEmails = query . where { Contact . Id notIn contactIdsWithEmails } . find () There is a special case with phone numbers. The ContactsContract provides a field that is true if the contact has at least one phone number; Fields.Contact.HasPhoneNumber . The phone number is the only kind of data that the ContactsContract provides with an indexed value such as this. The ContactsContract does NOT provide things like \"hasEmail\", \"hasWebsite\", etc. Regardless, this library provide functions to match contacts that \"has at least one instance of a kind of data\". The HasPhoneNumber field is not necessary to get contacts that have a phone number. However, this does provide an easy way to get contacts that have no phone numbers without having to make two queries. For example, val contactsWithNoPhoneNumbers = query . where { Contact . HasPhoneNumber notEqualTo true } . find ()","title":"Limitations"},{"location":"basics/query-contacts-advanced/#blank-contacts-and-the-where-function","text":"The where function is only used to query the Data table. Some contacts do not have any Data table rows. However, this library exposes some fields that belong to other tables, accessible via the Data table with joins; Fields.Contact Fields.RawContact Using these fields in the where clause does not have any effect in matching blank Contacts or blank RawContacts simply because they have no Data rows containing these joined fields. For more info, read about Blank contacts .","title":"Blank Contacts and the where function"},{"location":"basics/query-contacts/","text":"Query contacts \u00b6 This library provides the BroadQuery API that allows you to get the exact same search results as the native Contacts app! This query lets the Contacts Provider perform the search using its own custom matching algorithm via the wherePartiallyMatches function. This type of query is the basis of an app that does a broad search of the Contacts Provider. The technique is useful for apps that want to implement functionality similar to the People app's contact list screen. An instance of the BroadQuery API is obtained by, val query = Contacts ( context ). broadQuery () For a more granular, advanced queries, use the Query API. For more info, read Query contacts (advanced) . If you want to query Data directly instead of Contacts, read Query specific data kinds . If you want to get the device owner Contact Profile, read Query device owner Contact profile . A basic query \u00b6 To get all contacts ordered by the primary display name, val contacts = Contacts ( context ) . broadQuery () . orderBy ( ContactsFields . DisplayNamePrimary . asc ()) . find () To get all contacts that have any data (e.g. name, email, phone, address, organization, note, etc) that at least partially matches a given searchText , val contacts = Contacts ( context ) . broadQuery () . wherePartiallyMatches ( searchText ) . find () Including blank contacts \u00b6 The API allows you to specify if you want to include blank contacts or not, . includeBlanks ( true | false ) For more info, read about Blank contacts . Specifying Accounts \u00b6 To limit the search to only those contacts associated with one of the given accounts, . accounts ( accounts ) For example, to limit the search to contacts belonging to only one account, . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) For more info, read Query for Accounts . The Contacts returned may still contain RawContacts / data that belongs to other accounts not specified in the given accounts because Contacts may be made up of more than one RawContact from different Accounts. This is the same behavior as the native Contacts app. If no accounts are specified (this function is not called or called with no Accounts), then all RawContacts of Contacts are included in the search. A null Account may be provided here, which results in RawContacts with no associated Account to be included in the search. RawContacts without an associated account are considered local contacts or device-only contacts, which are not synced. For more info, read about Local (device-only) contacts . Note that this may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it. Specifying Groups \u00b6 To limit the search to only those RawContacts associated with at least one of the given groups, . groups ( groups ) For example, to limit the search to only favorites, . groups ( favoritesGroup ) For more info, read Query groups . Contacts returned may still contain RawContacts / data that belongs to other groups not specified in the given groups because Contacts may be made up of more than one RawContact from different Groups. This is the same behavior as the native Contacts app. If no groups are specified (this function is not called or called with no Groups), then all RawContacts of Contacts are included in the search. Note that this may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it. Including only specific data \u00b6 To include only the given set of fields (data) in each of the matching contacts, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations . Ordering \u00b6 To order resulting Contacts using one or more fields, . orderBy ( fieldOrder ) For example, to order contacts by favorite/starred status such that favorite/starred contacts appear first in the list AND order by display name primary in ascending order (from a to z ignoring case), . orderBy ( ContactsFields . Options . Starred . desc (), ContactsFields . DisplayNamePrimary . asc () ) String comparisons ignores case by default. Each orderBys provides ignoreCase as an optional parameter. Use ContactsFields to construct the orderBys. If you need to sort a collection of Contacts outside of a database query using any field (in addition to ContactsFields ), use contacts.core.util.ContactsComparator . For more info, read Convenience functions . Limiting and offsetting \u00b6 To limit the amount of contacts returned and/or offset (skip) a specified number of contacts, . limit ( limit ) . offset ( offset ) For example, to only get a maximum 20 contacts, skipping the first 20, . limit ( 20 ) . offset ( 20 ) This is useful for pagination =) Note that it is recommended to limit the number of contacts when querying to increase performance and decrease memory cost. Executing the query \u00b6 To execute the query, . find () Cancelling the query \u00b6 To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val contacts = query . find { ! isActive } } } Performing the query asynchronously \u00b6 Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes. Performing the query with permission \u00b6 Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Custom data support \u00b6 The BroadQuery API does not include custom data in the matching process. However, you may still use the include function with custom data. For more info, read Query custom data . Using the match and wherePartiallyMatches functions to specify matching criteria \u00b6 The BroadQuery API lets the Contacts Provider perform the search using its own custom matching algorithm via the wherePartiallyMatches function. There are several different types of matching algorithms that can be used. The type is set via the match function. Matching is case-insensitive (case is ignored). Custom data are not included in the matching process! To match custom data, use Query . Match.ANY \u00b6 Most, but not all, Contact data are included in the matching process. Some are not probably because some data may result in unintentional matching. Any contact data is included in the matching process. This is the default. Use this if you want to get the same results when searching contacts using the AOSP Contacts app and the Google Contacts app. Most, but not all, contact data are included in the matching process. E.G. name, email, phone, address, organization, note, etc. Data matching is more sophisticated under the hood than Query . The Contacts Provider matches parts of several types of data in segments. For example, a Contact having the email \"hologram@gram.net\" will be matched with the following texts; h HOLO @g @gram.net gram@ net holo.net hologram.net But will NOT be matched with the following texts; olo @ gram@gram am@gram.net Similarly, a Contact having the name \"Zack Air\" will be matched with the following texts; z zack zack, air air, zack za a , z , a ,a But will NOT be matched with the following texts; ack ir , Another example is a Contact having the note \"Lots of spa ces.\" will be matched with the following texts; l lots lots of of lots ces spa lots of. lo o sp ce . . . . . But will NOT be matched with the following texts; . ots Several types of data are matched in segments. E.G. A Contact with display name \"Bell Zee\" and phone numbers \"987\", \"1 23\", and \"456\" will be matched with \"be bell ze 9 123 1 98 456\". Match.PHONE \u00b6 Only phones or (contact display name + any phones) are included in the matching process. Use this if you want to get contacts that have a matching phone number or matching ( Contact.displayNamePrimary + any phone number). If you are attempting to matching contacts with phone numbers using Query , then you will most likely find it to difficult and tricky because the normalizedNumber could be null and matching formatted numbers (e.g. (718) 737-1991) would require some special regular expressions. This match might just be what you need =) Only the Contact.displayNamePrimary and the phone number/normalizedNumber are included in the matching process. For example, a contact with Contact.displayNamePrimary of \"Bob Dole\" and phone number \"(718) 737-1991\" (regardless of the value of normalizedNumber) will be matched with the following texts; 718 7187371991 7.1-8.7-3.7-19(91) bob dole Notice that \"bob\" and \"dole\" will trigger a match because the display name matches and the contact has a phone number. The following texts will NOT trigger a match because the comparison begins at the beginning of the string and not in the middle or end; 737 1991 Match.EMAIL \u00b6 Only emails or (contact display name + any emails) are included in the matching process. Only the Contact.displayNamePrimary and the email address are included in the matching process. For example, the search text \"bob\" will match the following contacts; Robert Parr (bob@incredibles.com) Bob Parr (incredible@android.com) Notice that the contact Bob Parr is also matched because the display name matches and an email exist (even though it does not match). The following search texts will NOT trigger a match because the comparison begins at the beginning of the string and not in the middle or end; android gmail @ .com Developer notes (or for advanced users) \u00b6 The following is taken from issue #197 Matching only by phone number or email address is possible thanks to the following filter Uris defined in ContactsContract , which exist for this specific purpose. ContactsContract { Contacts { CONTENT_FILTER_URI } // Default used by BroadQuery CommonDataKinds { Phone { CONTENT_FILTER_URI } Email { CONTENT_FILTER_URI } } } These special filter URIs are only available for the phone and email common data kinds. Note that the EMAIL and PHONE additionally matches the contact display name. Comparison table \u00b6 I've done some preliminary testing on the differences between the different matching/filter algorithms. So, given the following contacts... Display name: Robert Parr Email: bob@incredibles.com Display name: Bob Parr Email: incredible@android.com Display name: Bob Dole Phone: (718) 737-1991 Display name: vestrel00@gmail.com Email: vestrel00@gmail.com Display name: 646-123-4567 Phone: 646-123-4567 Display name: Secret agent. Address: Dole street Company: 718 Note: Agent code is 646000. His skills are incredible! Here are some search terms followed by matching contacts based on the type of Match used. Search term ANY PHONE EMAIL bob 1, 2, 3 3 1, 2 incredible 1, 2, 6 2 android 2 gmail 4 .com 1, 2, 4 @ 7187371991 3 3 7.1-8.7-3.7-19(91) 3 3 646 5, 6 5 646-646 6 718 3, 6 3 1991 4567 000 dole 3, 6 3 The above table gives us some insight on how sophisticated the matching (or search / indexing) algorithm is. For the search term \"bob\", PHONE matches contact 3. Display name matches and contact has a phone even though it does match. EMAIL matches contact 1, 2. 1 has a matching email \"bob\". 2 is also matched because the name matches even though the email does not. On the other hand, 3 is NOT matched even though the name matches because 3 has no email. Adding an email to 3 will cause 3 to be matched. For the search term \"incredible\", EMAIL matches 2 (incredible@android.com) but NOT 1 (bob@incredibles.com). This means that email matching does not use contains but rather a form of startsWith . TLDR ANY matches any contact data; name, email, phone, address, organization, note, etc. EMAIL matches emails or (display name + any email) PHONE matches phones or (display name + any phone) EMAIL and PHONE matching is NOT as simple as using the Query API with .where { [Email|Phone] contains searchTerm }","title":"Query contacts"},{"location":"basics/query-contacts/#query-contacts","text":"This library provides the BroadQuery API that allows you to get the exact same search results as the native Contacts app! This query lets the Contacts Provider perform the search using its own custom matching algorithm via the wherePartiallyMatches function. This type of query is the basis of an app that does a broad search of the Contacts Provider. The technique is useful for apps that want to implement functionality similar to the People app's contact list screen. An instance of the BroadQuery API is obtained by, val query = Contacts ( context ). broadQuery () For a more granular, advanced queries, use the Query API. For more info, read Query contacts (advanced) . If you want to query Data directly instead of Contacts, read Query specific data kinds . If you want to get the device owner Contact Profile, read Query device owner Contact profile .","title":"Query contacts"},{"location":"basics/query-contacts/#a-basic-query","text":"To get all contacts ordered by the primary display name, val contacts = Contacts ( context ) . broadQuery () . orderBy ( ContactsFields . DisplayNamePrimary . asc ()) . find () To get all contacts that have any data (e.g. name, email, phone, address, organization, note, etc) that at least partially matches a given searchText , val contacts = Contacts ( context ) . broadQuery () . wherePartiallyMatches ( searchText ) . find ()","title":"A basic query"},{"location":"basics/query-contacts/#including-blank-contacts","text":"The API allows you to specify if you want to include blank contacts or not, . includeBlanks ( true | false ) For more info, read about Blank contacts .","title":"Including blank contacts"},{"location":"basics/query-contacts/#specifying-accounts","text":"To limit the search to only those contacts associated with one of the given accounts, . accounts ( accounts ) For example, to limit the search to contacts belonging to only one account, . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) For more info, read Query for Accounts . The Contacts returned may still contain RawContacts / data that belongs to other accounts not specified in the given accounts because Contacts may be made up of more than one RawContact from different Accounts. This is the same behavior as the native Contacts app. If no accounts are specified (this function is not called or called with no Accounts), then all RawContacts of Contacts are included in the search. A null Account may be provided here, which results in RawContacts with no associated Account to be included in the search. RawContacts without an associated account are considered local contacts or device-only contacts, which are not synced. For more info, read about Local (device-only) contacts . Note that this may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it.","title":"Specifying Accounts"},{"location":"basics/query-contacts/#specifying-groups","text":"To limit the search to only those RawContacts associated with at least one of the given groups, . groups ( groups ) For example, to limit the search to only favorites, . groups ( favoritesGroup ) For more info, read Query groups . Contacts returned may still contain RawContacts / data that belongs to other groups not specified in the given groups because Contacts may be made up of more than one RawContact from different Groups. This is the same behavior as the native Contacts app. If no groups are specified (this function is not called or called with no Groups), then all RawContacts of Contacts are included in the search. Note that this may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it.","title":"Specifying Groups"},{"location":"basics/query-contacts/#including-only-specific-data","text":"To include only the given set of fields (data) in each of the matching contacts, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations .","title":"Including only specific data"},{"location":"basics/query-contacts/#ordering","text":"To order resulting Contacts using one or more fields, . orderBy ( fieldOrder ) For example, to order contacts by favorite/starred status such that favorite/starred contacts appear first in the list AND order by display name primary in ascending order (from a to z ignoring case), . orderBy ( ContactsFields . Options . Starred . desc (), ContactsFields . DisplayNamePrimary . asc () ) String comparisons ignores case by default. Each orderBys provides ignoreCase as an optional parameter. Use ContactsFields to construct the orderBys. If you need to sort a collection of Contacts outside of a database query using any field (in addition to ContactsFields ), use contacts.core.util.ContactsComparator . For more info, read Convenience functions .","title":"Ordering"},{"location":"basics/query-contacts/#limiting-and-offsetting","text":"To limit the amount of contacts returned and/or offset (skip) a specified number of contacts, . limit ( limit ) . offset ( offset ) For example, to only get a maximum 20 contacts, skipping the first 20, . limit ( 20 ) . offset ( 20 ) This is useful for pagination =) Note that it is recommended to limit the number of contacts when querying to increase performance and decrease memory cost.","title":"Limiting and offsetting"},{"location":"basics/query-contacts/#executing-the-query","text":"To execute the query, . find ()","title":"Executing the query"},{"location":"basics/query-contacts/#cancelling-the-query","text":"To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val contacts = query . find { ! isActive } } }","title":"Cancelling the query"},{"location":"basics/query-contacts/#performing-the-query-asynchronously","text":"Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes.","title":"Performing the query asynchronously"},{"location":"basics/query-contacts/#performing-the-query-with-permission","text":"Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the query with permission"},{"location":"basics/query-contacts/#custom-data-support","text":"The BroadQuery API does not include custom data in the matching process. However, you may still use the include function with custom data. For more info, read Query custom data .","title":"Custom data support"},{"location":"basics/query-contacts/#using-the-match-and-wherepartiallymatches-functions-to-specify-matching-criteria","text":"The BroadQuery API lets the Contacts Provider perform the search using its own custom matching algorithm via the wherePartiallyMatches function. There are several different types of matching algorithms that can be used. The type is set via the match function. Matching is case-insensitive (case is ignored). Custom data are not included in the matching process! To match custom data, use Query .","title":"Using the match and wherePartiallyMatches functions to specify matching criteria"},{"location":"basics/query-contacts/#matchany","text":"Most, but not all, Contact data are included in the matching process. Some are not probably because some data may result in unintentional matching. Any contact data is included in the matching process. This is the default. Use this if you want to get the same results when searching contacts using the AOSP Contacts app and the Google Contacts app. Most, but not all, contact data are included in the matching process. E.G. name, email, phone, address, organization, note, etc. Data matching is more sophisticated under the hood than Query . The Contacts Provider matches parts of several types of data in segments. For example, a Contact having the email \"hologram@gram.net\" will be matched with the following texts; h HOLO @g @gram.net gram@ net holo.net hologram.net But will NOT be matched with the following texts; olo @ gram@gram am@gram.net Similarly, a Contact having the name \"Zack Air\" will be matched with the following texts; z zack zack, air air, zack za a , z , a ,a But will NOT be matched with the following texts; ack ir , Another example is a Contact having the note \"Lots of spa ces.\" will be matched with the following texts; l lots lots of of lots ces spa lots of. lo o sp ce . . . . . But will NOT be matched with the following texts; . ots Several types of data are matched in segments. E.G. A Contact with display name \"Bell Zee\" and phone numbers \"987\", \"1 23\", and \"456\" will be matched with \"be bell ze 9 123 1 98 456\".","title":"Match.ANY"},{"location":"basics/query-contacts/#matchphone","text":"Only phones or (contact display name + any phones) are included in the matching process. Use this if you want to get contacts that have a matching phone number or matching ( Contact.displayNamePrimary + any phone number). If you are attempting to matching contacts with phone numbers using Query , then you will most likely find it to difficult and tricky because the normalizedNumber could be null and matching formatted numbers (e.g. (718) 737-1991) would require some special regular expressions. This match might just be what you need =) Only the Contact.displayNamePrimary and the phone number/normalizedNumber are included in the matching process. For example, a contact with Contact.displayNamePrimary of \"Bob Dole\" and phone number \"(718) 737-1991\" (regardless of the value of normalizedNumber) will be matched with the following texts; 718 7187371991 7.1-8.7-3.7-19(91) bob dole Notice that \"bob\" and \"dole\" will trigger a match because the display name matches and the contact has a phone number. The following texts will NOT trigger a match because the comparison begins at the beginning of the string and not in the middle or end; 737 1991","title":"Match.PHONE"},{"location":"basics/query-contacts/#matchemail","text":"Only emails or (contact display name + any emails) are included in the matching process. Only the Contact.displayNamePrimary and the email address are included in the matching process. For example, the search text \"bob\" will match the following contacts; Robert Parr (bob@incredibles.com) Bob Parr (incredible@android.com) Notice that the contact Bob Parr is also matched because the display name matches and an email exist (even though it does not match). The following search texts will NOT trigger a match because the comparison begins at the beginning of the string and not in the middle or end; android gmail @ .com","title":"Match.EMAIL"},{"location":"basics/query-contacts/#developer-notes-or-for-advanced-users","text":"The following is taken from issue #197 Matching only by phone number or email address is possible thanks to the following filter Uris defined in ContactsContract , which exist for this specific purpose. ContactsContract { Contacts { CONTENT_FILTER_URI } // Default used by BroadQuery CommonDataKinds { Phone { CONTENT_FILTER_URI } Email { CONTENT_FILTER_URI } } } These special filter URIs are only available for the phone and email common data kinds. Note that the EMAIL and PHONE additionally matches the contact display name.","title":"Developer notes (or for advanced users)"},{"location":"basics/query-contacts/#comparison-table","text":"I've done some preliminary testing on the differences between the different matching/filter algorithms. So, given the following contacts... Display name: Robert Parr Email: bob@incredibles.com Display name: Bob Parr Email: incredible@android.com Display name: Bob Dole Phone: (718) 737-1991 Display name: vestrel00@gmail.com Email: vestrel00@gmail.com Display name: 646-123-4567 Phone: 646-123-4567 Display name: Secret agent. Address: Dole street Company: 718 Note: Agent code is 646000. His skills are incredible! Here are some search terms followed by matching contacts based on the type of Match used. Search term ANY PHONE EMAIL bob 1, 2, 3 3 1, 2 incredible 1, 2, 6 2 android 2 gmail 4 .com 1, 2, 4 @ 7187371991 3 3 7.1-8.7-3.7-19(91) 3 3 646 5, 6 5 646-646 6 718 3, 6 3 1991 4567 000 dole 3, 6 3 The above table gives us some insight on how sophisticated the matching (or search / indexing) algorithm is. For the search term \"bob\", PHONE matches contact 3. Display name matches and contact has a phone even though it does match. EMAIL matches contact 1, 2. 1 has a matching email \"bob\". 2 is also matched because the name matches even though the email does not. On the other hand, 3 is NOT matched even though the name matches because 3 has no email. Adding an email to 3 will cause 3 to be matched. For the search term \"incredible\", EMAIL matches 2 (incredible@android.com) but NOT 1 (bob@incredibles.com). This means that email matching does not use contains but rather a form of startsWith . TLDR ANY matches any contact data; name, email, phone, address, organization, note, etc. EMAIL matches emails or (display name + any email) PHONE matches phones or (display name + any phone) EMAIL and PHONE matching is NOT as simple as using the Query API with .where { [Email|Phone] contains searchTerm }","title":"Comparison table"},{"location":"basics/update-contacts/","text":"Update contacts \u00b6 This library provides the Update API that allows you to update one or more Contacts, RawContacts, and Data. An instance of the Update API is obtained by, val update = Contacts ( context ). update () If you want to update the device owner Contact Profile, read Update device owner Contact profile . If you want to update a set of Data, read Update existing sets of data . A basic update \u00b6 To update a Contact and all of its RawContacts, val updateResult = Contacts ( context ) . update () . contacts ( johnDoe . mutableCopy { setOrganization { company = \"Microsoft\" title = \"Newb\" } emails (). first (). apply { address = \"john.doe@microsoft.com\" } }) . commit () To update a RawContact directly, val updateResult = Contacts ( context ) . update () . rawContacts ( johnDoeFromGmail . mutableCopy { setOrganization { company = \"Microsoft\" title = \"Newb\" } emails (). first (). apply { address = \"john.doe@microsoft.com\" } }) . commit () Deleting blanks \u00b6 The API allows you to specify if you want the update operation to delete blank contacts or not, . deleteBlanks ( true | false ) For more info, read about Blank contacts . Blank data are deleted \u00b6 Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are deleted by update APIs. For more info, read about Blank data . Including only specific data \u00b6 To include only the given set of fields (data) in each of the update operation, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations . Executing the update \u00b6 To execute the update, . commit () Handling the update result \u00b6 The commit function returns a Result , val contactsApi = Contacts ( context ) val mutableContact1 = contact1 . mutableCopy { ... } val mutableContact2 = contact2 . mutableCopy { ... } val updateResult = contactsApi . update () . contacts ( mutableContact1 , mutableContact2 ) . commit () To check if all updates succeeded, val allUpdatesSuccessful = updateResult . isSuccessful To check if a particular update succeeded, val firstUpdateSuccessful = updateResult . isSuccessful ( mutableContact1 ) Once you have performed the updates, you can retrieve the updated Contacts references via the Query API, val updatedContacts = contactsApi . query () . where { Contact . Id `in` listOf ( contact1 . id ) } . find () For more info, read Query contacts (advanced) . Alternatively, you may use the extensions provided in ContactRefresh and RawContactRefresh . To get the updated Contact and all of its RawContacts and Data, val updatedContact1 = contact1 . refresh ( contactsApi ) To get an updated RawContact and Data, val updatedRawContact1 = contact1 . rawContacts . first (). refresh ( contactsApi ) Cancelling the update \u00b6 To cancel an update amid execution, . commit { returnTrueIfUpdateShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel update processing as soon as possible. The function is called numerous times during update processing to check if processing should stop or continue. This gives you the option to cancel the update. For example, to automatically cancel the update inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val updateResult = update . commit { ! isActive } } } Performing the update and result processing asynchronously \u00b6 Updates are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the update with permission \u00b6 Updates require the android.permission.WRITE_CONTACTS permissions. If not granted, the update will do nothing and return a failed result. To perform the update with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Custom data support \u00b6 The Update API supports custom data. For more info, read Update custom data . Modifiable Contact fields \u00b6 As per documentation in android.provider.ContactsContract.Contacts , Only certain columns of Contact are modifiable: STARRED, CUSTOM_RINGTONE, SEND_TO_VOICEMAIL. Changing any of these columns on the Contact also changes them on all constituent raw contacts. The rest of the APIs provided in this library allow you to modify Data fields (e.g. Email, Phone, etc). Essentially, anything that the Contacts Provider allows for modification =) Updating photos and thumbnails \u00b6 Full-sized photos (and by API design thumbnails) can be set using other functions. For more info, read Get set remove full-sized and thumbnail contact photos . Local RawContacts \u00b6 Updates to local RawContacts are not synced! For more info, read Sync contact data across devices . There are also certain data kinds that are ignored on insert or update if the RawContact is local. For more info, read about Local (device-only) contacts .","title":"Update contacts"},{"location":"basics/update-contacts/#update-contacts","text":"This library provides the Update API that allows you to update one or more Contacts, RawContacts, and Data. An instance of the Update API is obtained by, val update = Contacts ( context ). update () If you want to update the device owner Contact Profile, read Update device owner Contact profile . If you want to update a set of Data, read Update existing sets of data .","title":"Update contacts"},{"location":"basics/update-contacts/#a-basic-update","text":"To update a Contact and all of its RawContacts, val updateResult = Contacts ( context ) . update () . contacts ( johnDoe . mutableCopy { setOrganization { company = \"Microsoft\" title = \"Newb\" } emails (). first (). apply { address = \"john.doe@microsoft.com\" } }) . commit () To update a RawContact directly, val updateResult = Contacts ( context ) . update () . rawContacts ( johnDoeFromGmail . mutableCopy { setOrganization { company = \"Microsoft\" title = \"Newb\" } emails (). first (). apply { address = \"john.doe@microsoft.com\" } }) . commit ()","title":"A basic update"},{"location":"basics/update-contacts/#deleting-blanks","text":"The API allows you to specify if you want the update operation to delete blank contacts or not, . deleteBlanks ( true | false ) For more info, read about Blank contacts .","title":"Deleting blanks"},{"location":"basics/update-contacts/#blank-data-are-deleted","text":"Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are deleted by update APIs. For more info, read about Blank data .","title":"Blank data are deleted"},{"location":"basics/update-contacts/#including-only-specific-data","text":"To include only the given set of fields (data) in each of the update operation, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations .","title":"Including only specific data"},{"location":"basics/update-contacts/#executing-the-update","text":"To execute the update, . commit ()","title":"Executing the update"},{"location":"basics/update-contacts/#handling-the-update-result","text":"The commit function returns a Result , val contactsApi = Contacts ( context ) val mutableContact1 = contact1 . mutableCopy { ... } val mutableContact2 = contact2 . mutableCopy { ... } val updateResult = contactsApi . update () . contacts ( mutableContact1 , mutableContact2 ) . commit () To check if all updates succeeded, val allUpdatesSuccessful = updateResult . isSuccessful To check if a particular update succeeded, val firstUpdateSuccessful = updateResult . isSuccessful ( mutableContact1 ) Once you have performed the updates, you can retrieve the updated Contacts references via the Query API, val updatedContacts = contactsApi . query () . where { Contact . Id `in` listOf ( contact1 . id ) } . find () For more info, read Query contacts (advanced) . Alternatively, you may use the extensions provided in ContactRefresh and RawContactRefresh . To get the updated Contact and all of its RawContacts and Data, val updatedContact1 = contact1 . refresh ( contactsApi ) To get an updated RawContact and Data, val updatedRawContact1 = contact1 . rawContacts . first (). refresh ( contactsApi )","title":"Handling the update result"},{"location":"basics/update-contacts/#cancelling-the-update","text":"To cancel an update amid execution, . commit { returnTrueIfUpdateShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel update processing as soon as possible. The function is called numerous times during update processing to check if processing should stop or continue. This gives you the option to cancel the update. For example, to automatically cancel the update inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val updateResult = update . commit { ! isActive } } }","title":"Cancelling the update"},{"location":"basics/update-contacts/#performing-the-update-and-result-processing-asynchronously","text":"Updates are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the update and result processing asynchronously"},{"location":"basics/update-contacts/#performing-the-update-with-permission","text":"Updates require the android.permission.WRITE_CONTACTS permissions. If not granted, the update will do nothing and return a failed result. To perform the update with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the update with permission"},{"location":"basics/update-contacts/#custom-data-support","text":"The Update API supports custom data. For more info, read Update custom data .","title":"Custom data support"},{"location":"basics/update-contacts/#modifiable-contact-fields","text":"As per documentation in android.provider.ContactsContract.Contacts , Only certain columns of Contact are modifiable: STARRED, CUSTOM_RINGTONE, SEND_TO_VOICEMAIL. Changing any of these columns on the Contact also changes them on all constituent raw contacts. The rest of the APIs provided in this library allow you to modify Data fields (e.g. Email, Phone, etc). Essentially, anything that the Contacts Provider allows for modification =)","title":"Modifiable Contact fields"},{"location":"basics/update-contacts/#updating-photos-and-thumbnails","text":"Full-sized photos (and by API design thumbnails) can be set using other functions. For more info, read Get set remove full-sized and thumbnail contact photos .","title":"Updating photos and thumbnails"},{"location":"basics/update-contacts/#local-rawcontacts","text":"Updates to local RawContacts are not synced! For more info, read Sync contact data across devices . There are also certain data kinds that are ignored on insert or update if the RawContact is local. For more info, read about Local (device-only) contacts .","title":"Local RawContacts"},{"location":"blockednumbers/about-blocked-numbers/","text":"Blocked numbers \u00b6 The Android 7.0 (API 24) release introduced the Blocked Numbers content provider that stores a list of phone numbers the user has specified should not be able to contact them via telephony communications (calls, SMS, MMS). This library provides the following APIs that allow you to read/write blocked numbers; BlockedNumbersQuery BlockedNumbersInsert BlockedNumbersDelete Blocked number data \u00b6 Blocked number data consists of the number and normalizedNumber . The BlockedNumber.number is the phone number to block as the user entered it. It may or may not be formatted (e.g. (012) 345-6789). Other than regular phone numbers, the blocked number provider can also store addresses (such as email) from which a user can receive messages, and calls. The BlockedNumber.normalizedNumber is the number 's E164 representation (e.g. +10123456789). This value can be omitted in which case the provider will try to automatically infer it. (It'll be left null if the provider fails to infer.) If present, number has to be set as well (it will be ignored otherwise). If you want to set this value yourself, you may want to look at android.telephony.PhoneNumberUtils . This may contain an email if number is an email. Privileges to read/write blocked numbers directly \u00b6 Reading and writing directly to the Blocked Numbers database table can only be done by certain privileged apps. The Blocked Number APIs this library provides will only work if all of the following requirements are met; your app must is a system app and/or the default dialer/phone app and/or the default SMS/messaging app the current user (if in a multi-user environment) must be allowed to read/write blocked numbers the runtime OS version is at least Android 7.0 (N) (API 24) To check if all of the requirements specified above are met, val canReadAndWriteBlockedNumbers = Contacts ( context ). blockedNumbers (). privileges . canReadAndWrite () Starting with Android 11 (API 30), you must include the following to your app's manifest in order to successfully use this function and therefore the bocked number APIs provided in this library . The above is required to be able to check if your app is the default SMS/messaging app. Use the builtin Blocked Numbers activity \u00b6 If your app does not have the privilege to read/write directly to the blocked number provider, you may instead launch the builtin system Blocked numbers activity. It provides a fully functional UI allowing users to see, add, and remove blocked numbers. It is the same activity used by the native ( AOSP) Contacts app and Google Contacts app when accessing the \"Blocked numbers\". Contacts ( context ). blockedNumbers (). startBlockedNumbersActivity ( activity ) If the activity is null, the builtin blocked numbers activity will be launched as a new task, separate from the current application instance. If it is provided, then the activity will be part of the current application's stack/history. Blocked numbers have been introduced in Android 7.0 (N) (API 24). Therefore, this will do nothing for versions lower than API 24. Using the DefaultDialerRequest extensions \u00b6 The most common way for 3rd party apps (apps that don't come pre-installed by the OEM) to get direct read/write access to the blocked numbers table is to be set as the default dialer/phone or SMS/messaging app. The contacts.ui.util.DefaultDialerRequest.kt in the ui module` provides extension functions that allow you to prompt the user to set your app as the default dialer/phone app. To use it, Activity { fun onRequestToBeTheDefaultDialerAppClicked () { requestToBeTheDefaultDialerApp () } override fun onActivityResult ( requestCode : Int , resultCode : Int , data : Intent?) { super . onActivityResult ( requestCode , resultCode , data ) onRequestToBeDefaultDialerAppResult ( requestCode , resultCode ) { // You are now able to use the BlockedNumbersQuery, BlockedNumbersInsert, and // BlockedNumbersDelete APIs. } } } Your app must have an activity with following intent filters in your manifest. Otherwise, this will do nothing. The above intent filters do NOT need to be added to the activity where the extension functions are invoked. It can be placed in any activity within the application. To check if your app is the default dialer/phone app, Context . isDefaultDialerApp () If your app is not a dialer/phone app , then you should not set it as the default dialer/phone app. Otherwise, users of your app may get confused as to why you are prompting them for this privilege. If you still want to read/write blocked numbers directly, you may still use this method. However, make it clear to your users as to why you are doing this despite your app not being a dialer/phone app. Update an existing blocked number entry \u00b6 Update operations are not supported by the Blocked Number provider. Use delete and insert instead. Debugging \u00b6 To look at all of the rows in the Blocked Numbers table, use the Context.logBlockedNumbersTable function in the debug module. For more info, read Debug the Blocked Number Provider tables .","title":"About blocked numbers"},{"location":"blockednumbers/about-blocked-numbers/#blocked-numbers","text":"The Android 7.0 (API 24) release introduced the Blocked Numbers content provider that stores a list of phone numbers the user has specified should not be able to contact them via telephony communications (calls, SMS, MMS). This library provides the following APIs that allow you to read/write blocked numbers; BlockedNumbersQuery BlockedNumbersInsert BlockedNumbersDelete","title":"Blocked numbers"},{"location":"blockednumbers/about-blocked-numbers/#blocked-number-data","text":"Blocked number data consists of the number and normalizedNumber . The BlockedNumber.number is the phone number to block as the user entered it. It may or may not be formatted (e.g. (012) 345-6789). Other than regular phone numbers, the blocked number provider can also store addresses (such as email) from which a user can receive messages, and calls. The BlockedNumber.normalizedNumber is the number 's E164 representation (e.g. +10123456789). This value can be omitted in which case the provider will try to automatically infer it. (It'll be left null if the provider fails to infer.) If present, number has to be set as well (it will be ignored otherwise). If you want to set this value yourself, you may want to look at android.telephony.PhoneNumberUtils . This may contain an email if number is an email.","title":"Blocked number data"},{"location":"blockednumbers/about-blocked-numbers/#privileges-to-readwrite-blocked-numbers-directly","text":"Reading and writing directly to the Blocked Numbers database table can only be done by certain privileged apps. The Blocked Number APIs this library provides will only work if all of the following requirements are met; your app must is a system app and/or the default dialer/phone app and/or the default SMS/messaging app the current user (if in a multi-user environment) must be allowed to read/write blocked numbers the runtime OS version is at least Android 7.0 (N) (API 24) To check if all of the requirements specified above are met, val canReadAndWriteBlockedNumbers = Contacts ( context ). blockedNumbers (). privileges . canReadAndWrite () Starting with Android 11 (API 30), you must include the following to your app's manifest in order to successfully use this function and therefore the bocked number APIs provided in this library . The above is required to be able to check if your app is the default SMS/messaging app.","title":"Privileges to read/write blocked numbers directly"},{"location":"blockednumbers/about-blocked-numbers/#use-the-builtin-blocked-numbers-activity","text":"If your app does not have the privilege to read/write directly to the blocked number provider, you may instead launch the builtin system Blocked numbers activity. It provides a fully functional UI allowing users to see, add, and remove blocked numbers. It is the same activity used by the native ( AOSP) Contacts app and Google Contacts app when accessing the \"Blocked numbers\". Contacts ( context ). blockedNumbers (). startBlockedNumbersActivity ( activity ) If the activity is null, the builtin blocked numbers activity will be launched as a new task, separate from the current application instance. If it is provided, then the activity will be part of the current application's stack/history. Blocked numbers have been introduced in Android 7.0 (N) (API 24). Therefore, this will do nothing for versions lower than API 24.","title":"Use the builtin Blocked Numbers activity"},{"location":"blockednumbers/about-blocked-numbers/#using-the-defaultdialerrequest-extensions","text":"The most common way for 3rd party apps (apps that don't come pre-installed by the OEM) to get direct read/write access to the blocked numbers table is to be set as the default dialer/phone or SMS/messaging app. The contacts.ui.util.DefaultDialerRequest.kt in the ui module` provides extension functions that allow you to prompt the user to set your app as the default dialer/phone app. To use it, Activity { fun onRequestToBeTheDefaultDialerAppClicked () { requestToBeTheDefaultDialerApp () } override fun onActivityResult ( requestCode : Int , resultCode : Int , data : Intent?) { super . onActivityResult ( requestCode , resultCode , data ) onRequestToBeDefaultDialerAppResult ( requestCode , resultCode ) { // You are now able to use the BlockedNumbersQuery, BlockedNumbersInsert, and // BlockedNumbersDelete APIs. } } } Your app must have an activity with following intent filters in your manifest. Otherwise, this will do nothing. The above intent filters do NOT need to be added to the activity where the extension functions are invoked. It can be placed in any activity within the application. To check if your app is the default dialer/phone app, Context . isDefaultDialerApp () If your app is not a dialer/phone app , then you should not set it as the default dialer/phone app. Otherwise, users of your app may get confused as to why you are prompting them for this privilege. If you still want to read/write blocked numbers directly, you may still use this method. However, make it clear to your users as to why you are doing this despite your app not being a dialer/phone app.","title":"Using the DefaultDialerRequest extensions"},{"location":"blockednumbers/about-blocked-numbers/#update-an-existing-blocked-number-entry","text":"Update operations are not supported by the Blocked Number provider. Use delete and insert instead.","title":"Update an existing blocked number entry"},{"location":"blockednumbers/about-blocked-numbers/#debugging","text":"To look at all of the rows in the Blocked Numbers table, use the Context.logBlockedNumbersTable function in the debug module. For more info, read Debug the Blocked Number Provider tables .","title":"Debugging"},{"location":"blockednumbers/delete-blocked-numbers/","text":"Delete blocked numbers \u00b6 This library provides the BlockedNumbersDelete API that allows you to delete existing BlockedNumbers. An instance of the BlockedNumbersDelete API is obtained by, val delete = Contacts ( context ). blockedNumbers (). delete () Note that blocked number deletions will only work for privileged apps. For more info, read about Blocked numbers . A basic delete \u00b6 To delete a set of existing blocked numbers, val deleteResult = Contacts ( context ) . blockedNumbers () . delete () . blockedNumbers ( existingBlockedNumbers ) . commit () Executing the delete \u00b6 To execute the delete, . commit () If you want to delete all given blockedNumbers in a single atomic transaction, . commitInOneTransaction () The call to commitInOneTransaction will only succeed if ALL given blocked numbers are successfully deleted. If one delete fails, the entire operation will fail and everything will be reverted prior to the delete operation. In contrast, commit allows for some deletes to succeed and some to fail. Handling the delete result \u00b6 The commit and commitInOneTransaction functions returns a Result , To check if all deletes succeeded, val allDeletesSuccessful = deleteResult . isSuccessful To check if a particular delete succeeded, val firstDeleteSuccessful = deleteResult . isSuccessful ( blockedNumber1 ) Performing the delete and result processing asynchronously \u00b6 Deletes are executed when the commit or commitInOneTransaction function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the delete with permission \u00b6 There are no permissions required for blocked numbers. However, there are privileges that must be acquired. For more info, read about Blocked numbers .","title":"Delete blocked numbers"},{"location":"blockednumbers/delete-blocked-numbers/#delete-blocked-numbers","text":"This library provides the BlockedNumbersDelete API that allows you to delete existing BlockedNumbers. An instance of the BlockedNumbersDelete API is obtained by, val delete = Contacts ( context ). blockedNumbers (). delete () Note that blocked number deletions will only work for privileged apps. For more info, read about Blocked numbers .","title":"Delete blocked numbers"},{"location":"blockednumbers/delete-blocked-numbers/#a-basic-delete","text":"To delete a set of existing blocked numbers, val deleteResult = Contacts ( context ) . blockedNumbers () . delete () . blockedNumbers ( existingBlockedNumbers ) . commit ()","title":"A basic delete"},{"location":"blockednumbers/delete-blocked-numbers/#executing-the-delete","text":"To execute the delete, . commit () If you want to delete all given blockedNumbers in a single atomic transaction, . commitInOneTransaction () The call to commitInOneTransaction will only succeed if ALL given blocked numbers are successfully deleted. If one delete fails, the entire operation will fail and everything will be reverted prior to the delete operation. In contrast, commit allows for some deletes to succeed and some to fail.","title":"Executing the delete"},{"location":"blockednumbers/delete-blocked-numbers/#handling-the-delete-result","text":"The commit and commitInOneTransaction functions returns a Result , To check if all deletes succeeded, val allDeletesSuccessful = deleteResult . isSuccessful To check if a particular delete succeeded, val firstDeleteSuccessful = deleteResult . isSuccessful ( blockedNumber1 )","title":"Handling the delete result"},{"location":"blockednumbers/delete-blocked-numbers/#performing-the-delete-and-result-processing-asynchronously","text":"Deletes are executed when the commit or commitInOneTransaction function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the delete and result processing asynchronously"},{"location":"blockednumbers/delete-blocked-numbers/#performing-the-delete-with-permission","text":"There are no permissions required for blocked numbers. However, there are privileges that must be acquired. For more info, read about Blocked numbers .","title":"Performing the delete with permission"},{"location":"blockednumbers/insert-blocked-numbers/","text":"Insert blocked numbers \u00b6 This library provides the BlockedNumbersInsert API that allows you to create/insert blocked numbers. An instance of the BlockedNumbersInsert API is obtained by, val insert = Contacts ( context ). blockedNumbers (). insert () Note that blocked number insertions will only work for privileged apps. For more info, read about Blocked numbers . A basic insert \u00b6 To create/insert a new blocked number, val insertResult = Contacts ( context ) . blockedNumbers () . insert () . blockedNumber { number = \"(555) 555-5555\" } . commit () If you need to insert multiple blocked numbers, val newBlockedNumber1 = NewBlockedNumber ( number = \"(555) 555-5555\" ) val newBlockedNumber2 = NewBlockedNumber ( number = \"(123) 456-7890\" ) val insertResult = Contacts ( context ) . blockedNumbers () . insert () . blockedNumbers ( newBlockedNumber1 , newBlockedNumber2 ) . commit () Executing the insert \u00b6 To execute the insert, . commit () Handling the insert result \u00b6 The commit function returns a Result . To check if all inserts succeeded, val allInsertsSuccessful = insertResult . isSuccessful To check if a particular insert succeeded, val firstInsertSuccessful = insertResult . isSuccessful ( newBlockedNumber1 ) To get the BlockedNumber IDs of all the newly created BlockedNumbers, val allBlockedNumberIds = insertResult . blockedNumberIds To get the BlockedNumber ID of a particular BlockedNumber, val secondBlockedNumberId = insertResult . blockedNumberId ( newBlockedNumber2 ) Once you have the BlockedNumber IDs, you can retrieve the newly created BlockedNumbers via the BlockedNumbersQuery API, val blockedNumbers = contactsApi . blockedNumbers () . query () . where { Id `in` allBlockedNumberIds } . find () For more info, read Query blocked numbers . Alternatively, you may use the extensions provided in BlockedNumbersInsertResult . To get all newly created BlockedNumbers, val blockedNumbers = insertResult . blockedNumbers ( contactsApi ) To get a particular blockedNumber, val blockedNumber = insertResult . blockedNumber ( contactsApi , newBlockedNumber1 ) Handling insert failure \u00b6 The insert may fail for a particular blocked number for various reasons, insertResult . failureReason ( newBlockedNumber1 ) ?. let { when ( it ) { NUMBER_ALREADY_BLOCKED -> tellUserTheNumberIsAlreadyBlocked () NUMBER_IS_BLANK -> promptUserProvideANonBlankNumber () UNKNOWN -> showGenericErrorMessage () } } Cancelling the insert \u00b6 To cancel an insert amid execution, . commit { returnTrueIfInsertShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel insert processing as soon as possible. The function is called numerous times during insert processing to check if processing should stop or continue. This gives you the option to cancel the insert. For example, to automatically cancel the insert inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val insertResult = insert . commit { ! isActive } } } Performing the insert and result processing asynchronously \u00b6 Inserts are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the insert with permission \u00b6 There are no permissions required for blocked numbers. However, there are privileges that must be acquired. For more info, read about Blocked numbers .","title":"Insert blocked numbers"},{"location":"blockednumbers/insert-blocked-numbers/#insert-blocked-numbers","text":"This library provides the BlockedNumbersInsert API that allows you to create/insert blocked numbers. An instance of the BlockedNumbersInsert API is obtained by, val insert = Contacts ( context ). blockedNumbers (). insert () Note that blocked number insertions will only work for privileged apps. For more info, read about Blocked numbers .","title":"Insert blocked numbers"},{"location":"blockednumbers/insert-blocked-numbers/#a-basic-insert","text":"To create/insert a new blocked number, val insertResult = Contacts ( context ) . blockedNumbers () . insert () . blockedNumber { number = \"(555) 555-5555\" } . commit () If you need to insert multiple blocked numbers, val newBlockedNumber1 = NewBlockedNumber ( number = \"(555) 555-5555\" ) val newBlockedNumber2 = NewBlockedNumber ( number = \"(123) 456-7890\" ) val insertResult = Contacts ( context ) . blockedNumbers () . insert () . blockedNumbers ( newBlockedNumber1 , newBlockedNumber2 ) . commit ()","title":"A basic insert"},{"location":"blockednumbers/insert-blocked-numbers/#executing-the-insert","text":"To execute the insert, . commit ()","title":"Executing the insert"},{"location":"blockednumbers/insert-blocked-numbers/#handling-the-insert-result","text":"The commit function returns a Result . To check if all inserts succeeded, val allInsertsSuccessful = insertResult . isSuccessful To check if a particular insert succeeded, val firstInsertSuccessful = insertResult . isSuccessful ( newBlockedNumber1 ) To get the BlockedNumber IDs of all the newly created BlockedNumbers, val allBlockedNumberIds = insertResult . blockedNumberIds To get the BlockedNumber ID of a particular BlockedNumber, val secondBlockedNumberId = insertResult . blockedNumberId ( newBlockedNumber2 ) Once you have the BlockedNumber IDs, you can retrieve the newly created BlockedNumbers via the BlockedNumbersQuery API, val blockedNumbers = contactsApi . blockedNumbers () . query () . where { Id `in` allBlockedNumberIds } . find () For more info, read Query blocked numbers . Alternatively, you may use the extensions provided in BlockedNumbersInsertResult . To get all newly created BlockedNumbers, val blockedNumbers = insertResult . blockedNumbers ( contactsApi ) To get a particular blockedNumber, val blockedNumber = insertResult . blockedNumber ( contactsApi , newBlockedNumber1 )","title":"Handling the insert result"},{"location":"blockednumbers/insert-blocked-numbers/#handling-insert-failure","text":"The insert may fail for a particular blocked number for various reasons, insertResult . failureReason ( newBlockedNumber1 ) ?. let { when ( it ) { NUMBER_ALREADY_BLOCKED -> tellUserTheNumberIsAlreadyBlocked () NUMBER_IS_BLANK -> promptUserProvideANonBlankNumber () UNKNOWN -> showGenericErrorMessage () } }","title":"Handling insert failure"},{"location":"blockednumbers/insert-blocked-numbers/#cancelling-the-insert","text":"To cancel an insert amid execution, . commit { returnTrueIfInsertShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel insert processing as soon as possible. The function is called numerous times during insert processing to check if processing should stop or continue. This gives you the option to cancel the insert. For example, to automatically cancel the insert inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val insertResult = insert . commit { ! isActive } } }","title":"Cancelling the insert"},{"location":"blockednumbers/insert-blocked-numbers/#performing-the-insert-and-result-processing-asynchronously","text":"Inserts are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the insert and result processing asynchronously"},{"location":"blockednumbers/insert-blocked-numbers/#performing-the-insert-with-permission","text":"There are no permissions required for blocked numbers. However, there are privileges that must be acquired. For more info, read about Blocked numbers .","title":"Performing the insert with permission"},{"location":"blockednumbers/query-blocked-numbers/","text":"Query blocked numbers \u00b6 This library provides the BlockedNumbersQuery API that allows you to get blocked numbers. An instance of the BlockedNumbersQuery API is obtained by, val query = Contacts ( context ). blockedNumbers (). query () Note that blocked number queries will only work for privileged apps. For more info, read about Blocked numbers . A basic query \u00b6 To get all of the blocked numbers, val blockedNumbers = Contacts ( context ) . blockedNumbers () . query () . find () Ordering \u00b6 To order resulting BlockedNumbers using one or more fields, . orderBy ( fieldOrder ) For example, to order blocked numbers by number, . orderBy ( BlockedNumbersFields . Number . asc ()) String comparisons ignores case by default. Each orderBys provides ignoreCase as an optional parameter. Use BlockedNumbersFields to construct the orderBys. Limiting and offsetting \u00b6 To limit the amount of blocked numbers returned and/or offset (skip) a specified number of blocked numbers, . limit ( limit ) . offset ( offset ) For example, to only get a maximum 20 blocked numbers, skipping the first 20, . limit ( 20 ) . offset ( 20 ) This is useful for pagination =) Note that it is recommended to limit the number of blocked numbers when querying to increase performance and decrease memory cost. Executing the query \u00b6 To execute the query, . find () Cancelling the query \u00b6 To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val blockedNumbers = query . find { ! isActive } } } Performing the query asynchronously \u00b6 Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the query with permission \u00b6 There are no permissions required for blocked numbers. However, there are privileges that must be acquired. For more info, read about Blocked numbers . Using the where function to specify matching criteria \u00b6 Use the contacts.core.BlockedNumbersFields combined with the extensions from contacts.core.Where to form WHERE clauses. This docs page will not provide a tutorial on database where clauses. It assumes that you know the basics. If you don't know the basics, then search for sqlite where clause . For example, to find blocked numbers that contains \"555\", . where { Number contains \"555\" } To get a list of blocked numbers by IDs, . where { Id `in` blockedNumberIds }","title":"Query blocked numbers"},{"location":"blockednumbers/query-blocked-numbers/#query-blocked-numbers","text":"This library provides the BlockedNumbersQuery API that allows you to get blocked numbers. An instance of the BlockedNumbersQuery API is obtained by, val query = Contacts ( context ). blockedNumbers (). query () Note that blocked number queries will only work for privileged apps. For more info, read about Blocked numbers .","title":"Query blocked numbers"},{"location":"blockednumbers/query-blocked-numbers/#a-basic-query","text":"To get all of the blocked numbers, val blockedNumbers = Contacts ( context ) . blockedNumbers () . query () . find ()","title":"A basic query"},{"location":"blockednumbers/query-blocked-numbers/#ordering","text":"To order resulting BlockedNumbers using one or more fields, . orderBy ( fieldOrder ) For example, to order blocked numbers by number, . orderBy ( BlockedNumbersFields . Number . asc ()) String comparisons ignores case by default. Each orderBys provides ignoreCase as an optional parameter. Use BlockedNumbersFields to construct the orderBys.","title":"Ordering"},{"location":"blockednumbers/query-blocked-numbers/#limiting-and-offsetting","text":"To limit the amount of blocked numbers returned and/or offset (skip) a specified number of blocked numbers, . limit ( limit ) . offset ( offset ) For example, to only get a maximum 20 blocked numbers, skipping the first 20, . limit ( 20 ) . offset ( 20 ) This is useful for pagination =) Note that it is recommended to limit the number of blocked numbers when querying to increase performance and decrease memory cost.","title":"Limiting and offsetting"},{"location":"blockednumbers/query-blocked-numbers/#executing-the-query","text":"To execute the query, . find ()","title":"Executing the query"},{"location":"blockednumbers/query-blocked-numbers/#cancelling-the-query","text":"To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val blockedNumbers = query . find { ! isActive } } }","title":"Cancelling the query"},{"location":"blockednumbers/query-blocked-numbers/#performing-the-query-asynchronously","text":"Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the query asynchronously"},{"location":"blockednumbers/query-blocked-numbers/#performing-the-query-with-permission","text":"There are no permissions required for blocked numbers. However, there are privileges that must be acquired. For more info, read about Blocked numbers .","title":"Performing the query with permission"},{"location":"blockednumbers/query-blocked-numbers/#using-the-where-function-to-specify-matching-criteria","text":"Use the contacts.core.BlockedNumbersFields combined with the extensions from contacts.core.Where to form WHERE clauses. This docs page will not provide a tutorial on database where clauses. It assumes that you know the basics. If you don't know the basics, then search for sqlite where clause . For example, to find blocked numbers that contains \"555\", . where { Number contains \"555\" } To get a list of blocked numbers by IDs, . where { Id `in` blockedNumberIds }","title":"Using the where function to specify matching criteria"},{"location":"customdata/delete-custom-data/","text":"Delete custom data \u00b6 This library provides several APIs that supports deleting custom data. DataDelete Delete existing sets of data Delete Delete Contacts ProfileDelete Delete device owner Contact profile Update Update contacts ProfileUpdate Update device owner Contact profile To help illustrate how custom data integrates with these update APIs, we'll use the HandleName and Gender custom data. For more info about custom data, read Integrate custom data . Deleting custom data via Contacts/RawContacts \u00b6 Custom data, just like regular data kinds, are attached to a RawContact. They follow the same rules as regular data kinds. For more info, read about API Entities . For example, you are able to delete existing handle names and the gender of an existing RawContact, mutableRawContact . removeHandleName ( contactsApi , handleName ) mutableRawContact . setGender ( contactsApi , null ) There are also extensions that allow you to delete custom data of an existing RawContact via a Contact, which can be made up of one or more RawContacts, mutableContact . removeHandleName ( contactsApi , handleName ) mutableContact . setGender ( contactsApi , null ) Once you have removed custom data, you can perform the update operation on the RawContact to commit your changes into the database using Update or ProfileUpdate . You may also delete an entire Contact or RawContact using Delete or ProfileDelete in order delete all associated data. Deleting set of custom data directly \u00b6 All custom data are compatible with the DataDelete API, which allows you to delete sets of existing regular and custom data kinds. For example, to delete a set of HandleName s and Gender s, val handleNames : List < MutableHandleName > val genders : List < MutableGender > val deleteResult = Contacts ( this ) . data () . delete () . data ( handleNames + genders ) . commit () For more info, read Delete existing sets of data .","title":"Delete custom data"},{"location":"customdata/delete-custom-data/#delete-custom-data","text":"This library provides several APIs that supports deleting custom data. DataDelete Delete existing sets of data Delete Delete Contacts ProfileDelete Delete device owner Contact profile Update Update contacts ProfileUpdate Update device owner Contact profile To help illustrate how custom data integrates with these update APIs, we'll use the HandleName and Gender custom data. For more info about custom data, read Integrate custom data .","title":"Delete custom data"},{"location":"customdata/delete-custom-data/#deleting-custom-data-via-contactsrawcontacts","text":"Custom data, just like regular data kinds, are attached to a RawContact. They follow the same rules as regular data kinds. For more info, read about API Entities . For example, you are able to delete existing handle names and the gender of an existing RawContact, mutableRawContact . removeHandleName ( contactsApi , handleName ) mutableRawContact . setGender ( contactsApi , null ) There are also extensions that allow you to delete custom data of an existing RawContact via a Contact, which can be made up of one or more RawContacts, mutableContact . removeHandleName ( contactsApi , handleName ) mutableContact . setGender ( contactsApi , null ) Once you have removed custom data, you can perform the update operation on the RawContact to commit your changes into the database using Update or ProfileUpdate . You may also delete an entire Contact or RawContact using Delete or ProfileDelete in order delete all associated data.","title":"Deleting custom data via Contacts/RawContacts"},{"location":"customdata/delete-custom-data/#deleting-set-of-custom-data-directly","text":"All custom data are compatible with the DataDelete API, which allows you to delete sets of existing regular and custom data kinds. For example, to delete a set of HandleName s and Gender s, val handleNames : List < MutableHandleName > val genders : List < MutableGender > val deleteResult = Contacts ( this ) . data () . delete () . data ( handleNames + genders ) . commit () For more info, read Delete existing sets of data .","title":"Deleting set of custom data directly"},{"location":"customdata/insert-custom-data/","text":"Insert custom data into new or existing contacts \u00b6 Regular and custom data can only be created/inserted into the database whenever inserting or updating new or existing contacts. This library provides several insert and update APIs that support custom data integration. Insert Insert contacts ProfileInsert Insert device owner Contact profile Update Update contacts ProfileUpdate Update device owner Contact profile To help illustrate how custom data integrates with these query APIs, we'll use the HandleName and Gender custom data. For more info, read Integrate the gender custom data and Integrate the handle name custom data . Creating/inserting custom data into a RawContact \u00b6 Custom data, just like regular data kinds, are attached to a RawContact. They follow the same rules as regular data kinds. For more info, read about API Entities . For example, you are able to add handle names and set gender of a new RawContact, newRawContact . addHandleName ( contactsApi ) { handle = \"dude91\" } newRawContact . setGender ( contactsApi ) { type = GenderEntity . Type . MALE } Once you have created/insert the custom data into the RawContact, you can perform the insert operation on the new RawContact to commit your changes into the database. For more info, read Insert data into new or existing contacts . The include function and custom data \u00b6 All of the above mentioned APIs provide an include function that allows you to include only a given set of fields (data) to be processed in the insert operation. Custom data entries provides fields that can be used in this function. By default, not calling the include function will include all fields, including custom data fields. For example, to specifically include only HandleName and Gender fields, . include ( HandleNameFields . all + GenderFields . all ) For more info, read Include only certain fields for read and write operations . Blank data are not inserted \u00b6 Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are ignored and are not inserted by insert APIs. For more info, read about Blank data .","title":"Insert custom data into new or existing contacts"},{"location":"customdata/insert-custom-data/#insert-custom-data-into-new-or-existing-contacts","text":"Regular and custom data can only be created/inserted into the database whenever inserting or updating new or existing contacts. This library provides several insert and update APIs that support custom data integration. Insert Insert contacts ProfileInsert Insert device owner Contact profile Update Update contacts ProfileUpdate Update device owner Contact profile To help illustrate how custom data integrates with these query APIs, we'll use the HandleName and Gender custom data. For more info, read Integrate the gender custom data and Integrate the handle name custom data .","title":"Insert custom data into new or existing contacts"},{"location":"customdata/insert-custom-data/#creatinginserting-custom-data-into-a-rawcontact","text":"Custom data, just like regular data kinds, are attached to a RawContact. They follow the same rules as regular data kinds. For more info, read about API Entities . For example, you are able to add handle names and set gender of a new RawContact, newRawContact . addHandleName ( contactsApi ) { handle = \"dude91\" } newRawContact . setGender ( contactsApi ) { type = GenderEntity . Type . MALE } Once you have created/insert the custom data into the RawContact, you can perform the insert operation on the new RawContact to commit your changes into the database. For more info, read Insert data into new or existing contacts .","title":"Creating/inserting custom data into a RawContact"},{"location":"customdata/insert-custom-data/#the-include-function-and-custom-data","text":"All of the above mentioned APIs provide an include function that allows you to include only a given set of fields (data) to be processed in the insert operation. Custom data entries provides fields that can be used in this function. By default, not calling the include function will include all fields, including custom data fields. For example, to specifically include only HandleName and Gender fields, . include ( HandleNameFields . all + GenderFields . all ) For more info, read Include only certain fields for read and write operations .","title":"The include function and custom data"},{"location":"customdata/insert-custom-data/#blank-data-are-not-inserted","text":"Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are ignored and are not inserted by insert APIs. For more info, read about Blank data .","title":"Blank data are not inserted"},{"location":"customdata/integrate-custom-data-from-other-apps/","text":"Integrate custom data from other apps \u00b6 If you are looking to create and integrate your own custom data, read Integrate custom data . If you are looking to integrate custom data from other apps, you are in the right place! There are a lot of other apps out there that provide their own custom data, such as Google Contacts and WhatsApp . There are two parts to \"integrating custom data\"; Providing create (insert), read (query), update, and delete (CRUD) APIs for custom data associated with RawContacts. Providing sync adapters to sync custom data across devices. This library only handles the first part. Other (third party) apps typically provide sync adapters to sync their custom data across devices. This library does not interfere with any syncing functionality of custom data from other apps. What this library does is allow you and others to easily read and write custom data from other apps in your own apps. Research what custom data the third party app's are adding, if any \u00b6 The hardest part will be researching what custom data a particular third party app provides, what they are used for, and how they behave. Here are some things you can do to find the answers. Install and log into the app you are interested in researching. Then, use the debug module functions in your app to log the Data table via Context.logDataTable() . Look for any mime types that look like like they belong to the app. Figure out how where in the app's UI the data is shown and/or how the app uses it in general. Deconstruct the APK and look for res/xml/contacts.xml and other places in code where custom data may reside. A good place to look will be in sync adapter related classes . Search the internet for any official documentation on the custom data added by the app. There is a high chance that this does not exist. Search the internet for other people's research on the app's custom data, if any. The first strategy is the most effective strategy to take because you are able to experience first-hand and play around with the custom data and document everything about it. Nothing beats first-hand research! The second strategy is a bit more hacky and advanced and time consuming but it could pay off. The third strategy is optimistic but could end up being the most useful if you are able to locate official documentation from the app developers themselves. The fourth strategy could be unreliable as it depends on other people's knowledge, which could be inaccurate. Integrate the third party app custom data with this library \u00b6 Once you have figured out all of the details of all of the custom data (mimetypes) that the third party app adds, you may proceed to write the code that will allow you and others to perform read and write operations on it using the CRUD APIs provided in this library. To proceed, read Integrate custom data . Example, Google Contacts app custom data \u00b6 Issue #165: Google Contacts app custom data integrates custom data from the Google Contacts app into this library. You may use it as an example on how to get started with the research and also what code to write after the research has been completed. Consider adding your integration of third party apps' custom data to this library \u00b6 Let's say that you have written the code that integrates custom data from a third party app into your app using this library. That's great and all but your app will be the only app that will be able to use it! In the spirit of open source, please feel free to add your third party app custom data integration into this library so that other people using this library can optionally integrate it into their own apps! Please create a GitHub issue and file a pull request!","title":"Integrate custom data from other apps"},{"location":"customdata/integrate-custom-data-from-other-apps/#integrate-custom-data-from-other-apps","text":"If you are looking to create and integrate your own custom data, read Integrate custom data . If you are looking to integrate custom data from other apps, you are in the right place! There are a lot of other apps out there that provide their own custom data, such as Google Contacts and WhatsApp . There are two parts to \"integrating custom data\"; Providing create (insert), read (query), update, and delete (CRUD) APIs for custom data associated with RawContacts. Providing sync adapters to sync custom data across devices. This library only handles the first part. Other (third party) apps typically provide sync adapters to sync their custom data across devices. This library does not interfere with any syncing functionality of custom data from other apps. What this library does is allow you and others to easily read and write custom data from other apps in your own apps.","title":"Integrate custom data from other apps"},{"location":"customdata/integrate-custom-data-from-other-apps/#research-what-custom-data-the-third-party-apps-are-adding-if-any","text":"The hardest part will be researching what custom data a particular third party app provides, what they are used for, and how they behave. Here are some things you can do to find the answers. Install and log into the app you are interested in researching. Then, use the debug module functions in your app to log the Data table via Context.logDataTable() . Look for any mime types that look like like they belong to the app. Figure out how where in the app's UI the data is shown and/or how the app uses it in general. Deconstruct the APK and look for res/xml/contacts.xml and other places in code where custom data may reside. A good place to look will be in sync adapter related classes . Search the internet for any official documentation on the custom data added by the app. There is a high chance that this does not exist. Search the internet for other people's research on the app's custom data, if any. The first strategy is the most effective strategy to take because you are able to experience first-hand and play around with the custom data and document everything about it. Nothing beats first-hand research! The second strategy is a bit more hacky and advanced and time consuming but it could pay off. The third strategy is optimistic but could end up being the most useful if you are able to locate official documentation from the app developers themselves. The fourth strategy could be unreliable as it depends on other people's knowledge, which could be inaccurate.","title":"Research what custom data the third party app's are adding, if any"},{"location":"customdata/integrate-custom-data-from-other-apps/#integrate-the-third-party-app-custom-data-with-this-library","text":"Once you have figured out all of the details of all of the custom data (mimetypes) that the third party app adds, you may proceed to write the code that will allow you and others to perform read and write operations on it using the CRUD APIs provided in this library. To proceed, read Integrate custom data .","title":"Integrate the third party app custom data with this library"},{"location":"customdata/integrate-custom-data-from-other-apps/#example-google-contacts-app-custom-data","text":"Issue #165: Google Contacts app custom data integrates custom data from the Google Contacts app into this library. You may use it as an example on how to get started with the research and also what code to write after the research has been completed.","title":"Example, Google Contacts app custom data"},{"location":"customdata/integrate-custom-data-from-other-apps/#consider-adding-your-integration-of-third-party-apps-custom-data-to-this-library","text":"Let's say that you have written the code that integrates custom data from a third party app into your app using this library. That's great and all but your app will be the only app that will be able to use it! In the spirit of open source, please feel free to add your third party app custom data integration into this library so that other people using this library can optionally integrate it into their own apps! Please create a GitHub issue and file a pull request!","title":"Consider adding your integration of third party apps' custom data to this library"},{"location":"customdata/integrate-custom-data/","text":"Integrate custom data \u00b6 If you are looking to integrate custom data from other apps, read Integrate custom data from other apps . If you are looking to create and integrate your own custom data, you are in the right place! There are two parts to \"integrating custom data\"; Providing create (insert), read (query), update, and delete (CRUD) APIs for custom data associated with RawContacts. Providing sync adapters to sync custom data across devices. This library only handles the first part. If you want to sync your custom data, then you need to implement a sync adapter to interface with your remote server. That is out of scope of this library. In order to create and integrate your own custom data for use in your own apps, there is a bit of boilerplate code that needs to be written. Thankfully none of this stuff is difficult! Here are the steps, in chronological order, on how to define and use your own custom data, Define the mimetype Define the entities Define the fields Implement the cursor Implement the mapper Implement the operation Define the count restriction Define RawContact getters and setters Define Contact getters and setters Define exceptions Implement the field mapper Define the data query function Define the custom data entry Define the custom data entry registration Register your custom data with the Contacts API instance Use your custom data in queries, inserts, updates, and deletes Maybe someday someone with code generation experience (or I'll learn how to do it), will create annotations and annotation processors to eliminate having to manually write this stuff =) To help illustrate the above steps, we'll use the HandleName and Gender custom data provided in this library's customdata-handlename and customdata-gender respectively as an example. For more specifics on these custom data, read Integrate the gender custom data and Integrate the handle name custom data . At the bottom of this page, we'll also discuss, Consider adding your custom data to this library Custom data without sync adapters will not be synced Displaying your custom data in other Contacts apps Summary of limitations Some of the code used in these examples are in Kotlin. If you would like a Java version of this page, create an issue in GitHub. You are also free to file a pull request with your own page. In the event that a Java version of this page is created, this quote block should be replaced with a link to that page. 1. Define the mimetype \u00b6 The mimetype is a string that describes what kind of data a row in the Data table represents. For Gender , internal object GenderMimeType : MimeType . Custom () { // Following Contacts Provider convention of \"vnd.android.cursor.item/.\" override val value : String = \"vnd.android.cursor.item/contacts.entities.custom.gender\" } For HandleName , internal object HandleNameMimeType : MimeType . Custom () { // Following Contacts Provider convention of \"vnd.android.cursor.item/.\" override val value : String = \"vnd.android.cursor.item/contacts.entities.custom.handlename\" } Do not change the mimetype value! If you have already deployed apps to production that use these mimetype values, then changing them could result in \"data loss\". Old rows in the Data table will not be compatible if the mimetype value changes. You can certainly perform migrations by creating a new custom data altogether and migrating your old custom data to your new one. Do not use built-in mimetypes! The Contacts Provider has predefined the mimetypes for all of the common data kinds it supports (e.g. email). Make sure that your custom data does not use any of those. You can take a look at built-in mimetypes in contacs.core.entities.MimeType.kt . But, here they are for your convenience =) Builtin data kind mimetype Address \"vnd.android.cursor.item/postal-address_v2\" Email \"vnd.android.cursor.item/email_v2\" Event \"vnd.android.cursor.item/contact_event\" GroupMembership \"vnd.android.cursor.item/group_membership\" Im \"vnd.android.cursor.item/im\" Name \"vnd.android.cursor.item/name\" Nickname \"vnd.android.cursor.item/nickname\" Note \"vnd.android.cursor.item/note\" Organization \"vnd.android.cursor.item/organization\" Phone \"vnd.android.cursor.item/phone_v2\" Photo \"vnd.android.cursor.item/photo\" Relation \"vnd.android.cursor.item/relation\" SipAddress \"vnd.android.cursor.item/sip_address\" Website \"vnd.android.cursor.item/website\" 2. Define the entities \u00b6 The entities are the main code that users of your custom data will be exposed to. The properties model/represent the fields/columns in the Data table. Due to the length of the Gender.kt and HandleName.kt files, I will not be copy-pasting them here. Please take a look at those files instead. A few things to note, Either inherit from CustomDataEntity or CustomDataEntityWithTypeAndLabel . Implement the mimeType using the mimetype you defined in the previous step. Implement the isBlank using the contacts.core.entities.propertiesAreAllNullOrBlank function. Put the properties that you consider to be important such that if they are null, then the data is useless (blank). Define an immutable class so that instances can be returned on queries. These would also need to inherit from ExistingCustomDataEntity and ImmutableCustomDataEntityWithMutableType (or ImmutableCustomDataEntityWithNullableMutableType ). All properties and types defined here must be immutable ( val ). Define a mutable class so that instances can be updated. These would also need to inherit from ExistingCustomDataEntity . Only modifiable fields should have properties and types defined as mutable ( var ). Define a \"new\" class so that instances can be inserted. These would also need to inherit from NewCustomDataEntity . Only modifiable fields should have properties and types defined as mutable ( var ). Properties that map to your custom data fields should be nullable ( ? ). The following properties should always be immutable ( val ); id , rawContactId , contactId , isPrimary , isSuperPrimary , and isRedacted . Be mindful of what properties should be redacted when implementing the redactedCopy function. All entity class must implement Parecelable . 3. Define the fields \u00b6 Fields (or columns) represent (or map to) one of the properties you defined in the previous step. These are used in queries, inserts, and update operations. For Gender , data class GenderField internal constructor ( private val columnName : ColumnName ) : AbstractCustomDataField ( columnName ) { override val customMimeType : MimeType . Custom = GenderMimeType } object GenderFields : AbstractCustomDataFieldSet < GenderField > () { @JvmField val Type = GenderField ( ColumnName . TYPE ) @JvmField val Label = GenderField ( ColumnName . LABEL ) override val all : Set < GenderField > = setOf ( Type , Label ) override val forMatching : Set < GenderField > = emptySet () } For HandleName , data class HandleNameField internal constructor ( private val columnName : ColumnName ) : AbstractCustomDataField ( columnName ) { override val customMimeType : MimeType . Custom = HandleNameMimeType } object HandleNameFields : AbstractCustomDataFieldSet < HandleNameField > () { @JvmField val Handle = HandleNameField ( ColumnName . DATA ) override val all : Set < HandleNameField > = setOf ( Handle ) override val forMatching : Set < HandleNameField > = setOf ( Handle ) } A few things to note, You need to define a AbstractCustomDataField and a AbstractCustomDataFieldSet . Annotate your field instances with @JvmField to make it more accessible for Java users. This is only helpful if you are writing code for other people to use. Carefully choose what to put in all and forMatching . If you are using ColumnName.BLOB , do not put it in all or forMatching ! For more info, read the in-code documentation on it. 4. Implement the cursor \u00b6 Cursors read the values from the Data table and convert them into the types you want (e.g. String). For Gender , internal class GenderDataCursor ( cursor : Cursor , includeFields : Set < GenderField > ) : AbstractCustomDataCursor < GenderField > ( cursor , includeFields ) { val type : GenderEntity . Type ? by type ( GenderFields . Type , typeFromValue = GenderEntity . Type :: fromValue ) val label : String? by string ( GenderFields . Label ) } For HandleName , internal class HandleNameDataCursor ( cursor : Cursor , includeFields : Set < HandleNameField > ) : AbstractCustomDataCursor < HandleNameField > ( cursor , includeFields ) { val handle : String? by string ( HandleNameFields . Handle ) } A few things to note, Inheritors of AbstractCustomDataCursor have access to several regular and delegate functions that extract data. All of them are defined in contacts.core.entities.cursor.AbstractEntityCursor . If you are using Java, you are only able to use the regular functions. The delegate functions are prettier but use Kotlin reflection, which could slightly affect runtime performance. You can either extract nullable or non-nullable values using these functions. 5. Implement the mapper \u00b6 Mappers use the cursors implemented in the previous step in order to create instances of your custom data entities. For Gender , internal class GenderMapperFactory : AbstractCustomDataEntityMapper . Factory < GenderField , GenderDataCursor , Gender > { override fun create ( cursor : Cursor , includeFields : Set < GenderField > ): AbstractCustomDataEntityMapper < GenderField , GenderDataCursor , Gender > = GenderMapper ( GenderDataCursor ( cursor , includeFields )) } private class GenderMapper ( cursor : GenderDataCursor ) : AbstractCustomDataEntityMapper < GenderField , GenderDataCursor , Gender > ( cursor ) { override fun value ( cursor : GenderDataCursor ) = Gender ( id = cursor . dataId , rawContactId = cursor . rawContactId , contactId = cursor . contactId , isPrimary = cursor . isPrimary , isSuperPrimary = cursor . isSuperPrimary , type = cursor . type , label = cursor . label , isRedacted = false ) } For HandleName , internal class HandleNameMapperFactory : AbstractCustomDataEntityMapper . Factory < HandleNameField , HandleNameDataCursor , HandleName > { override fun create ( cursor : Cursor , includeFields : Set < HandleNameField > ): AbstractCustomDataEntityMapper < HandleNameField , HandleNameDataCursor , HandleName > = HandleNameMapper ( HandleNameDataCursor ( cursor , includeFields )) } private class HandleNameMapper ( cursor : HandleNameDataCursor ) : AbstractCustomDataEntityMapper < HandleNameField , HandleNameDataCursor , HandleName > ( cursor ) { override fun value ( cursor : HandleNameDataCursor ) = HandleName ( id = cursor . dataId , rawContactId = cursor . rawContactId , contactId = cursor . contactId , isPrimary = cursor . isPrimary , isSuperPrimary = cursor . isSuperPrimary , handle = cursor . handle , isRedacted = false ) } A few things to note, This requires definitions and implementations done in the previous steps. If you are having compile-time issues at this point, make sure that you did not skip a step! Ensure that isRedacted is set to false (unless you are already performing the redaction) here. 6. Implement the operation \u00b6 Operations are used for inserts and updates from in-memory instances of your entities to the database. For Gender , internal class GenderOperationFactory : AbstractCustomDataOperation . Factory < GenderField , GenderEntity > { override fun create ( isProfile : Boolean , includeFields : Set < GenderField > ): AbstractCustomDataOperation < GenderField , GenderEntity > = GenderOperation ( isProfile , includeFields ) } private class GenderOperation ( isProfile : Boolean , includeFields : Set < GenderField > ) : AbstractCustomDataOperation < GenderField , GenderEntity > ( isProfile , includeFields ) { override val mimeType : MimeType . Custom = GenderMimeType override fun setCustomData ( data : GenderEntity , setValue : ( field : GenderField , value : Any? ) -> Unit ) { setValue ( GenderFields . Type , data . type ?. value ) setValue ( GenderFields . Label , data . label ) } } For HandleName , internal class HandleNameOperationFactory : AbstractCustomDataOperation . Factory < HandleNameField , HandleNameEntity > { override fun create ( isProfile : Boolean , includeFields : Set < HandleNameField > ): AbstractCustomDataOperation < HandleNameField , HandleNameEntity > = HandleNameOperation ( isProfile , includeFields ) } private class HandleNameOperation ( isProfile : Boolean , includeFields : Set < HandleNameField > ) : AbstractCustomDataOperation < HandleNameField , HandleNameEntity > ( isProfile , includeFields ) { override val mimeType : MimeType . Custom = HandleNameMimeType override fun setCustomData ( data : HandleNameEntity , setValue : ( field : HandleNameField , value : Any? ) -> Unit ) { setValue ( HandleNameFields . Handle , data . handle ) } } A few things to note, You just need to use your custom data fields and the corresponding data property it maps to in the setValue function provided in the setCustomData function. 7. Define the count restriction \u00b6 The count restriction defines whether a RawContact can have 0 or 1 of your custom data or if it can have 0, 1, or more. For Gender , /** * A RawContact may have at most 1 gender. */ internal val GENDER_COUNT_RESTRICTION = CustomDataCountRestriction . AT_MOST_ONE For HandleName , /** * A RawContact may have 0, 1, or more handle names. */ internal val HANDLE_NAME_COUNT_RESTRICTION = CustomDataCountRestriction . NO_LIMIT 8. Define RawContact getters and setters \u00b6 In order for you or your consumers to be able to get and set your custom data in instances of RawContacts they belong to, you must define a set of getters and setters. Due to the length of the RawContactGender.kt and RawContactHandleName.kt files, I will not be copy-pasting them here. Please take a look at those files instead. A few things to note, For getters, use the Contacts.customDataRegistry.customDataEntitiesFor function to extract the custom data instance(s) for the RawContact with your custom mimetype. Consider returning Sequence for the getters for optimizations in Kotlin. For setters use, the Contacts.customDataRegistry.putCustomDataEntityInto function to set the custom data instance into the RawContact. the Contacts.customDataRegistry.removeAllCustomDataEntityFrom function to remove the custom data instance from the RawContact. Define getters and setters for RawContact , MutableRawContact , and NewRawContact . Ensure to match the type of RawContact with the type of the custom data. For example, RawContact -> Gender , HandleName MutableRawContact -> MutableGenderEntity , MutableHandleNameEntity When setting/adding a new custom data entity, MutableRawContact -> NewGender , NewHandleName NewRawContact -> NewGender , NewHandleName Setters for custom data with count restriction of AT_MOST_ONE should use setXXX for the function name. Setters for custom data with count restriction of NO_LIMIT should use addXXX and removeXXX for the function names. 9. Define Contact getters and setters \u00b6 Defining getters and setters for RawContacts is the bare minimum. However, if you want to add some convenience functions so that you can access RawContact getters and setters from a Contact, then you are free (and recommended) to do so. Due to the length of the ContactGender.kt and ContactHandleName.kt files, I will not be copy-pasting them here. Please take a look at those files instead. A few things to note, For getters, consider returning Sequence for optimizations in Kotlin. For setters, use the first RawContact (in case there are more than one). Consider returning Sequence for the getters for optimizations in Kotlin. Define getters and setters for Contact and MutableContact . Ensure to match the type of Contact with the type of the custom data. For example, Contact -> Gender , HandleName MutableContact -> MutableGenderEntity , MutableHandleNameEntity When setting/adding a new custom data entity, MutableContact -> NewGender , NewHandleName 10. Define exceptions \u00b6 Whether you are building this custom data just for your own app or for others to use, it is useful to define a subclass of CustomDataException to help identify errors in certain custom data integrations. For Gender , class GenderDataException ( message : String ) : CustomDataException ( message ) For HandleName , class HandleNameDataException ( message : String ) : CustomDataException ( message ) 11. Implement the field mapper \u00b6 A field mapper maps your custom data field to the corresponding property in your custom data entity. For Gender , internal class GenderFieldMapper : CustomDataFieldMapper < GenderField , GenderEntity > { override fun valueOf ( field : GenderField , customDataEntity : GenderEntity ): String? = when ( field ) { GenderFields . Type -> customDataEntity . type ?. ordinal ?. toString () GenderFields . Label -> customDataEntity . label else -> throw GenderDataException ( \"Unrecognized gender field $ field \" ) } } For HandleName , internal class HandleNameFieldMapper : CustomDataFieldMapper < HandleNameField , HandleNameEntity > { override fun valueOf ( field : HandleNameField , customDataEntity : HandleNameEntity ): String? = when ( field ) { HandleNameFields . Handle -> customDataEntity . handle else -> throw HandleNameDataException ( \"Unrecognized handle name field $ field \" ) } } A few things to note, You should throw an instance of your custom data exception in the case that there is no mapping from a field to a property. This ensures that your custom data integration will fail and fail-fast in case you forget to add a mapping to a property. 12. Define the data query function \u00b6 These (extension) functions on the DataQueryFactory allows you and your consumers to use the DataQuery API to specifically query for only your custom data kind instead of Contacts. For Gender , fun DataQueryFactory . genders (): DataQuery < GenderField , GenderFields , Gender > = customData ( GenderMimeType ) For HandleName , fun DataQueryFactory . handleNames (): DataQuery < HandleNameField , HandleNameFields , HandleName > = customData ( HandleNameMimeType ) For more info on the DataQuery API, read Query specific data kinds and Query custom data . 13. Define the custom data entry \u00b6 The entry puts everything together so that it can be handed off to the custom data registry to integrate your custom data with all of the APIs provided in the library. For Gender , internal class GenderEntry : Entry < GenderField , GenderDataCursor , GenderEntity , Gender > { override val mimeType = GenderMimeType override val fieldSet = GenderFields override val fieldMapper = GenderFieldMapper () override val countRestriction = GENDER_COUNT_RESTRICTION override val mapperFactory = GenderMapperFactory () override val operationFactory = GenderOperationFactory () } For HandleName , internal class HandleNameEntry : Entry < HandleNameField , HandleNameDataCursor , HandleNameEntity , HandleName > { override val mimeType = HandleNameMimeType override val fieldSet = HandleNameFields override val fieldMapper = HandleNameFieldMapper () override val countRestriction = HANDLE_NAME_COUNT_RESTRICTION override val mapperFactory = HandleNameMapperFactory () override val operationFactory = HandleNameOperationFactory () } 14. Define the custom data entry registration \u00b6 The entry registration provides a way for you to keep your Entry internal to your library module. In Java, the closes thing to this is package-private. This is not necessary to implement. Feel free to make your Entry public so that it can be handed off to the custom data registry. For Gender , class GenderRegistration : CustomDataRegistry . EntryRegistration { override fun registerTo ( customDataRegistry : CustomDataRegistry ) { customDataRegistry . register ( GenderEntry ()) } } For HandleName , class HandleNameRegistration : CustomDataRegistry . EntryRegistration { override fun registerTo ( customDataRegistry : CustomDataRegistry ) { customDataRegistry . register ( HandleNameEntry ()) } } 15. Register your custom data with the Contacts API instance \u00b6 There are two ways to register your custom data. Either using the entry registration defined in the previous step or the entry itself defined in the step prior. Using Gender and HandleName entry registration, val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( GenderRegistration (), HandleNameRegistration () ) ) Alternatively, val contactsApi = Contacts ( context ) GenderRegistration (). registerTo ( contactsApi . customDataRegistry ) HandleNameRegistration (). registerTo ( contactsApi . customDataRegistry ) Using Gender and HandleName entry, Note that this is not possible with Gender and HandleName as their entries are internal. This is for demonstration purposes only. val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( GenderEntry (), HandleNameEntry () ) ) 16. Use your custom data in queries, inserts, updates, and deletes \u00b6 Once you have registered your custom data with the Contacts API instance, the API instance is now able to perform read and write operations on it. Query custom data Insert custom data into new or existing contacts Update custom data Delete custom data Consider adding your custom data to this library \u00b6 Let's say that you have created your own custom data in your own app. That's great and all but your app will be the only app that will be able to perform operations on it (unless the mimetype value you are using is also used by others). This is definitely something you want to do if you don't really want others to mess with your custom data (though you can't really stop others). If you want to add your custom data to this library so that other people using this library can optionally integrate it into their own apps, please create a GitHub issue and file a pull request! Custom data without sync adapters will not be synced \u00b6 Custom data provided by this library such as those in those in the customdata-gender , customdata-handlename , customdata-pokemon , and customdata-rpg modules are not synced because there are no sync adapters and a remote service to store those data. Therefore, they are not synced across devices and will remain local to the device regardless of Account sync settings. It is up to you to implement your own sync adapters for your own custom data. For more info, read Sync contact data across devices . Displaying your custom data in other Contacts apps \u00b6 If you want your custom data to be visible in the Android Open Source Project (AOSP) Contacts app (the default Contacts app that comes with a vanilla version of Android) and the Google Contacts app , then read this section. This is optional. If you only want your custom data to be visible in your application, then you should NOT do the things described in this part of the guide. Note that the Google Contacts app keeps its \"File as\" custom data invisible to other Contacts apps such as the AOSP Contacts app. However, it exposes the \"Custom field+label\" custom data by doing the things described in this section. Important! The first criteria for being able to show your custom data in the Contacts app is to define and implement your own sync adapter. If you do not have a sync adapter implementation, your custom data will not be shown in the Contacts app! Again, this library does not provide any sync adapters. That is for you to implement based on your account services. This library provides you and users of your library an easy, uniform way to perform read and write operations on your custom data. The act of syncing is up to you. The official documentation on custom data rows is as follows, By creating and using your own custom MIME types, you can insert, edit, delete, and retrieve your own data rows in the ContactsContract.Data table. Your rows are limited to using the column defined in ContactsContract.DataColumns , although you can map your own type-specific column names to the default column names. In the device's contacts application, the data for your rows is displayed but can't be edited or deleted, and users can't add additional data. To allow users to modify your custom data rows, you must provide an editor activity in your own application. To display your custom data, provide a contacts.xml file containing a element and one or more of its child elements. This is described in more detail in the section element. Let's break down the official documentation. Contacts applications such as the Android Open Source Project (AOSP) Contacts app (the default Contacts app that comes with a vanilla version of Android) and the Google Contacts app (and other Contacts app that support this feature) shows custom data from other apps when viewing contact details. Custom data from other apps are viewable but not editable in order to preserve and respect the rules surrounding those custom data managed by other apps. This library allows you to read (query) and write (insert, update, delete) custom data from other apps. It is up to you whether you want to follow the same limitations imposed by the AOSP and Google Contacts app. In order to show your custom data in the AOSP Contacts app and Google Contacts app (and other Contacts app that support this feature), you must add an xml file in your app; res/xml/contacts.xml . The res/xml/contacts.xml template looks like this, The full official documentation for each of those tags and attributes within each tag are available by clicking this link . For example, the bare-minimum contacts.xml for showing Gender and HandleName custom data in the AOSP and Google Contacts app is the following, A few things to note, The value of android:mimeType corresponds to the String value defined in GenderMimeType and HandleNameMimeType as seen in the previous sections of this guide. The value of android:summaryColumn and android:detailColumn corresponds to the values defined in contacts.core.Fields.kt#AbstractCustomDataField.ColumnName that are used by GenderFields and HandleNameFields . These values, as raw strings, are; data1 , data2 , data3 ,... data15 Again, in order for your custom data to be shown in the Contacts app, you must also provide a sync adapter implementation. For more info, read Sync contact data across devices . Summary of limitations \u00b6 To reiterate, this library does not provide a remote server or sync adapters to interface with that server. This library provides create (insert), read (query), update, and delete (CRUD) APIs for pretty, type-safe, and well-documented read and write operations on all data kinds, including custom data. This means that if you do not implement your own sync adapter for your custom data, then your custom data... will NOT be synced across devices will NOT be shown in AOSP and Google Contacts apps, and other Contacts apps that show custom data from other apps You may still do creative things with custom data without sync adapters as long as you understand these limitations. This library provides CRUD API integration with custom data with no sync adapters; customdata-gender customdata-handlename customdata-pokemon customdata-rpg Also provided are CRUD API integration with custom data from other apps that do have sync adapters; customdata-googlecontacts Please update the above list whenever adding new custom data modules.","title":"Integrate custom data"},{"location":"customdata/integrate-custom-data/#integrate-custom-data","text":"If you are looking to integrate custom data from other apps, read Integrate custom data from other apps . If you are looking to create and integrate your own custom data, you are in the right place! There are two parts to \"integrating custom data\"; Providing create (insert), read (query), update, and delete (CRUD) APIs for custom data associated with RawContacts. Providing sync adapters to sync custom data across devices. This library only handles the first part. If you want to sync your custom data, then you need to implement a sync adapter to interface with your remote server. That is out of scope of this library. In order to create and integrate your own custom data for use in your own apps, there is a bit of boilerplate code that needs to be written. Thankfully none of this stuff is difficult! Here are the steps, in chronological order, on how to define and use your own custom data, Define the mimetype Define the entities Define the fields Implement the cursor Implement the mapper Implement the operation Define the count restriction Define RawContact getters and setters Define Contact getters and setters Define exceptions Implement the field mapper Define the data query function Define the custom data entry Define the custom data entry registration Register your custom data with the Contacts API instance Use your custom data in queries, inserts, updates, and deletes Maybe someday someone with code generation experience (or I'll learn how to do it), will create annotations and annotation processors to eliminate having to manually write this stuff =) To help illustrate the above steps, we'll use the HandleName and Gender custom data provided in this library's customdata-handlename and customdata-gender respectively as an example. For more specifics on these custom data, read Integrate the gender custom data and Integrate the handle name custom data . At the bottom of this page, we'll also discuss, Consider adding your custom data to this library Custom data without sync adapters will not be synced Displaying your custom data in other Contacts apps Summary of limitations Some of the code used in these examples are in Kotlin. If you would like a Java version of this page, create an issue in GitHub. You are also free to file a pull request with your own page. In the event that a Java version of this page is created, this quote block should be replaced with a link to that page.","title":"Integrate custom data"},{"location":"customdata/integrate-custom-data/#1-define-the-mimetype","text":"The mimetype is a string that describes what kind of data a row in the Data table represents. For Gender , internal object GenderMimeType : MimeType . Custom () { // Following Contacts Provider convention of \"vnd.android.cursor.item/.\" override val value : String = \"vnd.android.cursor.item/contacts.entities.custom.gender\" } For HandleName , internal object HandleNameMimeType : MimeType . Custom () { // Following Contacts Provider convention of \"vnd.android.cursor.item/.\" override val value : String = \"vnd.android.cursor.item/contacts.entities.custom.handlename\" } Do not change the mimetype value! If you have already deployed apps to production that use these mimetype values, then changing them could result in \"data loss\". Old rows in the Data table will not be compatible if the mimetype value changes. You can certainly perform migrations by creating a new custom data altogether and migrating your old custom data to your new one. Do not use built-in mimetypes! The Contacts Provider has predefined the mimetypes for all of the common data kinds it supports (e.g. email). Make sure that your custom data does not use any of those. You can take a look at built-in mimetypes in contacs.core.entities.MimeType.kt . But, here they are for your convenience =) Builtin data kind mimetype Address \"vnd.android.cursor.item/postal-address_v2\" Email \"vnd.android.cursor.item/email_v2\" Event \"vnd.android.cursor.item/contact_event\" GroupMembership \"vnd.android.cursor.item/group_membership\" Im \"vnd.android.cursor.item/im\" Name \"vnd.android.cursor.item/name\" Nickname \"vnd.android.cursor.item/nickname\" Note \"vnd.android.cursor.item/note\" Organization \"vnd.android.cursor.item/organization\" Phone \"vnd.android.cursor.item/phone_v2\" Photo \"vnd.android.cursor.item/photo\" Relation \"vnd.android.cursor.item/relation\" SipAddress \"vnd.android.cursor.item/sip_address\" Website \"vnd.android.cursor.item/website\"","title":"1. Define the mimetype"},{"location":"customdata/integrate-custom-data/#2-define-the-entities","text":"The entities are the main code that users of your custom data will be exposed to. The properties model/represent the fields/columns in the Data table. Due to the length of the Gender.kt and HandleName.kt files, I will not be copy-pasting them here. Please take a look at those files instead. A few things to note, Either inherit from CustomDataEntity or CustomDataEntityWithTypeAndLabel . Implement the mimeType using the mimetype you defined in the previous step. Implement the isBlank using the contacts.core.entities.propertiesAreAllNullOrBlank function. Put the properties that you consider to be important such that if they are null, then the data is useless (blank). Define an immutable class so that instances can be returned on queries. These would also need to inherit from ExistingCustomDataEntity and ImmutableCustomDataEntityWithMutableType (or ImmutableCustomDataEntityWithNullableMutableType ). All properties and types defined here must be immutable ( val ). Define a mutable class so that instances can be updated. These would also need to inherit from ExistingCustomDataEntity . Only modifiable fields should have properties and types defined as mutable ( var ). Define a \"new\" class so that instances can be inserted. These would also need to inherit from NewCustomDataEntity . Only modifiable fields should have properties and types defined as mutable ( var ). Properties that map to your custom data fields should be nullable ( ? ). The following properties should always be immutable ( val ); id , rawContactId , contactId , isPrimary , isSuperPrimary , and isRedacted . Be mindful of what properties should be redacted when implementing the redactedCopy function. All entity class must implement Parecelable .","title":"2. Define the entities"},{"location":"customdata/integrate-custom-data/#3-define-the-fields","text":"Fields (or columns) represent (or map to) one of the properties you defined in the previous step. These are used in queries, inserts, and update operations. For Gender , data class GenderField internal constructor ( private val columnName : ColumnName ) : AbstractCustomDataField ( columnName ) { override val customMimeType : MimeType . Custom = GenderMimeType } object GenderFields : AbstractCustomDataFieldSet < GenderField > () { @JvmField val Type = GenderField ( ColumnName . TYPE ) @JvmField val Label = GenderField ( ColumnName . LABEL ) override val all : Set < GenderField > = setOf ( Type , Label ) override val forMatching : Set < GenderField > = emptySet () } For HandleName , data class HandleNameField internal constructor ( private val columnName : ColumnName ) : AbstractCustomDataField ( columnName ) { override val customMimeType : MimeType . Custom = HandleNameMimeType } object HandleNameFields : AbstractCustomDataFieldSet < HandleNameField > () { @JvmField val Handle = HandleNameField ( ColumnName . DATA ) override val all : Set < HandleNameField > = setOf ( Handle ) override val forMatching : Set < HandleNameField > = setOf ( Handle ) } A few things to note, You need to define a AbstractCustomDataField and a AbstractCustomDataFieldSet . Annotate your field instances with @JvmField to make it more accessible for Java users. This is only helpful if you are writing code for other people to use. Carefully choose what to put in all and forMatching . If you are using ColumnName.BLOB , do not put it in all or forMatching ! For more info, read the in-code documentation on it.","title":"3. Define the fields"},{"location":"customdata/integrate-custom-data/#4-implement-the-cursor","text":"Cursors read the values from the Data table and convert them into the types you want (e.g. String). For Gender , internal class GenderDataCursor ( cursor : Cursor , includeFields : Set < GenderField > ) : AbstractCustomDataCursor < GenderField > ( cursor , includeFields ) { val type : GenderEntity . Type ? by type ( GenderFields . Type , typeFromValue = GenderEntity . Type :: fromValue ) val label : String? by string ( GenderFields . Label ) } For HandleName , internal class HandleNameDataCursor ( cursor : Cursor , includeFields : Set < HandleNameField > ) : AbstractCustomDataCursor < HandleNameField > ( cursor , includeFields ) { val handle : String? by string ( HandleNameFields . Handle ) } A few things to note, Inheritors of AbstractCustomDataCursor have access to several regular and delegate functions that extract data. All of them are defined in contacts.core.entities.cursor.AbstractEntityCursor . If you are using Java, you are only able to use the regular functions. The delegate functions are prettier but use Kotlin reflection, which could slightly affect runtime performance. You can either extract nullable or non-nullable values using these functions.","title":"4. Implement the cursor"},{"location":"customdata/integrate-custom-data/#5-implement-the-mapper","text":"Mappers use the cursors implemented in the previous step in order to create instances of your custom data entities. For Gender , internal class GenderMapperFactory : AbstractCustomDataEntityMapper . Factory < GenderField , GenderDataCursor , Gender > { override fun create ( cursor : Cursor , includeFields : Set < GenderField > ): AbstractCustomDataEntityMapper < GenderField , GenderDataCursor , Gender > = GenderMapper ( GenderDataCursor ( cursor , includeFields )) } private class GenderMapper ( cursor : GenderDataCursor ) : AbstractCustomDataEntityMapper < GenderField , GenderDataCursor , Gender > ( cursor ) { override fun value ( cursor : GenderDataCursor ) = Gender ( id = cursor . dataId , rawContactId = cursor . rawContactId , contactId = cursor . contactId , isPrimary = cursor . isPrimary , isSuperPrimary = cursor . isSuperPrimary , type = cursor . type , label = cursor . label , isRedacted = false ) } For HandleName , internal class HandleNameMapperFactory : AbstractCustomDataEntityMapper . Factory < HandleNameField , HandleNameDataCursor , HandleName > { override fun create ( cursor : Cursor , includeFields : Set < HandleNameField > ): AbstractCustomDataEntityMapper < HandleNameField , HandleNameDataCursor , HandleName > = HandleNameMapper ( HandleNameDataCursor ( cursor , includeFields )) } private class HandleNameMapper ( cursor : HandleNameDataCursor ) : AbstractCustomDataEntityMapper < HandleNameField , HandleNameDataCursor , HandleName > ( cursor ) { override fun value ( cursor : HandleNameDataCursor ) = HandleName ( id = cursor . dataId , rawContactId = cursor . rawContactId , contactId = cursor . contactId , isPrimary = cursor . isPrimary , isSuperPrimary = cursor . isSuperPrimary , handle = cursor . handle , isRedacted = false ) } A few things to note, This requires definitions and implementations done in the previous steps. If you are having compile-time issues at this point, make sure that you did not skip a step! Ensure that isRedacted is set to false (unless you are already performing the redaction) here.","title":"5. Implement the mapper"},{"location":"customdata/integrate-custom-data/#6-implement-the-operation","text":"Operations are used for inserts and updates from in-memory instances of your entities to the database. For Gender , internal class GenderOperationFactory : AbstractCustomDataOperation . Factory < GenderField , GenderEntity > { override fun create ( isProfile : Boolean , includeFields : Set < GenderField > ): AbstractCustomDataOperation < GenderField , GenderEntity > = GenderOperation ( isProfile , includeFields ) } private class GenderOperation ( isProfile : Boolean , includeFields : Set < GenderField > ) : AbstractCustomDataOperation < GenderField , GenderEntity > ( isProfile , includeFields ) { override val mimeType : MimeType . Custom = GenderMimeType override fun setCustomData ( data : GenderEntity , setValue : ( field : GenderField , value : Any? ) -> Unit ) { setValue ( GenderFields . Type , data . type ?. value ) setValue ( GenderFields . Label , data . label ) } } For HandleName , internal class HandleNameOperationFactory : AbstractCustomDataOperation . Factory < HandleNameField , HandleNameEntity > { override fun create ( isProfile : Boolean , includeFields : Set < HandleNameField > ): AbstractCustomDataOperation < HandleNameField , HandleNameEntity > = HandleNameOperation ( isProfile , includeFields ) } private class HandleNameOperation ( isProfile : Boolean , includeFields : Set < HandleNameField > ) : AbstractCustomDataOperation < HandleNameField , HandleNameEntity > ( isProfile , includeFields ) { override val mimeType : MimeType . Custom = HandleNameMimeType override fun setCustomData ( data : HandleNameEntity , setValue : ( field : HandleNameField , value : Any? ) -> Unit ) { setValue ( HandleNameFields . Handle , data . handle ) } } A few things to note, You just need to use your custom data fields and the corresponding data property it maps to in the setValue function provided in the setCustomData function.","title":"6. Implement the operation"},{"location":"customdata/integrate-custom-data/#7-define-the-count-restriction","text":"The count restriction defines whether a RawContact can have 0 or 1 of your custom data or if it can have 0, 1, or more. For Gender , /** * A RawContact may have at most 1 gender. */ internal val GENDER_COUNT_RESTRICTION = CustomDataCountRestriction . AT_MOST_ONE For HandleName , /** * A RawContact may have 0, 1, or more handle names. */ internal val HANDLE_NAME_COUNT_RESTRICTION = CustomDataCountRestriction . NO_LIMIT","title":"7. Define the count restriction"},{"location":"customdata/integrate-custom-data/#8-define-rawcontact-getters-and-setters","text":"In order for you or your consumers to be able to get and set your custom data in instances of RawContacts they belong to, you must define a set of getters and setters. Due to the length of the RawContactGender.kt and RawContactHandleName.kt files, I will not be copy-pasting them here. Please take a look at those files instead. A few things to note, For getters, use the Contacts.customDataRegistry.customDataEntitiesFor function to extract the custom data instance(s) for the RawContact with your custom mimetype. Consider returning Sequence for the getters for optimizations in Kotlin. For setters use, the Contacts.customDataRegistry.putCustomDataEntityInto function to set the custom data instance into the RawContact. the Contacts.customDataRegistry.removeAllCustomDataEntityFrom function to remove the custom data instance from the RawContact. Define getters and setters for RawContact , MutableRawContact , and NewRawContact . Ensure to match the type of RawContact with the type of the custom data. For example, RawContact -> Gender , HandleName MutableRawContact -> MutableGenderEntity , MutableHandleNameEntity When setting/adding a new custom data entity, MutableRawContact -> NewGender , NewHandleName NewRawContact -> NewGender , NewHandleName Setters for custom data with count restriction of AT_MOST_ONE should use setXXX for the function name. Setters for custom data with count restriction of NO_LIMIT should use addXXX and removeXXX for the function names.","title":"8. Define RawContact getters and setters"},{"location":"customdata/integrate-custom-data/#9-define-contact-getters-and-setters","text":"Defining getters and setters for RawContacts is the bare minimum. However, if you want to add some convenience functions so that you can access RawContact getters and setters from a Contact, then you are free (and recommended) to do so. Due to the length of the ContactGender.kt and ContactHandleName.kt files, I will not be copy-pasting them here. Please take a look at those files instead. A few things to note, For getters, consider returning Sequence for optimizations in Kotlin. For setters, use the first RawContact (in case there are more than one). Consider returning Sequence for the getters for optimizations in Kotlin. Define getters and setters for Contact and MutableContact . Ensure to match the type of Contact with the type of the custom data. For example, Contact -> Gender , HandleName MutableContact -> MutableGenderEntity , MutableHandleNameEntity When setting/adding a new custom data entity, MutableContact -> NewGender , NewHandleName","title":"9. Define Contact getters and setters"},{"location":"customdata/integrate-custom-data/#10-define-exceptions","text":"Whether you are building this custom data just for your own app or for others to use, it is useful to define a subclass of CustomDataException to help identify errors in certain custom data integrations. For Gender , class GenderDataException ( message : String ) : CustomDataException ( message ) For HandleName , class HandleNameDataException ( message : String ) : CustomDataException ( message )","title":"10. Define exceptions"},{"location":"customdata/integrate-custom-data/#11-implement-the-field-mapper","text":"A field mapper maps your custom data field to the corresponding property in your custom data entity. For Gender , internal class GenderFieldMapper : CustomDataFieldMapper < GenderField , GenderEntity > { override fun valueOf ( field : GenderField , customDataEntity : GenderEntity ): String? = when ( field ) { GenderFields . Type -> customDataEntity . type ?. ordinal ?. toString () GenderFields . Label -> customDataEntity . label else -> throw GenderDataException ( \"Unrecognized gender field $ field \" ) } } For HandleName , internal class HandleNameFieldMapper : CustomDataFieldMapper < HandleNameField , HandleNameEntity > { override fun valueOf ( field : HandleNameField , customDataEntity : HandleNameEntity ): String? = when ( field ) { HandleNameFields . Handle -> customDataEntity . handle else -> throw HandleNameDataException ( \"Unrecognized handle name field $ field \" ) } } A few things to note, You should throw an instance of your custom data exception in the case that there is no mapping from a field to a property. This ensures that your custom data integration will fail and fail-fast in case you forget to add a mapping to a property.","title":"11. Implement the field mapper"},{"location":"customdata/integrate-custom-data/#12-define-the-data-query-function","text":"These (extension) functions on the DataQueryFactory allows you and your consumers to use the DataQuery API to specifically query for only your custom data kind instead of Contacts. For Gender , fun DataQueryFactory . genders (): DataQuery < GenderField , GenderFields , Gender > = customData ( GenderMimeType ) For HandleName , fun DataQueryFactory . handleNames (): DataQuery < HandleNameField , HandleNameFields , HandleName > = customData ( HandleNameMimeType ) For more info on the DataQuery API, read Query specific data kinds and Query custom data .","title":"12. Define the data query function"},{"location":"customdata/integrate-custom-data/#13-define-the-custom-data-entry","text":"The entry puts everything together so that it can be handed off to the custom data registry to integrate your custom data with all of the APIs provided in the library. For Gender , internal class GenderEntry : Entry < GenderField , GenderDataCursor , GenderEntity , Gender > { override val mimeType = GenderMimeType override val fieldSet = GenderFields override val fieldMapper = GenderFieldMapper () override val countRestriction = GENDER_COUNT_RESTRICTION override val mapperFactory = GenderMapperFactory () override val operationFactory = GenderOperationFactory () } For HandleName , internal class HandleNameEntry : Entry < HandleNameField , HandleNameDataCursor , HandleNameEntity , HandleName > { override val mimeType = HandleNameMimeType override val fieldSet = HandleNameFields override val fieldMapper = HandleNameFieldMapper () override val countRestriction = HANDLE_NAME_COUNT_RESTRICTION override val mapperFactory = HandleNameMapperFactory () override val operationFactory = HandleNameOperationFactory () }","title":"13. Define the custom data entry"},{"location":"customdata/integrate-custom-data/#14-define-the-custom-data-entry-registration","text":"The entry registration provides a way for you to keep your Entry internal to your library module. In Java, the closes thing to this is package-private. This is not necessary to implement. Feel free to make your Entry public so that it can be handed off to the custom data registry. For Gender , class GenderRegistration : CustomDataRegistry . EntryRegistration { override fun registerTo ( customDataRegistry : CustomDataRegistry ) { customDataRegistry . register ( GenderEntry ()) } } For HandleName , class HandleNameRegistration : CustomDataRegistry . EntryRegistration { override fun registerTo ( customDataRegistry : CustomDataRegistry ) { customDataRegistry . register ( HandleNameEntry ()) } }","title":"14. Define the custom data entry registration"},{"location":"customdata/integrate-custom-data/#15-register-your-custom-data-with-the-contacts-api-instance","text":"There are two ways to register your custom data. Either using the entry registration defined in the previous step or the entry itself defined in the step prior. Using Gender and HandleName entry registration, val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( GenderRegistration (), HandleNameRegistration () ) ) Alternatively, val contactsApi = Contacts ( context ) GenderRegistration (). registerTo ( contactsApi . customDataRegistry ) HandleNameRegistration (). registerTo ( contactsApi . customDataRegistry ) Using Gender and HandleName entry, Note that this is not possible with Gender and HandleName as their entries are internal. This is for demonstration purposes only. val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( GenderEntry (), HandleNameEntry () ) )","title":"15. Register your custom data with the Contacts API instance"},{"location":"customdata/integrate-custom-data/#16-use-your-custom-data-in-queries-inserts-updates-and-deletes","text":"Once you have registered your custom data with the Contacts API instance, the API instance is now able to perform read and write operations on it. Query custom data Insert custom data into new or existing contacts Update custom data Delete custom data","title":"16. Use your custom data in queries, inserts, updates, and deletes"},{"location":"customdata/integrate-custom-data/#consider-adding-your-custom-data-to-this-library","text":"Let's say that you have created your own custom data in your own app. That's great and all but your app will be the only app that will be able to perform operations on it (unless the mimetype value you are using is also used by others). This is definitely something you want to do if you don't really want others to mess with your custom data (though you can't really stop others). If you want to add your custom data to this library so that other people using this library can optionally integrate it into their own apps, please create a GitHub issue and file a pull request!","title":"Consider adding your custom data to this library"},{"location":"customdata/integrate-custom-data/#custom-data-without-sync-adapters-will-not-be-synced","text":"Custom data provided by this library such as those in those in the customdata-gender , customdata-handlename , customdata-pokemon , and customdata-rpg modules are not synced because there are no sync adapters and a remote service to store those data. Therefore, they are not synced across devices and will remain local to the device regardless of Account sync settings. It is up to you to implement your own sync adapters for your own custom data. For more info, read Sync contact data across devices .","title":"Custom data without sync adapters will not be synced"},{"location":"customdata/integrate-custom-data/#displaying-your-custom-data-in-other-contacts-apps","text":"If you want your custom data to be visible in the Android Open Source Project (AOSP) Contacts app (the default Contacts app that comes with a vanilla version of Android) and the Google Contacts app , then read this section. This is optional. If you only want your custom data to be visible in your application, then you should NOT do the things described in this part of the guide. Note that the Google Contacts app keeps its \"File as\" custom data invisible to other Contacts apps such as the AOSP Contacts app. However, it exposes the \"Custom field+label\" custom data by doing the things described in this section. Important! The first criteria for being able to show your custom data in the Contacts app is to define and implement your own sync adapter. If you do not have a sync adapter implementation, your custom data will not be shown in the Contacts app! Again, this library does not provide any sync adapters. That is for you to implement based on your account services. This library provides you and users of your library an easy, uniform way to perform read and write operations on your custom data. The act of syncing is up to you. The official documentation on custom data rows is as follows, By creating and using your own custom MIME types, you can insert, edit, delete, and retrieve your own data rows in the ContactsContract.Data table. Your rows are limited to using the column defined in ContactsContract.DataColumns , although you can map your own type-specific column names to the default column names. In the device's contacts application, the data for your rows is displayed but can't be edited or deleted, and users can't add additional data. To allow users to modify your custom data rows, you must provide an editor activity in your own application. To display your custom data, provide a contacts.xml file containing a element and one or more of its child elements. This is described in more detail in the section element. Let's break down the official documentation. Contacts applications such as the Android Open Source Project (AOSP) Contacts app (the default Contacts app that comes with a vanilla version of Android) and the Google Contacts app (and other Contacts app that support this feature) shows custom data from other apps when viewing contact details. Custom data from other apps are viewable but not editable in order to preserve and respect the rules surrounding those custom data managed by other apps. This library allows you to read (query) and write (insert, update, delete) custom data from other apps. It is up to you whether you want to follow the same limitations imposed by the AOSP and Google Contacts app. In order to show your custom data in the AOSP Contacts app and Google Contacts app (and other Contacts app that support this feature), you must add an xml file in your app; res/xml/contacts.xml . The res/xml/contacts.xml template looks like this, The full official documentation for each of those tags and attributes within each tag are available by clicking this link . For example, the bare-minimum contacts.xml for showing Gender and HandleName custom data in the AOSP and Google Contacts app is the following, A few things to note, The value of android:mimeType corresponds to the String value defined in GenderMimeType and HandleNameMimeType as seen in the previous sections of this guide. The value of android:summaryColumn and android:detailColumn corresponds to the values defined in contacts.core.Fields.kt#AbstractCustomDataField.ColumnName that are used by GenderFields and HandleNameFields . These values, as raw strings, are; data1 , data2 , data3 ,... data15 Again, in order for your custom data to be shown in the Contacts app, you must also provide a sync adapter implementation. For more info, read Sync contact data across devices .","title":"Displaying your custom data in other Contacts apps"},{"location":"customdata/integrate-custom-data/#summary-of-limitations","text":"To reiterate, this library does not provide a remote server or sync adapters to interface with that server. This library provides create (insert), read (query), update, and delete (CRUD) APIs for pretty, type-safe, and well-documented read and write operations on all data kinds, including custom data. This means that if you do not implement your own sync adapter for your custom data, then your custom data... will NOT be synced across devices will NOT be shown in AOSP and Google Contacts apps, and other Contacts apps that show custom data from other apps You may still do creative things with custom data without sync adapters as long as you understand these limitations. This library provides CRUD API integration with custom data with no sync adapters; customdata-gender customdata-handlename customdata-pokemon customdata-rpg Also provided are CRUD API integration with custom data from other apps that do have sync adapters; customdata-googlecontacts Please update the above list whenever adding new custom data modules.","title":"Summary of limitations"},{"location":"customdata/integrate-gender-custom-data/","text":"Integrate the gender custom data \u00b6 This library provides extensions for Gender custom data that allows you to read and write gender data for all of your contacts. These (optional) extensions live in the customdata-gender module. If you are looking to create your own custom data or get more insight on how the Gender custom data was built, read Integrate custom data . Register the gender custom data with the Contacts API instance \u00b6 You may register the Gender custom data when creating the Contacts API instance, val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( GenderRegistration () ) ) Or, alternatively after creating the Contacts API instance, val contactsApi = Contacts ( context ) GenderRegistration (). registerTo ( contactsApi . customDataRegistry ) Get/set gender custom data \u00b6 Just like regular data kinds, gender custom data belong to a RawContact. A RawContact may only have 0 or 1 gender. To get the gender of a RawContact, val gender = rawContact . gender ( contactsApi ) To get the genders of all RawContacts belonging to a Contact, val genderSequence = contact . genders ( contactsApi ) val genderList = contact . genderList ( contactsApi ) To set the gender of a (mutable) RawContact, mutableRawContact . setGender ( contacts , mutableGender ) // or mutableRawContact . setGender ( contacts ) { type = GenderEntity . Type . MALE } To set the gender of the first RawContact in a Contact, mutableContact . setGender ( contacts , mutableGender ) // or mutableContact . setGender ( contacts ) { type = GenderEntity . Type . MALE } Use the gender custom data in queries, inserts, updates, and deletes \u00b6 Once you have registered your gender custom data with the Contacts API instance, the API instance is now able to perform read and write operations on it. Query custom data Insert custom data into new or existing contacts Update custom data Delete custom data Syncing gender custom data \u00b6 This library does not provide sync adapters for gender custom data. Unless you implement your own sync adapter, gender custom data... will NOT be synced across devices will NOT be shown in AOSP and Google Contacts apps, and other Contacts apps that show custom data from other apps For more info, read Sync contact data across devices .","title":"Integrate the Gender custom data"},{"location":"customdata/integrate-gender-custom-data/#integrate-the-gender-custom-data","text":"This library provides extensions for Gender custom data that allows you to read and write gender data for all of your contacts. These (optional) extensions live in the customdata-gender module. If you are looking to create your own custom data or get more insight on how the Gender custom data was built, read Integrate custom data .","title":"Integrate the gender custom data"},{"location":"customdata/integrate-gender-custom-data/#register-the-gender-custom-data-with-the-contacts-api-instance","text":"You may register the Gender custom data when creating the Contacts API instance, val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( GenderRegistration () ) ) Or, alternatively after creating the Contacts API instance, val contactsApi = Contacts ( context ) GenderRegistration (). registerTo ( contactsApi . customDataRegistry )","title":"Register the gender custom data with the Contacts API instance"},{"location":"customdata/integrate-gender-custom-data/#getset-gender-custom-data","text":"Just like regular data kinds, gender custom data belong to a RawContact. A RawContact may only have 0 or 1 gender. To get the gender of a RawContact, val gender = rawContact . gender ( contactsApi ) To get the genders of all RawContacts belonging to a Contact, val genderSequence = contact . genders ( contactsApi ) val genderList = contact . genderList ( contactsApi ) To set the gender of a (mutable) RawContact, mutableRawContact . setGender ( contacts , mutableGender ) // or mutableRawContact . setGender ( contacts ) { type = GenderEntity . Type . MALE } To set the gender of the first RawContact in a Contact, mutableContact . setGender ( contacts , mutableGender ) // or mutableContact . setGender ( contacts ) { type = GenderEntity . Type . MALE }","title":"Get/set gender custom data"},{"location":"customdata/integrate-gender-custom-data/#use-the-gender-custom-data-in-queries-inserts-updates-and-deletes","text":"Once you have registered your gender custom data with the Contacts API instance, the API instance is now able to perform read and write operations on it. Query custom data Insert custom data into new or existing contacts Update custom data Delete custom data","title":"Use the gender custom data in queries, inserts, updates, and deletes"},{"location":"customdata/integrate-gender-custom-data/#syncing-gender-custom-data","text":"This library does not provide sync adapters for gender custom data. Unless you implement your own sync adapter, gender custom data... will NOT be synced across devices will NOT be shown in AOSP and Google Contacts apps, and other Contacts apps that show custom data from other apps For more info, read Sync contact data across devices .","title":"Syncing gender custom data"},{"location":"customdata/integrate-googlecontacts-custom-data/","text":"Integrate the Google Contacts custom data \u00b6 This library provides extensions for custom data from the Google Contacts app; FileAs and UserDefined , which allows you to read and write Google Contacts data for all of your contacts. These (optional) extensions live in the customdata-googlecontacts module. If you are looking to create your own custom data or get more insight on how the FileAs and UserDefined custom data was built, read Integrate custom data . Register the Google Contacts custom data with the Contacts API instance \u00b6 You may register all Google Contacts custom data when creating the Contacts API instance, val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( GoogleContactsRegistration () ) ) Or, alternatively after creating the Contacts API instance, val contactsApi = Contacts ( context ) GoogleContactsRegistration (). registerTo ( contactsApi . customDataRegistry ) Read/write Google Contacts custom data \u00b6 Get/set FileAs \u00b6 Just like regular data kinds, FileAs custom data belong to a RawContact. A RawContact may only have 0 or 1 FileAs . To get the FileAs of a RawContact, val fileAs = rawContact . fileAs ( contactsApi ) To get the FileAs of all RawContacts belonging to a Contact, val fileAsSequence = contact . fileAs ( contactsApi ) val fileAsList = contact . fileAsList ( contactsApi ) To set the FileAs of a (mutable) RawContact, mutableRawContact . setFileAs ( contacts , mutableFileAs ) // or mutableRawContact . setFileAs ( contacts ) { name = \"Robot\" } To set the FileAs of the first RawContact in a Contact, mutableContact . setFileAs ( contacts , mutableFileAs ) // or mutableContact . setFileAs ( contacts ) { name = \"Robot\" } Get/add/remove UserDefined \u00b6 Just like regular data kinds, UserDefined custom data belong to a RawContact. A RawContact may have 0, 1, or more UserDefined . To get the UserDefined list/sequence of a RawContact, val userDefinedSequence = rawContact . userDefined ( contactsApi ) val userDefinedList = rawContact . userDefinedList ( contactsApi ) To get the UserDefined of all RawContacts belonging to a Contact, val userDefinedSequence = contact . userDefined ( contactsApi ) val userDefinedList = contact . userDefinedList ( contactsApi ) To add a UserDefined to a (mutable) RawContact, mutableRawContact . addUserDefined ( contacts , mutableUserDefined ) // or mutableRawContact . addUserDefined ( contacts ) { field = \"My Field\" label = \"My Label\" } To add a UserDefined to the first RawContact in a Contact, mutableContact . addUserDefined ( contacts , mutableUserDefined ) // or mutableContact . addUserDefined ( contacts ) { field = \"My Field\" label = \"My Label\" } Use the Google Contacts custom data in queries, inserts, updates, and deletes \u00b6 Once you have registered the Google Contacts custom data with the Contacts API instance, the API instance is now able to perform read and write operations on it. Query custom data Insert custom data into new or existing contacts Update custom data Delete custom data Google Contacts app data integrity \u00b6 When inserting or updating a UserDefined data kind, the Google Contacts app enforces UserDefined.field and UserDefined.label to both be non-null and non-blank. Otherwise, the insert or update operation fails. To protect the data integrity that the Google Contacts app imposes, this library is silently not performing insert or update operations for these instances. Consumers are informed via documentation. Both field and label must be non-null and non-blank strings in order for insert and update operations to be performed on them. The corresponding fields must also be included in the insert or update operation. Otherwise, the update and insert operation will silently NOT be performed. We might change the way we handle this in the future. Maybe we'll throw an exception instead or fail the entire insert/update and bubble up the reason. For now, to avoid complicating the API in these early stages, we'll go with silent but documented. We'll see what the community thinks! Google Contacts app UI \u00b6 In the Google Contacts app , the FileAs and UserDefined custom data are only shown for RawContacts that are associated with a Google Account. Local (device-only) RawContacts do not have these custom data! For more info on local contacts, read about Local (device-only) contacts . Syncing Google Contacts custom data \u00b6 The Google Contacts app comes with sync adapters that is responsible for syncing FileAs and UserDefined custom data. As long as you have the Google Contacts app installed, these custom data should remain synced depending on account sync settings. This library does not provide sync adapters for Google Contacts custom data. For more info, read Sync contact data across devices .","title":"Integrate the Google Contacts custom data"},{"location":"customdata/integrate-googlecontacts-custom-data/#integrate-the-google-contacts-custom-data","text":"This library provides extensions for custom data from the Google Contacts app; FileAs and UserDefined , which allows you to read and write Google Contacts data for all of your contacts. These (optional) extensions live in the customdata-googlecontacts module. If you are looking to create your own custom data or get more insight on how the FileAs and UserDefined custom data was built, read Integrate custom data .","title":"Integrate the Google Contacts custom data"},{"location":"customdata/integrate-googlecontacts-custom-data/#register-the-google-contacts-custom-data-with-the-contacts-api-instance","text":"You may register all Google Contacts custom data when creating the Contacts API instance, val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( GoogleContactsRegistration () ) ) Or, alternatively after creating the Contacts API instance, val contactsApi = Contacts ( context ) GoogleContactsRegistration (). registerTo ( contactsApi . customDataRegistry )","title":"Register the Google Contacts custom data with the Contacts API instance"},{"location":"customdata/integrate-googlecontacts-custom-data/#readwrite-google-contacts-custom-data","text":"","title":"Read/write Google Contacts custom data"},{"location":"customdata/integrate-googlecontacts-custom-data/#getset-fileas","text":"Just like regular data kinds, FileAs custom data belong to a RawContact. A RawContact may only have 0 or 1 FileAs . To get the FileAs of a RawContact, val fileAs = rawContact . fileAs ( contactsApi ) To get the FileAs of all RawContacts belonging to a Contact, val fileAsSequence = contact . fileAs ( contactsApi ) val fileAsList = contact . fileAsList ( contactsApi ) To set the FileAs of a (mutable) RawContact, mutableRawContact . setFileAs ( contacts , mutableFileAs ) // or mutableRawContact . setFileAs ( contacts ) { name = \"Robot\" } To set the FileAs of the first RawContact in a Contact, mutableContact . setFileAs ( contacts , mutableFileAs ) // or mutableContact . setFileAs ( contacts ) { name = \"Robot\" }","title":"Get/set FileAs"},{"location":"customdata/integrate-googlecontacts-custom-data/#getaddremove-userdefined","text":"Just like regular data kinds, UserDefined custom data belong to a RawContact. A RawContact may have 0, 1, or more UserDefined . To get the UserDefined list/sequence of a RawContact, val userDefinedSequence = rawContact . userDefined ( contactsApi ) val userDefinedList = rawContact . userDefinedList ( contactsApi ) To get the UserDefined of all RawContacts belonging to a Contact, val userDefinedSequence = contact . userDefined ( contactsApi ) val userDefinedList = contact . userDefinedList ( contactsApi ) To add a UserDefined to a (mutable) RawContact, mutableRawContact . addUserDefined ( contacts , mutableUserDefined ) // or mutableRawContact . addUserDefined ( contacts ) { field = \"My Field\" label = \"My Label\" } To add a UserDefined to the first RawContact in a Contact, mutableContact . addUserDefined ( contacts , mutableUserDefined ) // or mutableContact . addUserDefined ( contacts ) { field = \"My Field\" label = \"My Label\" }","title":"Get/add/remove UserDefined"},{"location":"customdata/integrate-googlecontacts-custom-data/#use-the-google-contacts-custom-data-in-queries-inserts-updates-and-deletes","text":"Once you have registered the Google Contacts custom data with the Contacts API instance, the API instance is now able to perform read and write operations on it. Query custom data Insert custom data into new or existing contacts Update custom data Delete custom data","title":"Use the Google Contacts custom data in queries, inserts, updates, and deletes"},{"location":"customdata/integrate-googlecontacts-custom-data/#google-contacts-app-data-integrity","text":"When inserting or updating a UserDefined data kind, the Google Contacts app enforces UserDefined.field and UserDefined.label to both be non-null and non-blank. Otherwise, the insert or update operation fails. To protect the data integrity that the Google Contacts app imposes, this library is silently not performing insert or update operations for these instances. Consumers are informed via documentation. Both field and label must be non-null and non-blank strings in order for insert and update operations to be performed on them. The corresponding fields must also be included in the insert or update operation. Otherwise, the update and insert operation will silently NOT be performed. We might change the way we handle this in the future. Maybe we'll throw an exception instead or fail the entire insert/update and bubble up the reason. For now, to avoid complicating the API in these early stages, we'll go with silent but documented. We'll see what the community thinks!","title":"Google Contacts app data integrity"},{"location":"customdata/integrate-googlecontacts-custom-data/#google-contacts-app-ui","text":"In the Google Contacts app , the FileAs and UserDefined custom data are only shown for RawContacts that are associated with a Google Account. Local (device-only) RawContacts do not have these custom data! For more info on local contacts, read about Local (device-only) contacts .","title":"Google Contacts app UI"},{"location":"customdata/integrate-googlecontacts-custom-data/#syncing-google-contacts-custom-data","text":"The Google Contacts app comes with sync adapters that is responsible for syncing FileAs and UserDefined custom data. As long as you have the Google Contacts app installed, these custom data should remain synced depending on account sync settings. This library does not provide sync adapters for Google Contacts custom data. For more info, read Sync contact data across devices .","title":"Syncing Google Contacts custom data"},{"location":"customdata/integrate-handlename-custom-data/","text":"Integrate the handle name custom data \u00b6 This library provides extensions for HandleName custom data that allows you to read and write handle name data for all of your contacts. These (optional) extensions live in the customdata-handlename module. If you are looking to create your own custom data or get more insight on how the HandleName custom data was built, read Integrate custom data . Register the handle name custom data with the Contacts API instance \u00b6 You may register the HandleName custom data when creating the Contacts API instance, val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( HandleNameRegistration () ) ) Or, alternatively after creating the Contacts API instance, val contactsApi = Contacts ( context ) HandleNameRegistration (). registerTo ( contactsApi . customDataRegistry ) Get/add/remove handle name custom data \u00b6 Just like regular data kinds, handle name custom data belong to a RawContact. A RawContact may have 0, 1, or more handle names. To get the handle names of a RawContact, val handleNameSequence = rawContact . handleNames ( contactsApi ) val handleNameList = rawContact . handleNameList ( contactsApi ) To get the handle names of all RawContacts belonging to a Contact, val handleNameSequence = contact . handleNames ( contactsApi ) val handleNameList = contact . handleNameList ( contactsApi ) To add a handle name to a (mutable) RawContact, mutableRawContact . addHandleName ( contacts , mutableHandleName ) // or mutableRawContact . addHandleName ( contacts ) { handle = \"CoolDude91\" } To add a handle name to a the first RawContact or a Contact, mutableContact . addHandleName ( contacts , mutableHandleName ) // or mutableContact . addHandleName ( contacts ) { handle = \"CoolGal89\" } Use the handle name custom data in queries, inserts, updates, and deletes \u00b6 Once you have registered your handle name custom data with the Contacts API instance, the API instance is now able to perform read and write operations on it. Query custom data Insert custom data into new or existing contacts Update custom data Delete custom data Syncing handle name custom data \u00b6 This library does not provide sync adapters for handle name custom data. Unless you implement your own sync adapter, handle name custom data... will NOT be synced across devices will NOT be shown in AOSP and Google Contacts apps, and other Contacts apps that show custom data from other apps For more info, read Sync contact data across devices .","title":"Integrate the Handle Name custom data"},{"location":"customdata/integrate-handlename-custom-data/#integrate-the-handle-name-custom-data","text":"This library provides extensions for HandleName custom data that allows you to read and write handle name data for all of your contacts. These (optional) extensions live in the customdata-handlename module. If you are looking to create your own custom data or get more insight on how the HandleName custom data was built, read Integrate custom data .","title":"Integrate the handle name custom data"},{"location":"customdata/integrate-handlename-custom-data/#register-the-handle-name-custom-data-with-the-contacts-api-instance","text":"You may register the HandleName custom data when creating the Contacts API instance, val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( HandleNameRegistration () ) ) Or, alternatively after creating the Contacts API instance, val contactsApi = Contacts ( context ) HandleNameRegistration (). registerTo ( contactsApi . customDataRegistry )","title":"Register the handle name custom data with the Contacts API instance"},{"location":"customdata/integrate-handlename-custom-data/#getaddremove-handle-name-custom-data","text":"Just like regular data kinds, handle name custom data belong to a RawContact. A RawContact may have 0, 1, or more handle names. To get the handle names of a RawContact, val handleNameSequence = rawContact . handleNames ( contactsApi ) val handleNameList = rawContact . handleNameList ( contactsApi ) To get the handle names of all RawContacts belonging to a Contact, val handleNameSequence = contact . handleNames ( contactsApi ) val handleNameList = contact . handleNameList ( contactsApi ) To add a handle name to a (mutable) RawContact, mutableRawContact . addHandleName ( contacts , mutableHandleName ) // or mutableRawContact . addHandleName ( contacts ) { handle = \"CoolDude91\" } To add a handle name to a the first RawContact or a Contact, mutableContact . addHandleName ( contacts , mutableHandleName ) // or mutableContact . addHandleName ( contacts ) { handle = \"CoolGal89\" }","title":"Get/add/remove handle name custom data"},{"location":"customdata/integrate-handlename-custom-data/#use-the-handle-name-custom-data-in-queries-inserts-updates-and-deletes","text":"Once you have registered your handle name custom data with the Contacts API instance, the API instance is now able to perform read and write operations on it. Query custom data Insert custom data into new or existing contacts Update custom data Delete custom data","title":"Use the handle name custom data in queries, inserts, updates, and deletes"},{"location":"customdata/integrate-handlename-custom-data/#syncing-handle-name-custom-data","text":"This library does not provide sync adapters for handle name custom data. Unless you implement your own sync adapter, handle name custom data... will NOT be synced across devices will NOT be shown in AOSP and Google Contacts apps, and other Contacts apps that show custom data from other apps For more info, read Sync contact data across devices .","title":"Syncing handle name custom data"},{"location":"customdata/integrate-pokemon-custom-data/","text":"Integrate the Pokemon custom data \u00b6 This library provides extensions for Pokemon custom data that allows you to read and write pokemon data for all of your contacts. These (optional) extensions live in the customdata-pokemon module. If you are looking to create your own custom data or get more insight on how the Pokemon custom data was built, read Integrate custom data . Register the pokemon custom data with the Contacts API instance \u00b6 You may register the Pokemon custom data when creating the Contacts API instance, val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( PokemonRegistration () ) ) Or, alternatively after creating the Contacts API instance, val contactsApi = Contacts ( context ) PokemonRegistration (). registerTo ( contactsApi . customDataRegistry ) Get/add/remove pokemon custom data \u00b6 Just like regular data kinds, pokemon custom data belong to a RawContact. A RawContact may have 0, 1, or more pokemons. To get the pokemons of a RawContact, val pokemonSequence = rawContact . pokemons ( contactsApi ) val pokemonList = rawContact . pokemonList ( contactsApi ) To get the pokemons of all RawContacts belonging to a Contact, val pokemonSequence = contact . pokemons ( contactsApi ) val pokemonList = contact . pokemonList ( contactsApi ) To add a pokemon to a (mutable) RawContact, mutableRawContact . addPokemon ( contacts , mutablePokemon ) // or mutableRawContact . addPokemon ( contacts ) { name = \"ditto\" nickname = \"copy-cat\" level = 24 pokeApiId = 132 } To add a pokemon to a the first RawContact or a Contact, mutableContact . addPokemon ( contacts , mutablePokemon ) // or mutableContact . addPokemon ( contacts ) { name = \"ditto\" nickname = \"copy-cat\" level = 24 pokeApiId = 132 } Use the pokemon custom data in queries, inserts, updates, and deletes \u00b6 Once you have registered your pokemon custom data with the Contacts API instance, the API instance is now able to perform read and write operations on it. Query custom data Insert custom data into new or existing contacts Update custom data Delete custom data Syncing pokemon custom data \u00b6 This library does not provide sync adapters for pokemon custom data. Unless you implement your own sync adapter, pokemon custom data... will NOT be synced across devices will NOT be shown in AOSP and Google Contacts apps, and other Contacts apps that show custom data from other apps For more info, read Sync contact data across devices .","title":"Integrate the Pokemon custom data"},{"location":"customdata/integrate-pokemon-custom-data/#integrate-the-pokemon-custom-data","text":"This library provides extensions for Pokemon custom data that allows you to read and write pokemon data for all of your contacts. These (optional) extensions live in the customdata-pokemon module. If you are looking to create your own custom data or get more insight on how the Pokemon custom data was built, read Integrate custom data .","title":"Integrate the Pokemon custom data"},{"location":"customdata/integrate-pokemon-custom-data/#register-the-pokemon-custom-data-with-the-contacts-api-instance","text":"You may register the Pokemon custom data when creating the Contacts API instance, val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( PokemonRegistration () ) ) Or, alternatively after creating the Contacts API instance, val contactsApi = Contacts ( context ) PokemonRegistration (). registerTo ( contactsApi . customDataRegistry )","title":"Register the pokemon custom data with the Contacts API instance"},{"location":"customdata/integrate-pokemon-custom-data/#getaddremove-pokemon-custom-data","text":"Just like regular data kinds, pokemon custom data belong to a RawContact. A RawContact may have 0, 1, or more pokemons. To get the pokemons of a RawContact, val pokemonSequence = rawContact . pokemons ( contactsApi ) val pokemonList = rawContact . pokemonList ( contactsApi ) To get the pokemons of all RawContacts belonging to a Contact, val pokemonSequence = contact . pokemons ( contactsApi ) val pokemonList = contact . pokemonList ( contactsApi ) To add a pokemon to a (mutable) RawContact, mutableRawContact . addPokemon ( contacts , mutablePokemon ) // or mutableRawContact . addPokemon ( contacts ) { name = \"ditto\" nickname = \"copy-cat\" level = 24 pokeApiId = 132 } To add a pokemon to a the first RawContact or a Contact, mutableContact . addPokemon ( contacts , mutablePokemon ) // or mutableContact . addPokemon ( contacts ) { name = \"ditto\" nickname = \"copy-cat\" level = 24 pokeApiId = 132 }","title":"Get/add/remove pokemon custom data"},{"location":"customdata/integrate-pokemon-custom-data/#use-the-pokemon-custom-data-in-queries-inserts-updates-and-deletes","text":"Once you have registered your pokemon custom data with the Contacts API instance, the API instance is now able to perform read and write operations on it. Query custom data Insert custom data into new or existing contacts Update custom data Delete custom data","title":"Use the pokemon custom data in queries, inserts, updates, and deletes"},{"location":"customdata/integrate-pokemon-custom-data/#syncing-pokemon-custom-data","text":"This library does not provide sync adapters for pokemon custom data. Unless you implement your own sync adapter, pokemon custom data... will NOT be synced across devices will NOT be shown in AOSP and Google Contacts apps, and other Contacts apps that show custom data from other apps For more info, read Sync contact data across devices .","title":"Syncing pokemon custom data"},{"location":"customdata/integrate-rpg-custom-data/","text":"Integrate the Role Playing Game (RPG) custom data \u00b6 This provides extensions for RpgStats and RpgProfession custom data that allows you to read and write rpg data for all of your contacts. These (optional) extensions live in the customdata-rpg module. If you are looking to create your own custom data or get more insight on how the RpgStats and RpgProfession custom data was built, read Integrate custom data . Register the RPG custom data with the Contacts API instance \u00b6 You may register all RPG custom data when creating the Contacts API instance, val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( RpgRegistration () ) ) Or, alternatively after creating the Contacts API instance, val contactsApi = Contacts ( context ) RpgRegistration (). registerTo ( contactsApi . customDataRegistry ) Read/write RPG custom data \u00b6 Get/set RpgStats \u00b6 Just like regular data kinds, RpgStats custom data belong to a RawContact. A RawContact may only have 0 or 1 RpgStats . To get the RpgStats of a RawContact, val rpgStats = rawContact . rpgStats ( contactsApi ) To get the RpgStats of all RawContacts belonging to a Contact, val rpgStatsSequence = contact . rpgStats ( contactsApi ) val rpgStatsList = contact . rpgStatsList ( contactsApi ) To set the RpgStats of a (mutable) RawContact, mutableRawContact . setRpgStats ( contacts , mutableRpgStats ) // or mutableRawContact . setRpgStats ( contacts ) { level = 78 speed = 500 strength = 789 intelligence = 123 luck = 999 } To set the RpgStats of the first RawContact in a Contact, mutableContact . setRpgStats ( contacts , mutableRpgStats ) // or mutableContact . setRpgStats ( contacts ) { level = 78 speed = 500 strength = 789 intelligence = 123 luck = 999 } Get/set RpgProfession \u00b6 Just like regular data kinds, RpgProfession custom data belong to a RawContact. A RawContact may only have 0 or 1 RpgProfession . To get the RpgProfession of a RawContact, val rpgProfession = rawContact . rpgProfession ( contactsApi ) To get the RpgProfession of all RawContacts belonging to a Contact, val rpgProfessionSequence = contact . rpgProfessions ( contactsApi ) val rpgProfessionList = contact . rpgProfessionList ( contactsApi ) To set the RpgProfession of a (mutable) RawContact, mutableRawContact . setRpgProfession ( contacts , mutableRpgProfession ) // or mutableRawContact . setRpgProfession ( contacts ) { title = \"swordsman\" } To set the RpgProfession of the first RawContact in a Contact, mutableContact . setRpgProfession ( contacts , mutableRpgProfession ) // or mutableContact . setRpgProfession ( contacts ) { title = \"swordsman\" } Use the RPG custom data in queries, inserts, updates, and deletes \u00b6 Once you have registered the RPG custom data with the Contacts API instance, the API instance is now able to perform read and write operations on it. Query custom data Insert custom data into new or existing contacts Update custom data Delete custom data Syncing RPG custom data \u00b6 This library does not provide sync adapters for RPG custom data. Unless you implement your own sync adapter, RPG custom data... will NOT be synced across devices will NOT be shown in AOSP and Google Contacts apps, and other Contacts apps that show custom data from other apps For more info, read Sync contact data across devices .","title":"Integrate the RPG custom data"},{"location":"customdata/integrate-rpg-custom-data/#integrate-the-role-playing-game-rpg-custom-data","text":"This provides extensions for RpgStats and RpgProfession custom data that allows you to read and write rpg data for all of your contacts. These (optional) extensions live in the customdata-rpg module. If you are looking to create your own custom data or get more insight on how the RpgStats and RpgProfession custom data was built, read Integrate custom data .","title":"Integrate the Role Playing Game (RPG) custom data"},{"location":"customdata/integrate-rpg-custom-data/#register-the-rpg-custom-data-with-the-contacts-api-instance","text":"You may register all RPG custom data when creating the Contacts API instance, val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( RpgRegistration () ) ) Or, alternatively after creating the Contacts API instance, val contactsApi = Contacts ( context ) RpgRegistration (). registerTo ( contactsApi . customDataRegistry )","title":"Register the RPG custom data with the Contacts API instance"},{"location":"customdata/integrate-rpg-custom-data/#readwrite-rpg-custom-data","text":"","title":"Read/write RPG custom data"},{"location":"customdata/integrate-rpg-custom-data/#getset-rpgstats","text":"Just like regular data kinds, RpgStats custom data belong to a RawContact. A RawContact may only have 0 or 1 RpgStats . To get the RpgStats of a RawContact, val rpgStats = rawContact . rpgStats ( contactsApi ) To get the RpgStats of all RawContacts belonging to a Contact, val rpgStatsSequence = contact . rpgStats ( contactsApi ) val rpgStatsList = contact . rpgStatsList ( contactsApi ) To set the RpgStats of a (mutable) RawContact, mutableRawContact . setRpgStats ( contacts , mutableRpgStats ) // or mutableRawContact . setRpgStats ( contacts ) { level = 78 speed = 500 strength = 789 intelligence = 123 luck = 999 } To set the RpgStats of the first RawContact in a Contact, mutableContact . setRpgStats ( contacts , mutableRpgStats ) // or mutableContact . setRpgStats ( contacts ) { level = 78 speed = 500 strength = 789 intelligence = 123 luck = 999 }","title":"Get/set RpgStats"},{"location":"customdata/integrate-rpg-custom-data/#getset-rpgprofession","text":"Just like regular data kinds, RpgProfession custom data belong to a RawContact. A RawContact may only have 0 or 1 RpgProfession . To get the RpgProfession of a RawContact, val rpgProfession = rawContact . rpgProfession ( contactsApi ) To get the RpgProfession of all RawContacts belonging to a Contact, val rpgProfessionSequence = contact . rpgProfessions ( contactsApi ) val rpgProfessionList = contact . rpgProfessionList ( contactsApi ) To set the RpgProfession of a (mutable) RawContact, mutableRawContact . setRpgProfession ( contacts , mutableRpgProfession ) // or mutableRawContact . setRpgProfession ( contacts ) { title = \"swordsman\" } To set the RpgProfession of the first RawContact in a Contact, mutableContact . setRpgProfession ( contacts , mutableRpgProfession ) // or mutableContact . setRpgProfession ( contacts ) { title = \"swordsman\" }","title":"Get/set RpgProfession"},{"location":"customdata/integrate-rpg-custom-data/#use-the-rpg-custom-data-in-queries-inserts-updates-and-deletes","text":"Once you have registered the RPG custom data with the Contacts API instance, the API instance is now able to perform read and write operations on it. Query custom data Insert custom data into new or existing contacts Update custom data Delete custom data","title":"Use the RPG custom data in queries, inserts, updates, and deletes"},{"location":"customdata/integrate-rpg-custom-data/#syncing-rpg-custom-data","text":"This library does not provide sync adapters for RPG custom data. Unless you implement your own sync adapter, RPG custom data... will NOT be synced across devices will NOT be shown in AOSP and Google Contacts apps, and other Contacts apps that show custom data from other apps For more info, read Sync contact data across devices .","title":"Syncing RPG custom data"},{"location":"customdata/query-custom-data/","text":"Query custom data \u00b6 This library provides several query APIs that support custom data integration. Query Query contacts (advanced) BroadQuery Query contacts ProfileQuery Query device owner Contact profile DataQuery Query specific data kinds To help illustrate how custom data integrates with these query APIs, we'll use the HandleName and Gender custom data. For more info, read Integrate the gender custom data and Integrate the handle name custom data . Getting custom data from a Contact or RawContact \u00b6 Custom data, just like regular data kinds, are attached to a RawContact. They follow the same rules as regular data kinds. For more info, read about API Entities . For example, you are able to get the handle names and gender of a RawContact, val handleNames = rawContact . handleNames ( contactsApi ) val gender = rawContact . gender ( contactsApi ) There are also extensions that allow you to get custom data from a Contact, which can be made up of one or more RawContacts, val handleNames = contact . handleNames ( contactsApi ) val genders = contact . genders ( contactsApi ) Getting specific custom data kinds directly \u00b6 Every custom data provides an extension to the DataQuery that allows you to query for only that specific custom data kind. For example, to get all available HandleName s and Gender s from all contacts, val handleNames = Contacts ( context ). data (). query (). handleNames (). find () val genders = Contacts ( context ). data (). query (). genders (). find () To get all HandleName s starting with the letter \"h\", val handleNames = Contacts ( context ) . data () . query () . handleNames () . where { Handle startsWith \"h\" } . find () For more info, read Query specific data kinds . The include function and custom data \u00b6 All of the above mentioned APIs provide an include function that allows you to include only a given set of fields (data) in each of the returned entities. Custom data entries provides fields that can be used in this function. By default, not calling the include function will include all fields, including custom data fields. For example, to explicitly include all HandleName fields, . include ( HandleNameFields . all ) For more info, read Include only certain fields for read and write operations . The where function and custom data \u00b6 The Query and DataQuery APIs provides a where function that allows you to specify a matching criteria based on specific field values. Custom data entries provides fields that can be used in this function. For example, to match HandleName s starting with the letter \"h\", . where { Handle startsWith \"h\" } The BroadQuery API provides a whereAnyContactDataPartiallyMatches function that NOT support matching custom data. Only native data are included in the matching process. The ProfileQuery API does not provide a where function as there can only be one profile Contact per device. The orderBy function and custom data \u00b6 The DataQuery API provides an orderBy function that supports custom data. For example, to order HandleName s, . orderBy ( HandleNameFields . Handle . asc ()) The Query and BroadQuery APIs provides an orderBy function that only takes in fields from the Contacts table, not data. So there is no custom data, or native data, support for this. The ProfileQuery API does not provide an orderBy function as there can only be at most one profile Contact on the device.","title":"Query custom data"},{"location":"customdata/query-custom-data/#query-custom-data","text":"This library provides several query APIs that support custom data integration. Query Query contacts (advanced) BroadQuery Query contacts ProfileQuery Query device owner Contact profile DataQuery Query specific data kinds To help illustrate how custom data integrates with these query APIs, we'll use the HandleName and Gender custom data. For more info, read Integrate the gender custom data and Integrate the handle name custom data .","title":"Query custom data"},{"location":"customdata/query-custom-data/#getting-custom-data-from-a-contact-or-rawcontact","text":"Custom data, just like regular data kinds, are attached to a RawContact. They follow the same rules as regular data kinds. For more info, read about API Entities . For example, you are able to get the handle names and gender of a RawContact, val handleNames = rawContact . handleNames ( contactsApi ) val gender = rawContact . gender ( contactsApi ) There are also extensions that allow you to get custom data from a Contact, which can be made up of one or more RawContacts, val handleNames = contact . handleNames ( contactsApi ) val genders = contact . genders ( contactsApi )","title":"Getting custom data from a Contact or RawContact"},{"location":"customdata/query-custom-data/#getting-specific-custom-data-kinds-directly","text":"Every custom data provides an extension to the DataQuery that allows you to query for only that specific custom data kind. For example, to get all available HandleName s and Gender s from all contacts, val handleNames = Contacts ( context ). data (). query (). handleNames (). find () val genders = Contacts ( context ). data (). query (). genders (). find () To get all HandleName s starting with the letter \"h\", val handleNames = Contacts ( context ) . data () . query () . handleNames () . where { Handle startsWith \"h\" } . find () For more info, read Query specific data kinds .","title":"Getting specific custom data kinds directly"},{"location":"customdata/query-custom-data/#the-include-function-and-custom-data","text":"All of the above mentioned APIs provide an include function that allows you to include only a given set of fields (data) in each of the returned entities. Custom data entries provides fields that can be used in this function. By default, not calling the include function will include all fields, including custom data fields. For example, to explicitly include all HandleName fields, . include ( HandleNameFields . all ) For more info, read Include only certain fields for read and write operations .","title":"The include function and custom data"},{"location":"customdata/query-custom-data/#the-where-function-and-custom-data","text":"The Query and DataQuery APIs provides a where function that allows you to specify a matching criteria based on specific field values. Custom data entries provides fields that can be used in this function. For example, to match HandleName s starting with the letter \"h\", . where { Handle startsWith \"h\" } The BroadQuery API provides a whereAnyContactDataPartiallyMatches function that NOT support matching custom data. Only native data are included in the matching process. The ProfileQuery API does not provide a where function as there can only be one profile Contact per device.","title":"The where function and custom data"},{"location":"customdata/query-custom-data/#the-orderby-function-and-custom-data","text":"The DataQuery API provides an orderBy function that supports custom data. For example, to order HandleName s, . orderBy ( HandleNameFields . Handle . asc ()) The Query and BroadQuery APIs provides an orderBy function that only takes in fields from the Contacts table, not data. So there is no custom data, or native data, support for this. The ProfileQuery API does not provide an orderBy function as there can only be at most one profile Contact on the device.","title":"The orderBy function and custom data"},{"location":"customdata/update-custom-data/","text":"Update custom data \u00b6 This library provides several update APIs that support custom data integration. Update Update contacts ProfileUpdate Update device owner Contact profile DataUpdate Update existing sets of data To help illustrate how custom data integrates with these update APIs, we'll use the HandleName and Gender custom data. For more info about custom data, read Integrate custom data . Updating custom data via Contacts/RawContacts \u00b6 Custom data, just like regular data kinds, are attached to a RawContact. They follow the same rules as regular data kinds. For more info, read about API Entities . For example, you are able to update existing handle names and the gender of an existing RawContact, mutableRawContact . handleNames ( contactsApi ). firstOrNull () ?. apply { handle = \"gal91\" } mutableRawContact . gender ( contactsApi ) ?. apply { type = GenderEntity . Type . FEMALE } There are also extensions that allow you to update custom data of an existing RawContact via a Contact, which can be made up of one or more RawContacts, mutableContact . handleNames ( contactsApi ). firstOrNull () ?. apply { handle = \"gal91\" } mutableContact . genders ( contactsApi ). firstOrNull () ?. apply { type = GenderEntity . Type . FEMALE } Once you have made the updates to existing custom data, you can perform the update operation on the RawContact to commit your changes into the database using Update or ProfileUpdate . Updating sets of custom data directly \u00b6 All custom data are compatible with the DataUpdate API, which allows you to update sets of existing regular and custom data kinds. For example, to update a set of HandleName s and Gender s, val handleNames : List < MutableHandleName > val genders : List < MutableGender > val updateResult = Contacts ( this ) . data () . update () . data ( handleNames + genders ) . commit () For more info, read Update existing sets of data . The include function and custom data \u00b6 All of the above mentioned APIs provide an include function that allows you to include only a given set of fields (data) to be processed in the update operation. Custom data entries provides fields that can be used in this function. By default, not calling the include function will include all fields, including custom data fields. For example, to specifically include only HandleName and Gender fields, . include ( HandleNameFields . all + GenderFields . all ) For more info, read Include only certain fields for read and write operations . Blank data are deleted \u00b6 Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are deleted by update APIs. For more info, read about Blank data .","title":"Update custom data"},{"location":"customdata/update-custom-data/#update-custom-data","text":"This library provides several update APIs that support custom data integration. Update Update contacts ProfileUpdate Update device owner Contact profile DataUpdate Update existing sets of data To help illustrate how custom data integrates with these update APIs, we'll use the HandleName and Gender custom data. For more info about custom data, read Integrate custom data .","title":"Update custom data"},{"location":"customdata/update-custom-data/#updating-custom-data-via-contactsrawcontacts","text":"Custom data, just like regular data kinds, are attached to a RawContact. They follow the same rules as regular data kinds. For more info, read about API Entities . For example, you are able to update existing handle names and the gender of an existing RawContact, mutableRawContact . handleNames ( contactsApi ). firstOrNull () ?. apply { handle = \"gal91\" } mutableRawContact . gender ( contactsApi ) ?. apply { type = GenderEntity . Type . FEMALE } There are also extensions that allow you to update custom data of an existing RawContact via a Contact, which can be made up of one or more RawContacts, mutableContact . handleNames ( contactsApi ). firstOrNull () ?. apply { handle = \"gal91\" } mutableContact . genders ( contactsApi ). firstOrNull () ?. apply { type = GenderEntity . Type . FEMALE } Once you have made the updates to existing custom data, you can perform the update operation on the RawContact to commit your changes into the database using Update or ProfileUpdate .","title":"Updating custom data via Contacts/RawContacts"},{"location":"customdata/update-custom-data/#updating-sets-of-custom-data-directly","text":"All custom data are compatible with the DataUpdate API, which allows you to update sets of existing regular and custom data kinds. For example, to update a set of HandleName s and Gender s, val handleNames : List < MutableHandleName > val genders : List < MutableGender > val updateResult = Contacts ( this ) . data () . update () . data ( handleNames + genders ) . commit () For more info, read Update existing sets of data .","title":"Updating sets of custom data directly"},{"location":"customdata/update-custom-data/#the-include-function-and-custom-data","text":"All of the above mentioned APIs provide an include function that allows you to include only a given set of fields (data) to be processed in the update operation. Custom data entries provides fields that can be used in this function. By default, not calling the include function will include all fields, including custom data fields. For example, to specifically include only HandleName and Gender fields, . include ( HandleNameFields . all + GenderFields . all ) For more info, read Include only certain fields for read and write operations .","title":"The include function and custom data"},{"location":"customdata/update-custom-data/#blank-data-are-deleted","text":"Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are deleted by update APIs. For more info, read about Blank data .","title":"Blank data are deleted"},{"location":"data/delete-data-sets/","text":"Delete existing sets of data \u00b6 This library provides the DataDelete API that allows you to delete a list of any data kinds directly without having to delete them via Contacts/RawContacts. An instance of the DataDelete API is obtained by, val delete = Contacts ( context ). data (). delete () To delete all kinds of data via Contacts/RawContacts, you may remove them from the Contact/RawContact and then perform an update. For more info, read Update contacts . A basic delete \u00b6 To delete a set of data, val deleteResult = Contacts ( context ) . data () . delete () . data ( data ) . commit () If you want to delete a list of emails and phones, val deleteResult = Contacts ( context ) . data () . delete () . data ( emails + phones ) . commit () Executing the delete \u00b6 To execute the delete, . commit () If you want to delete all given data in a single atomic transaction, . commitInOneTransaction () The call to commitInOneTransaction will only succeed if ALL given data are successfully deleted. If one delete fails, the entire operation will fail and everything will be reverted prior to the delete operation. In contrast, commit allows for some deletes to succeed and some to fail. Handling the delete result \u00b6 The commit and commitInOneTransaction functions returns a Result , To check if all deletes succeeded, val allDeletesSuccessful = deleteResult . isSuccessful To check if a particular delete succeeded, val firstDeleteSuccessful = deleteResult . isSuccessful ( data1 ) Performing the delete and result processing asynchronously \u00b6 Deletes are executed when the commit or commitInOneTransaction function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the delete with permission \u00b6 Deletes require the android.permission.WRITE_CONTACTS permissions. If not granted, the delete will do nothing and return a failed result. To perform the delete with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Profile data \u00b6 The DataDelete API also supports deleting the Profile (device owner) contact data. To get an instance of this API for Profile data deletes, val profileDataDelete = Contacts ( context ). profile (). data (). delete () All deletes will be limited to the Profile, whether it exists or not. Custom data support \u00b6 The DataDelete API supports custom data. For more info, read Delete custom data .","title":"Delete existing sets of data"},{"location":"data/delete-data-sets/#delete-existing-sets-of-data","text":"This library provides the DataDelete API that allows you to delete a list of any data kinds directly without having to delete them via Contacts/RawContacts. An instance of the DataDelete API is obtained by, val delete = Contacts ( context ). data (). delete () To delete all kinds of data via Contacts/RawContacts, you may remove them from the Contact/RawContact and then perform an update. For more info, read Update contacts .","title":"Delete existing sets of data"},{"location":"data/delete-data-sets/#a-basic-delete","text":"To delete a set of data, val deleteResult = Contacts ( context ) . data () . delete () . data ( data ) . commit () If you want to delete a list of emails and phones, val deleteResult = Contacts ( context ) . data () . delete () . data ( emails + phones ) . commit ()","title":"A basic delete"},{"location":"data/delete-data-sets/#executing-the-delete","text":"To execute the delete, . commit () If you want to delete all given data in a single atomic transaction, . commitInOneTransaction () The call to commitInOneTransaction will only succeed if ALL given data are successfully deleted. If one delete fails, the entire operation will fail and everything will be reverted prior to the delete operation. In contrast, commit allows for some deletes to succeed and some to fail.","title":"Executing the delete"},{"location":"data/delete-data-sets/#handling-the-delete-result","text":"The commit and commitInOneTransaction functions returns a Result , To check if all deletes succeeded, val allDeletesSuccessful = deleteResult . isSuccessful To check if a particular delete succeeded, val firstDeleteSuccessful = deleteResult . isSuccessful ( data1 )","title":"Handling the delete result"},{"location":"data/delete-data-sets/#performing-the-delete-and-result-processing-asynchronously","text":"Deletes are executed when the commit or commitInOneTransaction function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the delete and result processing asynchronously"},{"location":"data/delete-data-sets/#performing-the-delete-with-permission","text":"Deletes require the android.permission.WRITE_CONTACTS permissions. If not granted, the delete will do nothing and return a failed result. To perform the delete with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the delete with permission"},{"location":"data/delete-data-sets/#profile-data","text":"The DataDelete API also supports deleting the Profile (device owner) contact data. To get an instance of this API for Profile data deletes, val profileDataDelete = Contacts ( context ). profile (). data (). delete () All deletes will be limited to the Profile, whether it exists or not.","title":"Profile data"},{"location":"data/delete-data-sets/#custom-data-support","text":"The DataDelete API supports custom data. For more info, read Delete custom data .","title":"Custom data support"},{"location":"data/insert-data-sets/","text":"Insert data into new or existing contacts \u00b6 Data can only be created/inserted into the database whenever inserting or updating new or existing contacts. When using insert and update APIs such as Insert , ProfileInsert , Update , and ProfileUpdate , you are able to create/insert data into new or existing RawContacts respectively. For example, to insert an email into a new contact using the Insert API, Contacts ( context ) . insert () . rawContact { addEmail ( email ) } . commit () For more info, read Insert contacts . To insert an email into a new Profile contact using the ProfileInsert API, Contacts ( context ) . profile () . insert () . rawContact { addEmail ( email ) } . commit () For more info, read Insert device owner Contact profile . To insert an email into an existing contact using the Update API, Contacts ( context ) . update () . contacts ( existingContact . mutableCopy { addEmail ( email ) }) . commit () For more info, read Update contacts . To insert an email into an the existing Profile Contact using the ProfileUpdate API, Contacts ( context ) . profile () . update () . contact ( existingProfileContact . mutableCopy { addEmail ( email ) }) . commit () For more info, read Update device owner Contact profile . Blank data are not inserted \u00b6 Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are ignored and are not inserted by insert APIs. For more info, read about Blank data .","title":"Insert data into new or existing contacts"},{"location":"data/insert-data-sets/#insert-data-into-new-or-existing-contacts","text":"Data can only be created/inserted into the database whenever inserting or updating new or existing contacts. When using insert and update APIs such as Insert , ProfileInsert , Update , and ProfileUpdate , you are able to create/insert data into new or existing RawContacts respectively. For example, to insert an email into a new contact using the Insert API, Contacts ( context ) . insert () . rawContact { addEmail ( email ) } . commit () For more info, read Insert contacts . To insert an email into a new Profile contact using the ProfileInsert API, Contacts ( context ) . profile () . insert () . rawContact { addEmail ( email ) } . commit () For more info, read Insert device owner Contact profile . To insert an email into an existing contact using the Update API, Contacts ( context ) . update () . contacts ( existingContact . mutableCopy { addEmail ( email ) }) . commit () For more info, read Update contacts . To insert an email into an the existing Profile Contact using the ProfileUpdate API, Contacts ( context ) . profile () . update () . contact ( existingProfileContact . mutableCopy { addEmail ( email ) }) . commit () For more info, read Update device owner Contact profile .","title":"Insert data into new or existing contacts"},{"location":"data/insert-data-sets/#blank-data-are-not-inserted","text":"Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are ignored and are not inserted by insert APIs. For more info, read about Blank data .","title":"Blank data are not inserted"},{"location":"data/query-data-sets/","text":"Query specific data kinds \u00b6 This library provides the DataQueryFactory API that allows you to get a list of specific data kinds directly without having to get them from Contacts/RawContacts. An instance of the DataQueryFactory API is obtained by, val query = Contacts ( context ). data (). query () To retrieve all kinds of data via Contacts/RawContacts, read Query contacts and Query contacts (advanced) . Data queries \u00b6 The DataQueryFactory API provides instances of DataQuery for every data kind in the library. The full list of queries are defined in the DataQueryFactory interface. Here it is for reference, val dataQueryFactory = Contacts ( context ). data (). query () val addressesQuery = dataQueryFactory . addresses () val emailsQuery = dataQueryFactory . emails () val eventsQuery = dataQueryFactory . events () val groupMembershipsQuery = dataQueryFactory . groupMemberships () val imsQuery = dataQueryFactory . ims () val namesQuery = dataQueryFactory . names () val nicknamesQuery = dataQueryFactory . nicknames () val notesQuery = dataQueryFactory . notes () val organizationsQuery = dataQueryFactory . organizations () val phonesQuery = dataQueryFactory . phones () val relationsQuery = dataQueryFactory . relations () val sipAddressesQuery = dataQueryFactory . sipAddresses () val websitesQuery = dataQueryFactory . websites () // Photos are intentionally left out as it is internal to the library. These query instances will allow you to query only specific data kinds from all contacts. For example, to get all emails from all contacts, val emails = Contacts ( context ). data (). query (). emails (). find () To get all websites with a \".net\" extension from contacts with the given IDs, val websites = Contacts ( this ) . data () . query () . websites () . where { ( Website . Url endsWith \".net\" ) and ( Contact . Id `in` contactIds ) } . find () Specifying Accounts \u00b6 To limit the search to only those data associated with one of the given accounts, . accounts ( accounts ) For example, to limit the search to data belonging to only one account, . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) For more info, read Query for Accounts . If no accounts are specified (this function is not called or called with no Accounts), then all data are included in the search. A null Account may be provided here, which results in RawContacts with no associated Account to be included in the search. RawContacts without an associated account are considered local contacts or device-only contacts, which are not synced. For more info, read about Local (device-only) contacts . Note that this may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it. Including only specific data \u00b6 To include only the given set of fields in each of the matching data , . include ( fields ) For example, to only include the given name and family name in a names query, . include ( Fields . Name . GivenName , Fields . Name . FamilyName ) For more info, read Include only certain fields for read and write operations . Ordering \u00b6 To order resulting data using one or more fields, . orderBy ( fieldOrder ) For example, to order emails by type first and then email address, . orderBy ( Fields . Email . Type . asc (), Fields . Email . Address . asc ()) String comparisons ignores case by default. Each orderBys provides ignoreCase as an optional parameter. Use the corresponding fields in Fields to construct the orderBys. Limiting and offsetting \u00b6 To limit the amount of data returned and/or offset (skip) a specified number of data, . limit ( limit ) . offset ( offset ) For example, to only get a maximum 20 data, skipping the first 20, . limit ( 20 ) . offset ( 20 ) This is useful for pagination =) Note that it is recommended to limit the number of data when querying to increase performance and decrease memory cost. Executing the query \u00b6 To execute the query, . find () Cancelling the query \u00b6 To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val data = query . find { ! isActive } } } Performing the query asynchronously \u00b6 Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes. Performing the query with permission \u00b6 Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Profile data \u00b6 The DataQueryFactory API (and its DataQuery instances) also supports querying the Profile (device owner) contact data. To get an instance of this API for Profile data queries, val profileDataQueryFactory = Contacts ( context ). profile (). data (). query () All queries will be limited to the Profile, whether it exists or not. Custom data support \u00b6 The DataQueryFactory API (and its DataQuery instances) supports custom data. For more info, read Query custom data . Using the where function to specify matching criteria \u00b6 Use the corresponding contacts.core.Fields combined with the extensions from contacts.core.Where to form WHERE clauses. This docs page will not provide a tutorial on database where clauses. It assumes that you know the basics. If you don't know the basics, then search for sqlite where clause . For example, to get all nicknames from all contacts, val nicknames = Contacts ( context ). data (). query (). nicknames (). find () To get all birthday events from all contacts, val birthdayEvents = Contacts ( this ) . data () . query () . events () . where { Event . Type equalTo EventEntity . Type . BIRTHDAY } . find () Limitations \u00b6 This library only provides basic WHERE functions. It does not cover the entirety of SQLite, though the community may add more over time <3 Furthermore, this library is constrained by rules and limitations set by the Contacts Provider and the behavior of the native Contacts app. One such rule/limitation has resulted in this library not providing WHERE functions such as isNull or isNullOrEmpty to prevent making misleading queries. Removing a piece of existing data results in the deletion of the row in the Data table if that row no longer contains any meaningful data. This is the behavior of the native Contacts app. Therefore, querying for null fields is not possible. For example, there may be no Data rows that exist where the email address is null. Thus, a query to search for all emails where the address is null may always return no results.","title":"Query specific data kinds"},{"location":"data/query-data-sets/#query-specific-data-kinds","text":"This library provides the DataQueryFactory API that allows you to get a list of specific data kinds directly without having to get them from Contacts/RawContacts. An instance of the DataQueryFactory API is obtained by, val query = Contacts ( context ). data (). query () To retrieve all kinds of data via Contacts/RawContacts, read Query contacts and Query contacts (advanced) .","title":"Query specific data kinds"},{"location":"data/query-data-sets/#data-queries","text":"The DataQueryFactory API provides instances of DataQuery for every data kind in the library. The full list of queries are defined in the DataQueryFactory interface. Here it is for reference, val dataQueryFactory = Contacts ( context ). data (). query () val addressesQuery = dataQueryFactory . addresses () val emailsQuery = dataQueryFactory . emails () val eventsQuery = dataQueryFactory . events () val groupMembershipsQuery = dataQueryFactory . groupMemberships () val imsQuery = dataQueryFactory . ims () val namesQuery = dataQueryFactory . names () val nicknamesQuery = dataQueryFactory . nicknames () val notesQuery = dataQueryFactory . notes () val organizationsQuery = dataQueryFactory . organizations () val phonesQuery = dataQueryFactory . phones () val relationsQuery = dataQueryFactory . relations () val sipAddressesQuery = dataQueryFactory . sipAddresses () val websitesQuery = dataQueryFactory . websites () // Photos are intentionally left out as it is internal to the library. These query instances will allow you to query only specific data kinds from all contacts. For example, to get all emails from all contacts, val emails = Contacts ( context ). data (). query (). emails (). find () To get all websites with a \".net\" extension from contacts with the given IDs, val websites = Contacts ( this ) . data () . query () . websites () . where { ( Website . Url endsWith \".net\" ) and ( Contact . Id `in` contactIds ) } . find ()","title":"Data queries"},{"location":"data/query-data-sets/#specifying-accounts","text":"To limit the search to only those data associated with one of the given accounts, . accounts ( accounts ) For example, to limit the search to data belonging to only one account, . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) For more info, read Query for Accounts . If no accounts are specified (this function is not called or called with no Accounts), then all data are included in the search. A null Account may be provided here, which results in RawContacts with no associated Account to be included in the search. RawContacts without an associated account are considered local contacts or device-only contacts, which are not synced. For more info, read about Local (device-only) contacts . Note that this may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it.","title":"Specifying Accounts"},{"location":"data/query-data-sets/#including-only-specific-data","text":"To include only the given set of fields in each of the matching data , . include ( fields ) For example, to only include the given name and family name in a names query, . include ( Fields . Name . GivenName , Fields . Name . FamilyName ) For more info, read Include only certain fields for read and write operations .","title":"Including only specific data"},{"location":"data/query-data-sets/#ordering","text":"To order resulting data using one or more fields, . orderBy ( fieldOrder ) For example, to order emails by type first and then email address, . orderBy ( Fields . Email . Type . asc (), Fields . Email . Address . asc ()) String comparisons ignores case by default. Each orderBys provides ignoreCase as an optional parameter. Use the corresponding fields in Fields to construct the orderBys.","title":"Ordering"},{"location":"data/query-data-sets/#limiting-and-offsetting","text":"To limit the amount of data returned and/or offset (skip) a specified number of data, . limit ( limit ) . offset ( offset ) For example, to only get a maximum 20 data, skipping the first 20, . limit ( 20 ) . offset ( 20 ) This is useful for pagination =) Note that it is recommended to limit the number of data when querying to increase performance and decrease memory cost.","title":"Limiting and offsetting"},{"location":"data/query-data-sets/#executing-the-query","text":"To execute the query, . find ()","title":"Executing the query"},{"location":"data/query-data-sets/#cancelling-the-query","text":"To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val data = query . find { ! isActive } } }","title":"Cancelling the query"},{"location":"data/query-data-sets/#performing-the-query-asynchronously","text":"Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes.","title":"Performing the query asynchronously"},{"location":"data/query-data-sets/#performing-the-query-with-permission","text":"Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the query with permission"},{"location":"data/query-data-sets/#profile-data","text":"The DataQueryFactory API (and its DataQuery instances) also supports querying the Profile (device owner) contact data. To get an instance of this API for Profile data queries, val profileDataQueryFactory = Contacts ( context ). profile (). data (). query () All queries will be limited to the Profile, whether it exists or not.","title":"Profile data"},{"location":"data/query-data-sets/#custom-data-support","text":"The DataQueryFactory API (and its DataQuery instances) supports custom data. For more info, read Query custom data .","title":"Custom data support"},{"location":"data/query-data-sets/#using-the-where-function-to-specify-matching-criteria","text":"Use the corresponding contacts.core.Fields combined with the extensions from contacts.core.Where to form WHERE clauses. This docs page will not provide a tutorial on database where clauses. It assumes that you know the basics. If you don't know the basics, then search for sqlite where clause . For example, to get all nicknames from all contacts, val nicknames = Contacts ( context ). data (). query (). nicknames (). find () To get all birthday events from all contacts, val birthdayEvents = Contacts ( this ) . data () . query () . events () . where { Event . Type equalTo EventEntity . Type . BIRTHDAY } . find ()","title":"Using the where function to specify matching criteria"},{"location":"data/query-data-sets/#limitations","text":"This library only provides basic WHERE functions. It does not cover the entirety of SQLite, though the community may add more over time <3 Furthermore, this library is constrained by rules and limitations set by the Contacts Provider and the behavior of the native Contacts app. One such rule/limitation has resulted in this library not providing WHERE functions such as isNull or isNullOrEmpty to prevent making misleading queries. Removing a piece of existing data results in the deletion of the row in the Data table if that row no longer contains any meaningful data. This is the behavior of the native Contacts app. Therefore, querying for null fields is not possible. For example, there may be no Data rows that exist where the email address is null. Thus, a query to search for all emails where the address is null may always return no results.","title":"Limitations"},{"location":"data/update-data-sets/","text":"Update existing sets of data \u00b6 This library provides the DataUpdate API that allows you to update a list of any data kinds directly without having to update them via Contacts/RawContacts. An instance of the DataUpdate API is obtained by, val update = Contacts ( context ). data (). update () To update all kinds of data via Contacts/RawContacts, read Update contacts . A basic update \u00b6 To update a set of data, val updateResult = Contacts ( context ) . data () . update () . data ( data ) . commit () If you want to update a list of mutable emails and phones, val updateResult = Contacts ( context ) . data () . update () . data ( mutableEmails + mutablePhones ) . commit () Blank data are deleted \u00b6 Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are deleted by update APIs. For more info, read about Blank data . Including only specific data \u00b6 To include only the given set of fields (data) in each of the update operation, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations . Executing the update \u00b6 To execute the update, . commit () Handling the update result \u00b6 The commit function returns a Result , val contactsApi = Contacts ( context ) val mutableEmail = email . mutableCopy { ... } val mutablePhone = phone . mutableCopy { ... } val updateResult = contactsApi . date () . update () . data ( mutableEmail , mutablePhone ) . commit () To check if all updates succeeded, val allUpdatesSuccessful = updateResult . isSuccessful To check if a particular update succeeded, val emailUpdateSuccessful = updateResult . isSuccessful ( mutableEmail ) Once you have performed the updates, you can retrieve the updated data references via the DataQuery APIs, val updatedEmail = contactsApi . data () . query () . emails () . where { Email . Id equalTo emailId } . find () For more info, read Query specific data kinds . Alternatively, you may use the extensions provided in DataRefresh . To get the updated phone, val updatedPhone = phone . refresh ( contactsApi ) Cancelling the update \u00b6 To cancel an update amid execution, . commit { returnTrueIfUpdateShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel update processing as soon as possible. The function is called numerous times during update processing to check if processing should stop or continue. This gives you the option to cancel the update. For example, to automatically cancel the update inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val updateResult = update . commit { ! isActive } } } Performing the update and result processing asynchronously \u00b6 Updates are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the update with permission \u00b6 Updates require the android.permission.WRITE_CONTACTS permissions. If not granted, the update will do nothing and return a failed result. To perform the update with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Profile data \u00b6 The DataUpdate API also supports updating the Profile (device owner) contact data. To get an instance of this API for Profile data updates, val profileDataUpdate = Contacts ( context ). profile (). data (). update () All updates will be limited to the Profile, whether it exists or not. Custom data support \u00b6 The DataUpdate API supports custom data. For more info, read Update custom data .","title":"Update existing sets of data"},{"location":"data/update-data-sets/#update-existing-sets-of-data","text":"This library provides the DataUpdate API that allows you to update a list of any data kinds directly without having to update them via Contacts/RawContacts. An instance of the DataUpdate API is obtained by, val update = Contacts ( context ). data (). update () To update all kinds of data via Contacts/RawContacts, read Update contacts .","title":"Update existing sets of data"},{"location":"data/update-data-sets/#a-basic-update","text":"To update a set of data, val updateResult = Contacts ( context ) . data () . update () . data ( data ) . commit () If you want to update a list of mutable emails and phones, val updateResult = Contacts ( context ) . data () . update () . data ( mutableEmails + mutablePhones ) . commit ()","title":"A basic update"},{"location":"data/update-data-sets/#blank-data-are-deleted","text":"Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are deleted by update APIs. For more info, read about Blank data .","title":"Blank data are deleted"},{"location":"data/update-data-sets/#including-only-specific-data","text":"To include only the given set of fields (data) in each of the update operation, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations .","title":"Including only specific data"},{"location":"data/update-data-sets/#executing-the-update","text":"To execute the update, . commit ()","title":"Executing the update"},{"location":"data/update-data-sets/#handling-the-update-result","text":"The commit function returns a Result , val contactsApi = Contacts ( context ) val mutableEmail = email . mutableCopy { ... } val mutablePhone = phone . mutableCopy { ... } val updateResult = contactsApi . date () . update () . data ( mutableEmail , mutablePhone ) . commit () To check if all updates succeeded, val allUpdatesSuccessful = updateResult . isSuccessful To check if a particular update succeeded, val emailUpdateSuccessful = updateResult . isSuccessful ( mutableEmail ) Once you have performed the updates, you can retrieve the updated data references via the DataQuery APIs, val updatedEmail = contactsApi . data () . query () . emails () . where { Email . Id equalTo emailId } . find () For more info, read Query specific data kinds . Alternatively, you may use the extensions provided in DataRefresh . To get the updated phone, val updatedPhone = phone . refresh ( contactsApi )","title":"Handling the update result"},{"location":"data/update-data-sets/#cancelling-the-update","text":"To cancel an update amid execution, . commit { returnTrueIfUpdateShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel update processing as soon as possible. The function is called numerous times during update processing to check if processing should stop or continue. This gives you the option to cancel the update. For example, to automatically cancel the update inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val updateResult = update . commit { ! isActive } } }","title":"Cancelling the update"},{"location":"data/update-data-sets/#performing-the-update-and-result-processing-asynchronously","text":"Updates are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the update and result processing asynchronously"},{"location":"data/update-data-sets/#performing-the-update-with-permission","text":"Updates require the android.permission.WRITE_CONTACTS permissions. If not granted, the update will do nothing and return a failed result. To perform the update with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the update with permission"},{"location":"data/update-data-sets/#profile-data","text":"The DataUpdate API also supports updating the Profile (device owner) contact data. To get an instance of this API for Profile data updates, val profileDataUpdate = Contacts ( context ). profile (). data (). update () All updates will be limited to the Profile, whether it exists or not.","title":"Profile data"},{"location":"data/update-data-sets/#custom-data-support","text":"The DataUpdate API supports custom data. For more info, read Update custom data .","title":"Custom data support"},{"location":"debug/debug-blockednumber-provider-tables/","text":"Debug the Blocked Number Provider tables \u00b6 If you want to take a look at the contents of Blocked Number Provider database tables that this library uses, then use the debug module functions to print relevant columns and all rows of a particular table to the Logcat. This is useful if you are experiencing an issue and are trying to figure out if it is this library's fault or not. This is most useful for contributors of this library. It allows us to verify that the work we are doing is correct. Consumers may also use it, especially if they are building their own full-fledged contacts application. Context . logBlockedNumbersTable () This is not meant to be used in production code! \u00b6 DO NOT include usages of the debug module in your production code! It is only meant to be used as a debugging tool during development ! Debug functions do not depend on the core library \u00b6 Notice that the debug module does not depend on the core module, or any other modules in this project. This is done to ensure that whatever is being logged is independent of the core API implementation! This is important for debugging the core APIs during development. We wouldn't exactly want to debug the core APIs using the core APIs. That's just a recipe for disaster! Debug functions assume that privileges are acquired \u00b6 There are no permissions required for blocked numbers. However, there are privileges that must be acquired. For more info, read about Blocked numbers . If privileges are not acquired, the debug functions will not print any table rows.","title":"Debug the BlockedNumber Provider tables"},{"location":"debug/debug-blockednumber-provider-tables/#debug-the-blocked-number-provider-tables","text":"If you want to take a look at the contents of Blocked Number Provider database tables that this library uses, then use the debug module functions to print relevant columns and all rows of a particular table to the Logcat. This is useful if you are experiencing an issue and are trying to figure out if it is this library's fault or not. This is most useful for contributors of this library. It allows us to verify that the work we are doing is correct. Consumers may also use it, especially if they are building their own full-fledged contacts application. Context . logBlockedNumbersTable ()","title":"Debug the Blocked Number Provider tables"},{"location":"debug/debug-blockednumber-provider-tables/#this-is-not-meant-to-be-used-in-production-code","text":"DO NOT include usages of the debug module in your production code! It is only meant to be used as a debugging tool during development !","title":"This is not meant to be used in production code!"},{"location":"debug/debug-blockednumber-provider-tables/#debug-functions-do-not-depend-on-the-core-library","text":"Notice that the debug module does not depend on the core module, or any other modules in this project. This is done to ensure that whatever is being logged is independent of the core API implementation! This is important for debugging the core APIs during development. We wouldn't exactly want to debug the core APIs using the core APIs. That's just a recipe for disaster!","title":"Debug functions do not depend on the core library"},{"location":"debug/debug-blockednumber-provider-tables/#debug-functions-assume-that-privileges-are-acquired","text":"There are no permissions required for blocked numbers. However, there are privileges that must be acquired. For more info, read about Blocked numbers . If privileges are not acquired, the debug functions will not print any table rows.","title":"Debug functions assume that privileges are acquired"},{"location":"debug/debug-contacts-provider-tables/","text":"Debug the Contacts Provider tables \u00b6 If you want to take a look at the contents of Contacts Provider database tables that this library uses, then use the debug module functions to print relevant columns and all rows of a particular table to the Logcat. This is useful if you are experiencing an issue and are trying to figure out if it is this library's fault or not. This is most useful for contributors of this library. It allows us to verify that the work we are doing is correct. Consumers may also use it, especially if they are building their own full-fledged contacts application. Table Function Groups Context.logGroupsTable() AggregationExceptions Context.logAggregationExceptionsTable() Profile Context.logProfile() Contacts Context.logContactsTable() RawContacts Context.logRawContactsTable() Data Context.logDataTable() To log all of the above tables in a single call, Context . logContactsProviderTables () This is not meant to be used in production code! \u00b6 DO NOT include usages of the debug module in your production code! It is only meant to be used as a debugging tool during development ! There are several reasons why you should only use this for debugging. First, Contacts database tables may be very lengthy. Imagine trying to print thousands of contact data! It would slow down your app significantly if you log in the UI thread. Second, Contacts database tables will most likely contain sensitive, private information about your users. If you are working on a contacts app and you are logging your user's Contacts database table rows into remote tracking services for analytics or crash reporting, you could be violating GDPR depending on how you use that information. Be careful. This is why logging functions in the debug module are not customizable and are not part of the core API. Other forms of logging outside of the debug module implemented by this library allows consumers to uphold privacy laws. The debug module is a power tool that should only be used for local debugging purposes! Debug functions do not depend on the core library \u00b6 Notice that the debug module does not depend on the core module, or any other modules in this project. This is done to ensure that whatever is being logged is independent of the core API implementation! This is important for debugging the core APIs during development. We wouldn't exactly want to debug the core APIs using the core APIs. That's just a recipe for disaster! Debug functions assume that permissions have been granted \u00b6 If the read permission android.permission.READ_CONTACTS is not granted, the debug functions will not print any table rows. Debugging other tables \u00b6 To debug Blocked Number Provider tables, read Debug the Blocked Number Provider tables . To debug SIM Contacts table, read Debug the Sim Contacts table .","title":"Debug the Contacts Provider tables"},{"location":"debug/debug-contacts-provider-tables/#debug-the-contacts-provider-tables","text":"If you want to take a look at the contents of Contacts Provider database tables that this library uses, then use the debug module functions to print relevant columns and all rows of a particular table to the Logcat. This is useful if you are experiencing an issue and are trying to figure out if it is this library's fault or not. This is most useful for contributors of this library. It allows us to verify that the work we are doing is correct. Consumers may also use it, especially if they are building their own full-fledged contacts application. Table Function Groups Context.logGroupsTable() AggregationExceptions Context.logAggregationExceptionsTable() Profile Context.logProfile() Contacts Context.logContactsTable() RawContacts Context.logRawContactsTable() Data Context.logDataTable() To log all of the above tables in a single call, Context . logContactsProviderTables ()","title":"Debug the Contacts Provider tables"},{"location":"debug/debug-contacts-provider-tables/#this-is-not-meant-to-be-used-in-production-code","text":"DO NOT include usages of the debug module in your production code! It is only meant to be used as a debugging tool during development ! There are several reasons why you should only use this for debugging. First, Contacts database tables may be very lengthy. Imagine trying to print thousands of contact data! It would slow down your app significantly if you log in the UI thread. Second, Contacts database tables will most likely contain sensitive, private information about your users. If you are working on a contacts app and you are logging your user's Contacts database table rows into remote tracking services for analytics or crash reporting, you could be violating GDPR depending on how you use that information. Be careful. This is why logging functions in the debug module are not customizable and are not part of the core API. Other forms of logging outside of the debug module implemented by this library allows consumers to uphold privacy laws. The debug module is a power tool that should only be used for local debugging purposes!","title":"This is not meant to be used in production code!"},{"location":"debug/debug-contacts-provider-tables/#debug-functions-do-not-depend-on-the-core-library","text":"Notice that the debug module does not depend on the core module, or any other modules in this project. This is done to ensure that whatever is being logged is independent of the core API implementation! This is important for debugging the core APIs during development. We wouldn't exactly want to debug the core APIs using the core APIs. That's just a recipe for disaster!","title":"Debug functions do not depend on the core library"},{"location":"debug/debug-contacts-provider-tables/#debug-functions-assume-that-permissions-have-been-granted","text":"If the read permission android.permission.READ_CONTACTS is not granted, the debug functions will not print any table rows.","title":"Debug functions assume that permissions have been granted"},{"location":"debug/debug-contacts-provider-tables/#debugging-other-tables","text":"To debug Blocked Number Provider tables, read Debug the Blocked Number Provider tables . To debug SIM Contacts table, read Debug the Sim Contacts table .","title":"Debugging other tables"},{"location":"debug/debug-sim-contacts-tables/","text":"Debug the Sim Contacts table \u00b6 If you want to take a look at the contents of Sim Contact database tables that this library uses, then use the debug module functions to print relevant columns and all rows of a particular table to the Logcat. This is useful if you are experiencing an issue and are trying to figure out if it is this library's fault or not. This is most useful for contributors of this library. It allows us to verify that the work we are doing is correct. Consumers may also use it, especially if they are building their own full-fledged contacts application. Context . logSimContactsTable () This is not meant to be used in production code! \u00b6 DO NOT include usages of the debug module in your production code! It is only meant to be used as a debugging tool during development ! Debug functions do not depend on the core library \u00b6 Notice that the debug module does not depend on the core module, or any other modules in this project. This is done to ensure that whatever is being logged is independent of the core API implementation! This is important for debugging the core APIs during development. We wouldn't exactly want to debug the core APIs using the core APIs. That's just a recipe for disaster! Debug functions assume that permissions have been granted \u00b6 If the read permission android.permission.READ_CONTACTS is not granted, the debug functions will not print any table rows.","title":"Debug the Sim Contacts table"},{"location":"debug/debug-sim-contacts-tables/#debug-the-sim-contacts-table","text":"If you want to take a look at the contents of Sim Contact database tables that this library uses, then use the debug module functions to print relevant columns and all rows of a particular table to the Logcat. This is useful if you are experiencing an issue and are trying to figure out if it is this library's fault or not. This is most useful for contributors of this library. It allows us to verify that the work we are doing is correct. Consumers may also use it, especially if they are building their own full-fledged contacts application. Context . logSimContactsTable ()","title":"Debug the Sim Contacts table"},{"location":"debug/debug-sim-contacts-tables/#this-is-not-meant-to-be-used-in-production-code","text":"DO NOT include usages of the debug module in your production code! It is only meant to be used as a debugging tool during development !","title":"This is not meant to be used in production code!"},{"location":"debug/debug-sim-contacts-tables/#debug-functions-do-not-depend-on-the-core-library","text":"Notice that the debug module does not depend on the core module, or any other modules in this project. This is done to ensure that whatever is being logged is independent of the core API implementation! This is important for debugging the core APIs during development. We wouldn't exactly want to debug the core APIs using the core APIs. That's just a recipe for disaster!","title":"Debug functions do not depend on the core library"},{"location":"debug/debug-sim-contacts-tables/#debug-functions-assume-that-permissions-have-been-granted","text":"If the read permission android.permission.READ_CONTACTS is not granted, the debug functions will not print any table rows.","title":"Debug functions assume that permissions have been granted"},{"location":"entities/about-api-entities/","text":"API Entities \u00b6 First, it's important to understand the most basic concept of the Android Contacts Provider / ContactsContract . Afterwards, everything in this library should just make sense. There is only one thing you need to know outside of this library. The library handles the rest of the details so you don't have to! Contacts Provider / ContactsContract Basic Concept \u00b6 There are 3 main database tables used in dealing with contacts. These tables are all connected. Contacts Rows representing different people. E.G. John Doe RawContacts Rows that link Contacts rows to specific Accounts. E.G. John Doe from john.doe@gmail.com, John Doe from john.dow@hotmail.com Data Rows containing data (e.g. name, email) for a RawContacts row. E.G. John Doe from Gmail's name and email, John Doe from Hotmail's phone and address There are more tables but it won't be covered in this docs for brevity. In the example given (E.G.) above, there is one row in the Contacts table for the person John Doe there are 2 rows in the RawContacts table that make up the Contact John Doe there are 4 rows in the Data table belonging to the Contact John Doe. 2 of these rows belong to John Doe from Gmail and the other 2 belong to John Doe from Hotmail In the background, the Contacts Provider automatically performs the RawContacts linking/aggregation into a single Contact. To forcefully link or unlink sets of RawContacts, read Link unlink Contacts . In the background, the Contacts Provider syncs all data from the local database to the remote database and vice versa (depending on system contact sync settings). Read more in Sync contact data across devices . That's all you need to know! Hopefully it wasn't too much. I know it was difficult for me to grasp in the beginning =P. Once you internalize this one to many relationship between Contacts -> RawContacts -> Data , you have unlocked the full potential of this library and the world is at the palm of your hands ! Contacts API Entities \u00b6 This library provides entities that model everything in the Contacts Provider database. Contact Primarily contains a list of RawContacts that are associated with this contact. RawContact Contains contact data that belong to an account. There may be more than one RawContact per Contact. DataEntity A specific kind of data of a RawContact. These entities model the common data kinds that are provided by the Contacts Provider. Address Email Event GroupMembership Im Name Nickname Note Organization Phone Photo Relation SipAddress Website You can find all of the above in the contacts.core.entities package. Note that there are other entities that are not mentioned in this docs for brevity. All entities are Parcelable to support state retention during app/activity/fragment/view recreation. Each entity has an immutable version (typically returned by queries) and a mutable version (typically used by insert, update, and delete functions). Most immutable entities have a mutableCopy function that returns a mutable copy (typically to be used for inserts and updates and other mutating API functions). Custom data kinds may also be integrated into the contacts database (though not synced across devices). For more info, read Integrate custom data . Default native and custom data may be retrieved, set, or cleared. For more info, read Get set clear default Contact data . Data kinds count restrictions \u00b6 A RawContact may have at most one OR no limits of certain kinds of data. A RawContact may have 0 or 1 of each of these data kinds; Name Nickname Note Organization Photo SipAddress A RawContact may have 0, 1, or more of each of these data kinds; Address Email Event GroupMembership Im Phone Relation Website The Contacts Provider may or may not enforce these count restrictions. However, the native Contacts app imposes these restrictions. Therefore, this library also imposes these restrictions and disables consumers from violating them. The core library does not explicitly expose count restrictions to consumers. However, it is exposed when integrating custom data via the CustomDataCountRestriction . Data kinds Account restrictions \u00b6 Entries of some data kinds should not be allowed to exist for local RawContacts (those that are not associated with an Account). For more info, read about Local (device-only) contacts . Automatic data kinds creation \u00b6 An entry of each of the following data kinds are automatically created for all contacts, if not provided; GroupMembership , underlying value defaults to the account's default system group Name , underlying value defaults to null Nickname , underlying value defaults to null Note , underlying value defaults to null This automatic creation occur automatically in the background (typically after creation) only for RawContacts that are associated with an Account. If a valid account is provided, membership to the (auto add) system group is automatically created immediately by the Contacts Provider at the time of creation. The name, nickname, and note are automatically created at a later time. Note that the query APIs in this library do not return blanks in results. In this case, the Name , Nickname , and Note will not be included in the RawContact because their primary values are all null. Blanks are also ignored on insert and deleted on update. For more info, read about Blank data . If a valid account is not provided, no entries of the above are automatically created. To determine if a RawContact is associated with an Account or not, read Query for Accounts . Data integrity \u00b6 There is a section in the official Contacts Provider documentation about \"Data Integrity\"; https://developer.android.com/guide/topics/providers/contacts-provider#DataIntegrity It enumerates four general rules to follow to retain the \"integrity of data\" :D Paraphrasing in terms of this library, the rules are as follows; Always add a Name for every RawContact . Always link new Data to their parent RawContact . Change data only for those raw contacts that you own. Always use the constants defined in ContactsContract and its subclasses for authorities, content URIs, URI paths, column names, MIME types, and TYPE values. This library follows rules 2 and 4. Rule 1 is ignored because the native Contacts app also ignores that rule. Enforcing this rule means that a name has to be provided for every RawContact , which is not practical at all. Users should be able to create contacts with just an email or phone number, without a name. This library follows the native Contacts app behavior, which also disregards this rule =P Rule 3 is intentionally ignored. There are two kinds of data; a. those that are defined in the Contacts Provider (e.g. name, email, phone number, etc) b. those that are defined by other apps (e.g. custom data from other apps) This library allows modification of native data kinds and custom data kinds. Native data kinds should obviously be modifiable as it is the entire reason why the Contacts Provider exposes these data kinds to us in the first place. The question is, should this library provide functions for modifying (insert, update, delete) custom data defined by other apps/services (e.g. Google Contacts, WhatsApp, etc)? The answer to that will be determined when the time comes to support custom data from other apps in the future... Probably, yes! For more info, read Integrate custom data from other apps . Accessing contact data \u00b6 When you have an instance of Contact , you have complete (and correct) access to data stored in it. To access data of a Contact with only one RawContact, val contact : Contact val rawContact : RawContact = contact . rawContacts . first () Log . d ( \"Contact\" , \"\"\" ID: ${ contact . id } Lookup Key: ${ contact . lookupKey } Display name: ${ contact . displayNamePrimary } Display name alt: ${ contact . displayNameAlt } Photo Uri: ${ contact . photoUri } Thumbnail Uri: ${ contact . photoThumbnailUri } Last updated: ${ contact . lastUpdatedTimestamp } Starred?: ${ contact . options ?. starred } Send to voicemail?: ${ contact . options ?. sendToVoicemail } Ringtone: ${ contact . options ?. customRingtone } Addresses: ${ rawContact . addresses } Emails: ${ rawContact . emails } Events: ${ rawContact . events } Group memberships: ${ rawContact . groupMemberships } IMs: ${ rawContact . ims } Name: ${ rawContact . name } Nickname: ${ rawContact . nickname } Note: ${ rawContact . note } Organization: ${ rawContact . organization } Phones: ${ rawContact . phones } Relations: ${ rawContact . relations } SipAddress: ${ rawContact . sipAddress } Websites: ${ rawContact . websites } \"\"\" . trimIndent () // Photo require separate blocking function calls. ) To access data of a Contact with possibly more than one RawContact, we can use ContactData extensions to make our life easier, val contact : Contact Log . d ( \"Contact\" , \"\"\" ID: ${ contact . id } Lookup Key: ${ contact . lookupKey } Display name: ${ contact . displayNamePrimary } Display name alt: ${ contact . displayNameAlt } Photo Uri: ${ contact . photoUri } Thumbnail Uri: ${ contact . photoThumbnailUri } Last updated: ${ contact . lastUpdatedTimestamp } Starred?: ${ contact . options ?. starred } Send to voicemail?: ${ contact . options ?. sendToVoicemail } Ringtone: ${ contact . options ?. customRingtone } Aggregate data from all RawContacts of the contact ----------------------------------- Addresses: ${ contact . addressList () } Emails: ${ contact . emailList () } Events: ${ contact . eventList () } Group memberships: ${ contact . groupMembershipList () } IMs: ${ contact . imList () } Names: ${ contact . nameList () } Nicknames: ${ contact . nicknameList () } Notes: ${ contact . noteList () } Organizations: ${ contact . organizationList () } Phones: ${ contact . phoneList () } Relations: ${ contact . relationList () } SipAddresses: ${ contact . sipAddressList () } Websites: ${ contact . websiteList () } ----------------------------------- \"\"\" . trimIndent () // There are also aggregate data functions that return a sequence instead of a list. ) Each Contact may have more than one of the following data if the Contact is made up of 2 or more RawContacts; name, nickname, note, organization, sip address. For more info on how to easily aggregate data from all RawContacts in a Contact, read Convenience functions . To look into the actual Contacts Provider tables, read Debug the Contacts Provider tables . To learn more about the Contact lookup key, read about Contact lookup key vs ID . Redacting entities \u00b6 All Entity in this library are Redactable , which indicates that there could be sensitive private user data that could be redacted, for legal purposes. If you are logging contact data in production to remote data centers for analytics or crash reporting, then it is important to redact certain parts of every contact's data. For more info, read Redact entities and API input and output in production . Syncing contact data \u00b6 Syncing contact data, including groups, are done automatically by the Contacts Provider depending on the account sync settings. For more info, read Sync contact data across devices .","title":"API entities"},{"location":"entities/about-api-entities/#api-entities","text":"First, it's important to understand the most basic concept of the Android Contacts Provider / ContactsContract . Afterwards, everything in this library should just make sense. There is only one thing you need to know outside of this library. The library handles the rest of the details so you don't have to!","title":"API Entities"},{"location":"entities/about-api-entities/#contacts-provider-contactscontract-basic-concept","text":"There are 3 main database tables used in dealing with contacts. These tables are all connected. Contacts Rows representing different people. E.G. John Doe RawContacts Rows that link Contacts rows to specific Accounts. E.G. John Doe from john.doe@gmail.com, John Doe from john.dow@hotmail.com Data Rows containing data (e.g. name, email) for a RawContacts row. E.G. John Doe from Gmail's name and email, John Doe from Hotmail's phone and address There are more tables but it won't be covered in this docs for brevity. In the example given (E.G.) above, there is one row in the Contacts table for the person John Doe there are 2 rows in the RawContacts table that make up the Contact John Doe there are 4 rows in the Data table belonging to the Contact John Doe. 2 of these rows belong to John Doe from Gmail and the other 2 belong to John Doe from Hotmail In the background, the Contacts Provider automatically performs the RawContacts linking/aggregation into a single Contact. To forcefully link or unlink sets of RawContacts, read Link unlink Contacts . In the background, the Contacts Provider syncs all data from the local database to the remote database and vice versa (depending on system contact sync settings). Read more in Sync contact data across devices . That's all you need to know! Hopefully it wasn't too much. I know it was difficult for me to grasp in the beginning =P. Once you internalize this one to many relationship between Contacts -> RawContacts -> Data , you have unlocked the full potential of this library and the world is at the palm of your hands !","title":"Contacts Provider / ContactsContract Basic Concept"},{"location":"entities/about-api-entities/#contacts-api-entities","text":"This library provides entities that model everything in the Contacts Provider database. Contact Primarily contains a list of RawContacts that are associated with this contact. RawContact Contains contact data that belong to an account. There may be more than one RawContact per Contact. DataEntity A specific kind of data of a RawContact. These entities model the common data kinds that are provided by the Contacts Provider. Address Email Event GroupMembership Im Name Nickname Note Organization Phone Photo Relation SipAddress Website You can find all of the above in the contacts.core.entities package. Note that there are other entities that are not mentioned in this docs for brevity. All entities are Parcelable to support state retention during app/activity/fragment/view recreation. Each entity has an immutable version (typically returned by queries) and a mutable version (typically used by insert, update, and delete functions). Most immutable entities have a mutableCopy function that returns a mutable copy (typically to be used for inserts and updates and other mutating API functions). Custom data kinds may also be integrated into the contacts database (though not synced across devices). For more info, read Integrate custom data . Default native and custom data may be retrieved, set, or cleared. For more info, read Get set clear default Contact data .","title":"Contacts API Entities"},{"location":"entities/about-api-entities/#data-kinds-count-restrictions","text":"A RawContact may have at most one OR no limits of certain kinds of data. A RawContact may have 0 or 1 of each of these data kinds; Name Nickname Note Organization Photo SipAddress A RawContact may have 0, 1, or more of each of these data kinds; Address Email Event GroupMembership Im Phone Relation Website The Contacts Provider may or may not enforce these count restrictions. However, the native Contacts app imposes these restrictions. Therefore, this library also imposes these restrictions and disables consumers from violating them. The core library does not explicitly expose count restrictions to consumers. However, it is exposed when integrating custom data via the CustomDataCountRestriction .","title":"Data kinds count restrictions"},{"location":"entities/about-api-entities/#data-kinds-account-restrictions","text":"Entries of some data kinds should not be allowed to exist for local RawContacts (those that are not associated with an Account). For more info, read about Local (device-only) contacts .","title":"Data kinds Account restrictions"},{"location":"entities/about-api-entities/#automatic-data-kinds-creation","text":"An entry of each of the following data kinds are automatically created for all contacts, if not provided; GroupMembership , underlying value defaults to the account's default system group Name , underlying value defaults to null Nickname , underlying value defaults to null Note , underlying value defaults to null This automatic creation occur automatically in the background (typically after creation) only for RawContacts that are associated with an Account. If a valid account is provided, membership to the (auto add) system group is automatically created immediately by the Contacts Provider at the time of creation. The name, nickname, and note are automatically created at a later time. Note that the query APIs in this library do not return blanks in results. In this case, the Name , Nickname , and Note will not be included in the RawContact because their primary values are all null. Blanks are also ignored on insert and deleted on update. For more info, read about Blank data . If a valid account is not provided, no entries of the above are automatically created. To determine if a RawContact is associated with an Account or not, read Query for Accounts .","title":"Automatic data kinds creation"},{"location":"entities/about-api-entities/#data-integrity","text":"There is a section in the official Contacts Provider documentation about \"Data Integrity\"; https://developer.android.com/guide/topics/providers/contacts-provider#DataIntegrity It enumerates four general rules to follow to retain the \"integrity of data\" :D Paraphrasing in terms of this library, the rules are as follows; Always add a Name for every RawContact . Always link new Data to their parent RawContact . Change data only for those raw contacts that you own. Always use the constants defined in ContactsContract and its subclasses for authorities, content URIs, URI paths, column names, MIME types, and TYPE values. This library follows rules 2 and 4. Rule 1 is ignored because the native Contacts app also ignores that rule. Enforcing this rule means that a name has to be provided for every RawContact , which is not practical at all. Users should be able to create contacts with just an email or phone number, without a name. This library follows the native Contacts app behavior, which also disregards this rule =P Rule 3 is intentionally ignored. There are two kinds of data; a. those that are defined in the Contacts Provider (e.g. name, email, phone number, etc) b. those that are defined by other apps (e.g. custom data from other apps) This library allows modification of native data kinds and custom data kinds. Native data kinds should obviously be modifiable as it is the entire reason why the Contacts Provider exposes these data kinds to us in the first place. The question is, should this library provide functions for modifying (insert, update, delete) custom data defined by other apps/services (e.g. Google Contacts, WhatsApp, etc)? The answer to that will be determined when the time comes to support custom data from other apps in the future... Probably, yes! For more info, read Integrate custom data from other apps .","title":"Data integrity"},{"location":"entities/about-api-entities/#accessing-contact-data","text":"When you have an instance of Contact , you have complete (and correct) access to data stored in it. To access data of a Contact with only one RawContact, val contact : Contact val rawContact : RawContact = contact . rawContacts . first () Log . d ( \"Contact\" , \"\"\" ID: ${ contact . id } Lookup Key: ${ contact . lookupKey } Display name: ${ contact . displayNamePrimary } Display name alt: ${ contact . displayNameAlt } Photo Uri: ${ contact . photoUri } Thumbnail Uri: ${ contact . photoThumbnailUri } Last updated: ${ contact . lastUpdatedTimestamp } Starred?: ${ contact . options ?. starred } Send to voicemail?: ${ contact . options ?. sendToVoicemail } Ringtone: ${ contact . options ?. customRingtone } Addresses: ${ rawContact . addresses } Emails: ${ rawContact . emails } Events: ${ rawContact . events } Group memberships: ${ rawContact . groupMemberships } IMs: ${ rawContact . ims } Name: ${ rawContact . name } Nickname: ${ rawContact . nickname } Note: ${ rawContact . note } Organization: ${ rawContact . organization } Phones: ${ rawContact . phones } Relations: ${ rawContact . relations } SipAddress: ${ rawContact . sipAddress } Websites: ${ rawContact . websites } \"\"\" . trimIndent () // Photo require separate blocking function calls. ) To access data of a Contact with possibly more than one RawContact, we can use ContactData extensions to make our life easier, val contact : Contact Log . d ( \"Contact\" , \"\"\" ID: ${ contact . id } Lookup Key: ${ contact . lookupKey } Display name: ${ contact . displayNamePrimary } Display name alt: ${ contact . displayNameAlt } Photo Uri: ${ contact . photoUri } Thumbnail Uri: ${ contact . photoThumbnailUri } Last updated: ${ contact . lastUpdatedTimestamp } Starred?: ${ contact . options ?. starred } Send to voicemail?: ${ contact . options ?. sendToVoicemail } Ringtone: ${ contact . options ?. customRingtone } Aggregate data from all RawContacts of the contact ----------------------------------- Addresses: ${ contact . addressList () } Emails: ${ contact . emailList () } Events: ${ contact . eventList () } Group memberships: ${ contact . groupMembershipList () } IMs: ${ contact . imList () } Names: ${ contact . nameList () } Nicknames: ${ contact . nicknameList () } Notes: ${ contact . noteList () } Organizations: ${ contact . organizationList () } Phones: ${ contact . phoneList () } Relations: ${ contact . relationList () } SipAddresses: ${ contact . sipAddressList () } Websites: ${ contact . websiteList () } ----------------------------------- \"\"\" . trimIndent () // There are also aggregate data functions that return a sequence instead of a list. ) Each Contact may have more than one of the following data if the Contact is made up of 2 or more RawContacts; name, nickname, note, organization, sip address. For more info on how to easily aggregate data from all RawContacts in a Contact, read Convenience functions . To look into the actual Contacts Provider tables, read Debug the Contacts Provider tables . To learn more about the Contact lookup key, read about Contact lookup key vs ID .","title":"Accessing contact data"},{"location":"entities/about-api-entities/#redacting-entities","text":"All Entity in this library are Redactable , which indicates that there could be sensitive private user data that could be redacted, for legal purposes. If you are logging contact data in production to remote data centers for analytics or crash reporting, then it is important to redact certain parts of every contact's data. For more info, read Redact entities and API input and output in production .","title":"Redacting entities"},{"location":"entities/about-api-entities/#syncing-contact-data","text":"Syncing contact data, including groups, are done automatically by the Contacts Provider depending on the account sync settings. For more info, read Sync contact data across devices .","title":"Syncing contact data"},{"location":"entities/about-blank-contacts/","text":"Blank contacts \u00b6 Blank RawContacts and blank Contacts do not have any rows in the Data table. These do not have any non-blank data. An entity is blank if the concrete implementation of Entity.isBlank returns true. The Contacts Providers allows for RawContacts that have no rows in the Data table (let's call them \"blanks\") to exist. The native Contacts app does not allow insertion of new RawContacts without at least one data row. It also deletes blanks on update. Despite seemingly not allowing blanks, the native Contacts app shows them. This library provides APIs that follows the native Contacts app behavior by default but also allows you to override the default behavior. There are two scenarios where blanks may exist. Contact with RawContact(s) with no Data row(s). In this case, the Contact is blank as well as its RawContact(s). Contact that has RawContact with Data row(s) and a RawContact with no Data row. In this case, the Contact and the RawContact with Data row(s) are not blank but the RawContact with no Data row is blank. Blanks in queries \u00b6 A where clause that uses any fields from the Data table Fields will exclude blanks in the result (even if they are OR'ed) There are some joined fields that can be used to match blanks as long as no other fields are in the where clause ; Fields.Contact enables matching blank Contacts. The result will include all RawContact(s) belonging to the Contact(s), including blank(s). Examples; Fields.Contact.Id equalTo 5 Fields.Contact.Id in listOf(1,2,3) and Fields.Contact.DisplayNamePrimary contains \"a\" Fields.Contact.Options.Starred equalTo true Fields.RawContact enables matching blank RawContacts. The result will include all Contact(s) these belong to, including sibling RawContacts (blank and not blank). Examples; Fields.RawContact.Id equalTo 5 Fields.RawContact.Id notIn listOf(1,2,3) Blanks will not be included in the results even if they technically should if joined fields from other tables are in the where . In the below example, matching the Contact.Id to an existing blank Contact with Id of 5 will yield no results because it is joined by Fields.Email , which is not a part of Fields.Contact . It should technically return the blank Contact with Id of 5 because the OR operator is used. However, because we internally need to query the Contacts table to match the blanks, a DB exception will be thrown by the Contacts Provider because Fields.Email.Address (\"data1\" and \"mimetype\") are columns from the Data table that do not exist in the Contacts table. The same applies to the Fields.RawContact . Fields.Contact.Id equalTo 5 OR (Fields.Email.Address.isNotNull()) `Fields.RawContact.Id ... OR (Fields.Phone.Number...) Blank Contacts/RawContacts vs blank Data \u00b6 Blank RawContacts and blank Contacts do not have any rows in the Data table. These do not have any non-blank data. Blank data are data entities that have only null, empty, or blank primary value(s). For more info, read about Blank data .","title":"Blank contacts"},{"location":"entities/about-blank-contacts/#blank-contacts","text":"Blank RawContacts and blank Contacts do not have any rows in the Data table. These do not have any non-blank data. An entity is blank if the concrete implementation of Entity.isBlank returns true. The Contacts Providers allows for RawContacts that have no rows in the Data table (let's call them \"blanks\") to exist. The native Contacts app does not allow insertion of new RawContacts without at least one data row. It also deletes blanks on update. Despite seemingly not allowing blanks, the native Contacts app shows them. This library provides APIs that follows the native Contacts app behavior by default but also allows you to override the default behavior. There are two scenarios where blanks may exist. Contact with RawContact(s) with no Data row(s). In this case, the Contact is blank as well as its RawContact(s). Contact that has RawContact with Data row(s) and a RawContact with no Data row. In this case, the Contact and the RawContact with Data row(s) are not blank but the RawContact with no Data row is blank.","title":"Blank contacts"},{"location":"entities/about-blank-contacts/#blanks-in-queries","text":"A where clause that uses any fields from the Data table Fields will exclude blanks in the result (even if they are OR'ed) There are some joined fields that can be used to match blanks as long as no other fields are in the where clause ; Fields.Contact enables matching blank Contacts. The result will include all RawContact(s) belonging to the Contact(s), including blank(s). Examples; Fields.Contact.Id equalTo 5 Fields.Contact.Id in listOf(1,2,3) and Fields.Contact.DisplayNamePrimary contains \"a\" Fields.Contact.Options.Starred equalTo true Fields.RawContact enables matching blank RawContacts. The result will include all Contact(s) these belong to, including sibling RawContacts (blank and not blank). Examples; Fields.RawContact.Id equalTo 5 Fields.RawContact.Id notIn listOf(1,2,3) Blanks will not be included in the results even if they technically should if joined fields from other tables are in the where . In the below example, matching the Contact.Id to an existing blank Contact with Id of 5 will yield no results because it is joined by Fields.Email , which is not a part of Fields.Contact . It should technically return the blank Contact with Id of 5 because the OR operator is used. However, because we internally need to query the Contacts table to match the blanks, a DB exception will be thrown by the Contacts Provider because Fields.Email.Address (\"data1\" and \"mimetype\") are columns from the Data table that do not exist in the Contacts table. The same applies to the Fields.RawContact . Fields.Contact.Id equalTo 5 OR (Fields.Email.Address.isNotNull()) `Fields.RawContact.Id ... OR (Fields.Phone.Number...)","title":"Blanks in queries"},{"location":"entities/about-blank-contacts/#blank-contactsrawcontacts-vs-blank-data","text":"Blank RawContacts and blank Contacts do not have any rows in the Data table. These do not have any non-blank data. Blank data are data entities that have only null, empty, or blank primary value(s). For more info, read about Blank data .","title":"Blank Contacts/RawContacts vs blank Data"},{"location":"entities/about-blank-data/","text":"Blank data \u00b6 Blank data are data entities that have only null, empty, or blank primary value(s). An entity is blank if the concrete implementation of Entity.isBlank returns true. For example, Email only has one primary value, which is the address ... val blankEmail1 = NewEmail () val blankEmail2 = NewEmail ( address = null ) val blankEmail3 = NewEmail ( address = \"\" ) val blankEmail4 = NewEmail ( address = \" \" ) val blankEmail5 = NewEmail ( type = EmailEntity . Type . HOME ) val emailThatIsNotBlank = NewEmail ( address = \"john.doe@gmail.com\" ) Query APIs in this library do not return null, empty, or blank data in results if they somehow exist in the Contacts Provider database. Insert APIs also ignore blanks and are not inserted. Update APIs deletes blanks. This is the same behavior as the native Contacts app. This library does not allow you to modify this behavior. Blank Data vs blank Contacts/RawContacts \u00b6 Blank data are data entities that have only null, empty, or blank primary value(s). Blank RawContacts and blank Contacts do not have any rows in the Data table. These do not have any non-blank data. For more info, read about Blank contacts .","title":"Blank data"},{"location":"entities/about-blank-data/#blank-data","text":"Blank data are data entities that have only null, empty, or blank primary value(s). An entity is blank if the concrete implementation of Entity.isBlank returns true. For example, Email only has one primary value, which is the address ... val blankEmail1 = NewEmail () val blankEmail2 = NewEmail ( address = null ) val blankEmail3 = NewEmail ( address = \"\" ) val blankEmail4 = NewEmail ( address = \" \" ) val blankEmail5 = NewEmail ( type = EmailEntity . Type . HOME ) val emailThatIsNotBlank = NewEmail ( address = \"john.doe@gmail.com\" ) Query APIs in this library do not return null, empty, or blank data in results if they somehow exist in the Contacts Provider database. Insert APIs also ignore blanks and are not inserted. Update APIs deletes blanks. This is the same behavior as the native Contacts app. This library does not allow you to modify this behavior.","title":"Blank data"},{"location":"entities/about-blank-data/#blank-data-vs-blank-contactsrawcontacts","text":"Blank data are data entities that have only null, empty, or blank primary value(s). Blank RawContacts and blank Contacts do not have any rows in the Data table. These do not have any non-blank data. For more info, read about Blank contacts .","title":"Blank Data vs blank Contacts/RawContacts"},{"location":"entities/about-contact-lookup-key/","text":"Contact lookup key vs ID \u00b6 The Contact ID is a number in the Contacts table that serves as the unique identifier for a row in the local Contacts table . These look like any number used as an ID in a database table. For example; 4 , 8 , 15 , 16 , 23 , 42 , ... The Contact lookup key is a string that serves as the unique identifier for an aggregate contact in the local and remote databases . These look like randomly generated or hashed strings. For example; 2059i4a27289d88a0a4e7 , 0r62-2A2C2E , ... The official documentation for the Contact lookup key is, An opaque value that contains hints on how to find the contact if its row id changed as a result of a sync or aggregation. Let's dissect the documentation, \"if its row id changed\". This means that a Person's row ID can change! \"as a result of a sync\". The Contacts Provider allows sync adapters to modify the local and remote Contacts databases to ensure that Contact data is synced per user account. \"as a result of...aggregation\". Two or more Contacts (along with their constituent RawContacts) can be linked into a single Contact. When this happens, those Contacts will be consolidated into a single (existing) Contact row. Unlinking will result in the original Contacts prior to linking to have different IDs in the Contacts table because the previously deleted row IDs cannot be reused. Unlike the Contact ID, the lookup key is the same across devices (for contacts that are associated with an Account and are synced). The lookup key points to a person entity rather than just a row in a table. It is the unique identifier used by local and remote sync adapters to identify an aggregate contact. Actually, it seems like the Contact lookup key is a reference to a RawContact (or all of its constituent RawContacts). RawContacts have a reference to the parent Contact via the Contact ID. Similarly, the parent Contact has a reference to all of its constituent RawContacts via the lookup key. Note that RawContacts do not have a lookup key. It is exclusive to Contacts. When to use Contact lookup key vs Contact ID? \u00b6 Use the Contact lookup key when you need to save a reference to a Contact that you want to fetch after some period of time. Saving/restoring activity/fragment instance state. Saving to an external database, preferences, or files. Creating shortcuts. Use the Contact ID for everything else. Performing read/write operations in the same function call or session in your app. Performing read/write operations that require ID (e.g. Contact photo and options). How to get the Contact lookup key? \u00b6 Lookup keys are included in queries by default but are not required. This means that if you use do not invoke the include function in query APIs, then it will be included in the returned Contacts. However, if you do specify fields to include by invoking the include function, then you must explicitly specify the lookup key, . include ( Fields . Contact . LookupKey ) Contact s instances returned by the query will contain a value in the Contact.lookupKey property. For more info, read Include only certain fields for read and write operations . How to get Contacts using lookup keys? \u00b6 Use the decomposedLookupKeys functions in contacts.core.util.ContactLookupKey.kt to get contacts by lookup key, val contacts = query . where { decomposedLookupKeys ( lookupKeys ) whereOr { Contact . LookupKey contains it } }. find () Or use the lookupKeyIn extensions in contacts.core.util.ContactLookupKey.kt to get contacts by lookup key, val contacts = query . where { Contact . lookupKeyIn ( lookupKeys ) }. find () For an explanation on why you should use those functions instead of the lookup key directly, read the function documentation. Note that if the lookup key is a reference to a linked Contact (a Contact with two or more constituent RawContacts), and the linked Contact is unlinked, then the query will return multiple Contacts. For more info, read Query contacts (advanced) . Moving RawContacts between accounts and the lookup key \u00b6 Associating a local (device-only) RawContact to an Account will change the Contact lookup key. In general, set a RawContact's Account to something else will change the lookup key. In these cases, the changes to the lookup key will only be applied after the Contacts Provider and sync adapters sync the changes. This means that the local changes are not immediately applied. For more info, read Sync contact data across devices . Changing a RawContact's Account will result in a failed lookup using lookup keys prior to the Account change. For example, using the default AOSP Contacts app or the Google Contacts app... View a contact's details. Create a shortcut to it in the home screen (launcher). This shortcut uses the Contact lookup key (not the ID) to form a lookup URI. Change the RawContact's Account. Tap the shortcut in the home screen (launcher). Both Contacts apps will say that the Contact no longer exist or has been removed. This is not a bug. It is expected behavior due to the way the Contacts Provider works. For more info, read Associate local RawContacts to an Account . Linking/unlinking contacts and the lookup key \u00b6 Linking and unlinking RawContacts will change the value of the lookup key. However, as discussed in prior sections, you are still able to use the lookup key to find the aggregate Contact even though the Contact ID has changed. Linking/unlinking contacts will result in a successful lookup using lookup keys prior to the Account change. For example, using the default AOSP Contacts app or the Google Contacts app... View a contact's details. Create a shortcut to it in the home screen (launcher). This shortcut uses the Contact lookup key (not the ID) to form a lookup URI. Link the contact to another contact. Tap the shortcut in the home screen (launcher). Unlink the contact. Tap the shortcut in the home screen (launcher). In both cases, the shortcut successfully opens the correct aggregate Contact. For more info on linking/unlinking, read Link unlink Contacts . Developer notes (or for advanced users) \u00b6 The following section are note from developers of this library for other developers. It is copied from the DEV_NOTES . You may still read the following as a consumer of the library in case you need deeper insight. The Contacts._ID is the unique identifier for the row in the Contacts table. The Contacts.LOOKUP_KEY is the unique identifier for an aggregate Contact (a person). The _ID may change due to aggregation and sync. The same goes for the LOOKUP_KEY but unlike the ID it may still be used to find the aggregate contact. Unlike the Contact ID, the lookup key is the same across devices (for contacts that are associated with an Account and are synced). Note that I did the following investigation with a much larger data set. I simplified it here for brevity. Let's take a look at the following Contacts and RawContacts table rows, #### Contacts table Contact id: 55, lookupKey: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50, displayNamePrimary: Contact With Local RawContact Contact id: 56, lookupKey: 2059i6f5de8460f7f227e, displayNamePrimary: Contact With Synced RawContact #### RawContacts table RawContact id: 55, contactId: 55, displayNamePrimary: Contact With Local RawContact RawContact id: 56, contactId: 56, displayNamePrimary: Contact With Synced RawContact There are two Contacts each having one RawContact. Notice that the lookup keys are a bit different. Contact With Local RawContact: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 Contact With Synced RawContact: 2059i6f5de8460f7f227e The Contact with unsynced, device-only, local RawContact has a much longer (or shorter e.g. 0r62-2A2C2E) lookup key and starts with \"0r -\" and all characters after it are in uppercase. The other thing to notice is that the \"55\" in \"0r55-\" seems to be the same as the RawContact ID (I did a bit more experiments than what is written in these notes to confirm that it is indeed the RawContact ID and not the Contact ID). We probably don't need to worry about these details though the Contacts Provider probably uses these things internally. We also should not rely on it. However, it may be safe to assume that the Contact lookup key is a reference to a RawContact (or reference to more than one constituent RawContact when multiple RawContacts are linked). Again, an internal Contacts Provider detail we should not rely on BUT is probably relevant when implementing sync adapters. When we link the two, we get... Contact id: 55, lookupKey: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50.2059i6f5de8460f7f227e, displayNamePrimary: Contact With Synced RawContact #### RawContacts table RawContact id: 55, contactId: 55, displayNamePrimary: Contact With Local RawContact RawContact id: 56, contactId: 55, displayNamePrimary: Contact With Synced RawContact Notice, Contact with ID 56 has been deleted. Contact with ID 55 still exist with the lookup keys of both Contact 55 and 56 combined separated by a \".\" . This holds true in cases where two or more local-only or non-local-only RawContacts are linked. RawContacts remain unchanged except reference to Contact 56 has been replaced with 55. The primary display name of Contact 55 came from RawContact 55 prior to the link and now comes from RawContact 56 after the link. This primary name resolution is probably irrelevant so pay no attention to it. The most important part to notice is that the lookup keys get combined. The lookup uri is required to build a Contacts.CONTENT_LOOKUP_URI ... /** * A content:// style URI for this table that should be used to create * shortcuts or otherwise create long-term links to contacts. This URI * should always be followed by a \"/\" and the contact's {@link #LOOKUP_KEY}. * It can optionally also have a \"/\" and last known contact ID appended after * that. This \"complete\" format is an important optimization and is highly recommended. *

        * As long as the contact's row ID remains the same, this URI is * equivalent to {@link #CONTENT_URI}. If the contact's row ID changes * as a result of a sync or aggregation, this URI will look up the * contact using indirect information (sync IDs or constituent raw * contacts). *

        * Lookup key should be appended unencoded - it is stored in the encoded * form, ready for use in a URI. */ public static final Uri CONTENT_LOOKUP_URI = Uri . withAppendedPath ( CONTENT_URI , \"lookup\" ); /** * Build a {@link #CONTENT_LOOKUP_URI} lookup {@link Uri} using the * given {@link ContactsContract.Contacts#_ID} and {@link #LOOKUP_KEY}. *

        * Returns null if unable to construct a valid lookup URI from the * provided parameters. */ public static Uri getLookupUri ( long contactId , String lookupKey ) { if ( TextUtils . isEmpty ( lookupKey )) { return null ; } return ContentUris . withAppendedId ( Uri . withAppendedPath ( Contacts . CONTENT_LOOKUP_URI , lookupKey ), contactId ); } From the lookup uri, we can lookup the Contact row... public static Uri lookupContact ( ContentResolver resolver , Uri lookupUri ) { ... } Or simply get the Contact ID... // code inside `public static Uri lookupContact` resolver . query ( lookupUri , new String [] { Contacts . _ID }, null , null , null ) However, given that the lookup key of the deleted Contact 56 still lives on, it is possible to get the linked Contact 55 using the lookup key of Contact 56 using our standard query APIs! . where { Contact . LookupKey contains lookupKey } The above is correct as long as these assumptions hold true; the lookup key is unique there is no lookup key that can contain a shorter lookup key the Contact ID fails this test because a smaller number is contained in a larger number synced contacts have shorter lookup keys than local contacts. However, local contacts' lookup keys are capitalized whereas synced contact are not. Also, there seems to be other differences in pattern between long and short lookup keys. It should be safe to make this assumption. Until the community finds that this assumption is flawed, we'll assume that it is true! For now, we can avoid having to create another API or extensions just for using lookup keys . When we unlink , we get... #### Contacts table Contact id: 55, lookupKey: 2059i6f5de8460f7f227e, displayNamePrimary: Contact With Synced RawContact Contact id: 58, lookupKey: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50, displayNamePrimary: Contact With Local RawContact #### RawContacts table RawContact id: 55, contactId: 58, displayNamePrimary: Contact With Local RawContact RawContact id: 56, contactId: 55, displayNamePrimary: Contact With Synced RawContact Notice, A new Contact row with ID of 58 is created. The lookup keys are separated and distributed between Contact 55 and 58. RawContact 55 Contact reference has been set to Contact 58. Let's compare the Contact-RawContact relationship before and after linking and then unlinking. Contact ID Lookup Key RawContact.Contact ID Before 55, 56 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50, 2059i6f5de8460f7f227e 55, 56 After 55, 58 2059i6f5de8460f7f227e, 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 58, 55 Notice, Contact ID 55 swapped lookup keys with the former Contact 56 (now 58). RawContact ID 55 swapped Contact reference with RawContact 56. The Contact IDs and lookup keys got shuffled BUT the Contact-RawContact relationship remains the same if using the lookup keys as point of reference! Here is another way to look at the table, using the lookup key as the constant... Lookup Key Before After 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 Contact 55, RawContact 55 Contact 58, RawContact 55 2059i6f5de8460f7f227e Contact 56, RawContact 56 Contact 55, RawContact 56 Notice that the indirect relationship between the lookup key and RawContacts remains the same before and after the link-unlink even though the Contact IDs changed. As mentioned earlier in this section, the \"55\" in \"0r55-\" seems to be referencing the RawContact ID. In other words, since local RawContacts are not synced or tracked in a remote database where Contacts -> RawContacts mappings exist, the Contacts Provider most likely uses this \"0r -\" pattern to make the connection. This is not really relevant for us as we are not relying on this mechanism. I'm just pointing out my observations, which could be incorrect. This means that... If users of this library saved a reference Contact ID 55, then a link-unlink (or sync adapter functions) occur. Getting Contact by ID 55 will result in the RawContact-Data of the former Contact 56 to be returned. This is a bug! Same goes if users saved a reference to Contact ID 56. If users of this library saved a reference to the lookup keys, then a link-unlink (or sync adapter functions) occur. Getting Contact by lookup key will result in the correct RawContact-Data to be returned. So when to use Contact ID vs lookup key? Lookup key: for a reference to a Contact that needs to be loaded after some period of time. Saving/restoring activity/fragment instance state. Saving to an external database, preferences, or files. Creating shortcuts. ID: for everything else. Performing read/write operations in the same function call or session in your app. Performing read/write operations that require ID (e.g. Contact photo and options). Another thing to check is what happens when associating a local RawContact to an Account (move from device to Account) and vice versa. Is the lookup key of the Contact affected? After associating the local RawContact to an Account... #### Contacts table Contact id: 58, lookupKey: 2059i4abd4a8f8ff89642 #### RawContacts table RawContact id: 55, contactId: 58 The lookup key changed but the Contact ID remained the same! In this case, loading a reference to the previously local Contact will fail! I verified that this is indeed the behavior of the native (AOSP) Contacts app. Moving the RawContact from device to Google using Google Contacts app while having Contact details activity opened in the AOSP Contacts app will result in \"error Contact does not exist\" message in the AOSP Contacts app! The RawContact and its Data also remained the same in this case. Removing the account from it results in... #### Contacts table Contact id: 59, lookupKey: 0r58-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 #### RawContacts table RawContact id: 58, contactId: 59 The Contact and RawContacts row have been deleted and new rows have been created to replace them! I also verified that the Data rows have also been deleted and new rows have been created to replace them! This stuff is not really relevant for lookup key but still good to know for implementing moving between accounts in the future.","title":"Contact lookup key vs ID"},{"location":"entities/about-contact-lookup-key/#contact-lookup-key-vs-id","text":"The Contact ID is a number in the Contacts table that serves as the unique identifier for a row in the local Contacts table . These look like any number used as an ID in a database table. For example; 4 , 8 , 15 , 16 , 23 , 42 , ... The Contact lookup key is a string that serves as the unique identifier for an aggregate contact in the local and remote databases . These look like randomly generated or hashed strings. For example; 2059i4a27289d88a0a4e7 , 0r62-2A2C2E , ... The official documentation for the Contact lookup key is, An opaque value that contains hints on how to find the contact if its row id changed as a result of a sync or aggregation. Let's dissect the documentation, \"if its row id changed\". This means that a Person's row ID can change! \"as a result of a sync\". The Contacts Provider allows sync adapters to modify the local and remote Contacts databases to ensure that Contact data is synced per user account. \"as a result of...aggregation\". Two or more Contacts (along with their constituent RawContacts) can be linked into a single Contact. When this happens, those Contacts will be consolidated into a single (existing) Contact row. Unlinking will result in the original Contacts prior to linking to have different IDs in the Contacts table because the previously deleted row IDs cannot be reused. Unlike the Contact ID, the lookup key is the same across devices (for contacts that are associated with an Account and are synced). The lookup key points to a person entity rather than just a row in a table. It is the unique identifier used by local and remote sync adapters to identify an aggregate contact. Actually, it seems like the Contact lookup key is a reference to a RawContact (or all of its constituent RawContacts). RawContacts have a reference to the parent Contact via the Contact ID. Similarly, the parent Contact has a reference to all of its constituent RawContacts via the lookup key. Note that RawContacts do not have a lookup key. It is exclusive to Contacts.","title":"Contact lookup key vs ID"},{"location":"entities/about-contact-lookup-key/#when-to-use-contact-lookup-key-vs-contact-id","text":"Use the Contact lookup key when you need to save a reference to a Contact that you want to fetch after some period of time. Saving/restoring activity/fragment instance state. Saving to an external database, preferences, or files. Creating shortcuts. Use the Contact ID for everything else. Performing read/write operations in the same function call or session in your app. Performing read/write operations that require ID (e.g. Contact photo and options).","title":"When to use Contact lookup key vs Contact ID?"},{"location":"entities/about-contact-lookup-key/#how-to-get-the-contact-lookup-key","text":"Lookup keys are included in queries by default but are not required. This means that if you use do not invoke the include function in query APIs, then it will be included in the returned Contacts. However, if you do specify fields to include by invoking the include function, then you must explicitly specify the lookup key, . include ( Fields . Contact . LookupKey ) Contact s instances returned by the query will contain a value in the Contact.lookupKey property. For more info, read Include only certain fields for read and write operations .","title":"How to get the Contact lookup key?"},{"location":"entities/about-contact-lookup-key/#how-to-get-contacts-using-lookup-keys","text":"Use the decomposedLookupKeys functions in contacts.core.util.ContactLookupKey.kt to get contacts by lookup key, val contacts = query . where { decomposedLookupKeys ( lookupKeys ) whereOr { Contact . LookupKey contains it } }. find () Or use the lookupKeyIn extensions in contacts.core.util.ContactLookupKey.kt to get contacts by lookup key, val contacts = query . where { Contact . lookupKeyIn ( lookupKeys ) }. find () For an explanation on why you should use those functions instead of the lookup key directly, read the function documentation. Note that if the lookup key is a reference to a linked Contact (a Contact with two or more constituent RawContacts), and the linked Contact is unlinked, then the query will return multiple Contacts. For more info, read Query contacts (advanced) .","title":"How to get Contacts using lookup keys?"},{"location":"entities/about-contact-lookup-key/#moving-rawcontacts-between-accounts-and-the-lookup-key","text":"Associating a local (device-only) RawContact to an Account will change the Contact lookup key. In general, set a RawContact's Account to something else will change the lookup key. In these cases, the changes to the lookup key will only be applied after the Contacts Provider and sync adapters sync the changes. This means that the local changes are not immediately applied. For more info, read Sync contact data across devices . Changing a RawContact's Account will result in a failed lookup using lookup keys prior to the Account change. For example, using the default AOSP Contacts app or the Google Contacts app... View a contact's details. Create a shortcut to it in the home screen (launcher). This shortcut uses the Contact lookup key (not the ID) to form a lookup URI. Change the RawContact's Account. Tap the shortcut in the home screen (launcher). Both Contacts apps will say that the Contact no longer exist or has been removed. This is not a bug. It is expected behavior due to the way the Contacts Provider works. For more info, read Associate local RawContacts to an Account .","title":"Moving RawContacts between accounts and the lookup key"},{"location":"entities/about-contact-lookup-key/#linkingunlinking-contacts-and-the-lookup-key","text":"Linking and unlinking RawContacts will change the value of the lookup key. However, as discussed in prior sections, you are still able to use the lookup key to find the aggregate Contact even though the Contact ID has changed. Linking/unlinking contacts will result in a successful lookup using lookup keys prior to the Account change. For example, using the default AOSP Contacts app or the Google Contacts app... View a contact's details. Create a shortcut to it in the home screen (launcher). This shortcut uses the Contact lookup key (not the ID) to form a lookup URI. Link the contact to another contact. Tap the shortcut in the home screen (launcher). Unlink the contact. Tap the shortcut in the home screen (launcher). In both cases, the shortcut successfully opens the correct aggregate Contact. For more info on linking/unlinking, read Link unlink Contacts .","title":"Linking/unlinking contacts and the lookup key"},{"location":"entities/about-contact-lookup-key/#developer-notes-or-for-advanced-users","text":"The following section are note from developers of this library for other developers. It is copied from the DEV_NOTES . You may still read the following as a consumer of the library in case you need deeper insight. The Contacts._ID is the unique identifier for the row in the Contacts table. The Contacts.LOOKUP_KEY is the unique identifier for an aggregate Contact (a person). The _ID may change due to aggregation and sync. The same goes for the LOOKUP_KEY but unlike the ID it may still be used to find the aggregate contact. Unlike the Contact ID, the lookup key is the same across devices (for contacts that are associated with an Account and are synced). Note that I did the following investigation with a much larger data set. I simplified it here for brevity. Let's take a look at the following Contacts and RawContacts table rows, #### Contacts table Contact id: 55, lookupKey: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50, displayNamePrimary: Contact With Local RawContact Contact id: 56, lookupKey: 2059i6f5de8460f7f227e, displayNamePrimary: Contact With Synced RawContact #### RawContacts table RawContact id: 55, contactId: 55, displayNamePrimary: Contact With Local RawContact RawContact id: 56, contactId: 56, displayNamePrimary: Contact With Synced RawContact There are two Contacts each having one RawContact. Notice that the lookup keys are a bit different. Contact With Local RawContact: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 Contact With Synced RawContact: 2059i6f5de8460f7f227e The Contact with unsynced, device-only, local RawContact has a much longer (or shorter e.g. 0r62-2A2C2E) lookup key and starts with \"0r -\" and all characters after it are in uppercase. The other thing to notice is that the \"55\" in \"0r55-\" seems to be the same as the RawContact ID (I did a bit more experiments than what is written in these notes to confirm that it is indeed the RawContact ID and not the Contact ID). We probably don't need to worry about these details though the Contacts Provider probably uses these things internally. We also should not rely on it. However, it may be safe to assume that the Contact lookup key is a reference to a RawContact (or reference to more than one constituent RawContact when multiple RawContacts are linked). Again, an internal Contacts Provider detail we should not rely on BUT is probably relevant when implementing sync adapters. When we link the two, we get... Contact id: 55, lookupKey: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50.2059i6f5de8460f7f227e, displayNamePrimary: Contact With Synced RawContact #### RawContacts table RawContact id: 55, contactId: 55, displayNamePrimary: Contact With Local RawContact RawContact id: 56, contactId: 55, displayNamePrimary: Contact With Synced RawContact Notice, Contact with ID 56 has been deleted. Contact with ID 55 still exist with the lookup keys of both Contact 55 and 56 combined separated by a \".\" . This holds true in cases where two or more local-only or non-local-only RawContacts are linked. RawContacts remain unchanged except reference to Contact 56 has been replaced with 55. The primary display name of Contact 55 came from RawContact 55 prior to the link and now comes from RawContact 56 after the link. This primary name resolution is probably irrelevant so pay no attention to it. The most important part to notice is that the lookup keys get combined. The lookup uri is required to build a Contacts.CONTENT_LOOKUP_URI ... /** * A content:// style URI for this table that should be used to create * shortcuts or otherwise create long-term links to contacts. This URI * should always be followed by a \"/\" and the contact's {@link #LOOKUP_KEY}. * It can optionally also have a \"/\" and last known contact ID appended after * that. This \"complete\" format is an important optimization and is highly recommended. *

        * As long as the contact's row ID remains the same, this URI is * equivalent to {@link #CONTENT_URI}. If the contact's row ID changes * as a result of a sync or aggregation, this URI will look up the * contact using indirect information (sync IDs or constituent raw * contacts). *

        * Lookup key should be appended unencoded - it is stored in the encoded * form, ready for use in a URI. */ public static final Uri CONTENT_LOOKUP_URI = Uri . withAppendedPath ( CONTENT_URI , \"lookup\" ); /** * Build a {@link #CONTENT_LOOKUP_URI} lookup {@link Uri} using the * given {@link ContactsContract.Contacts#_ID} and {@link #LOOKUP_KEY}. *

        * Returns null if unable to construct a valid lookup URI from the * provided parameters. */ public static Uri getLookupUri ( long contactId , String lookupKey ) { if ( TextUtils . isEmpty ( lookupKey )) { return null ; } return ContentUris . withAppendedId ( Uri . withAppendedPath ( Contacts . CONTENT_LOOKUP_URI , lookupKey ), contactId ); } From the lookup uri, we can lookup the Contact row... public static Uri lookupContact ( ContentResolver resolver , Uri lookupUri ) { ... } Or simply get the Contact ID... // code inside `public static Uri lookupContact` resolver . query ( lookupUri , new String [] { Contacts . _ID }, null , null , null ) However, given that the lookup key of the deleted Contact 56 still lives on, it is possible to get the linked Contact 55 using the lookup key of Contact 56 using our standard query APIs! . where { Contact . LookupKey contains lookupKey } The above is correct as long as these assumptions hold true; the lookup key is unique there is no lookup key that can contain a shorter lookup key the Contact ID fails this test because a smaller number is contained in a larger number synced contacts have shorter lookup keys than local contacts. However, local contacts' lookup keys are capitalized whereas synced contact are not. Also, there seems to be other differences in pattern between long and short lookup keys. It should be safe to make this assumption. Until the community finds that this assumption is flawed, we'll assume that it is true! For now, we can avoid having to create another API or extensions just for using lookup keys . When we unlink , we get... #### Contacts table Contact id: 55, lookupKey: 2059i6f5de8460f7f227e, displayNamePrimary: Contact With Synced RawContact Contact id: 58, lookupKey: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50, displayNamePrimary: Contact With Local RawContact #### RawContacts table RawContact id: 55, contactId: 58, displayNamePrimary: Contact With Local RawContact RawContact id: 56, contactId: 55, displayNamePrimary: Contact With Synced RawContact Notice, A new Contact row with ID of 58 is created. The lookup keys are separated and distributed between Contact 55 and 58. RawContact 55 Contact reference has been set to Contact 58. Let's compare the Contact-RawContact relationship before and after linking and then unlinking. Contact ID Lookup Key RawContact.Contact ID Before 55, 56 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50, 2059i6f5de8460f7f227e 55, 56 After 55, 58 2059i6f5de8460f7f227e, 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 58, 55 Notice, Contact ID 55 swapped lookup keys with the former Contact 56 (now 58). RawContact ID 55 swapped Contact reference with RawContact 56. The Contact IDs and lookup keys got shuffled BUT the Contact-RawContact relationship remains the same if using the lookup keys as point of reference! Here is another way to look at the table, using the lookup key as the constant... Lookup Key Before After 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 Contact 55, RawContact 55 Contact 58, RawContact 55 2059i6f5de8460f7f227e Contact 56, RawContact 56 Contact 55, RawContact 56 Notice that the indirect relationship between the lookup key and RawContacts remains the same before and after the link-unlink even though the Contact IDs changed. As mentioned earlier in this section, the \"55\" in \"0r55-\" seems to be referencing the RawContact ID. In other words, since local RawContacts are not synced or tracked in a remote database where Contacts -> RawContacts mappings exist, the Contacts Provider most likely uses this \"0r -\" pattern to make the connection. This is not really relevant for us as we are not relying on this mechanism. I'm just pointing out my observations, which could be incorrect. This means that... If users of this library saved a reference Contact ID 55, then a link-unlink (or sync adapter functions) occur. Getting Contact by ID 55 will result in the RawContact-Data of the former Contact 56 to be returned. This is a bug! Same goes if users saved a reference to Contact ID 56. If users of this library saved a reference to the lookup keys, then a link-unlink (or sync adapter functions) occur. Getting Contact by lookup key will result in the correct RawContact-Data to be returned. So when to use Contact ID vs lookup key? Lookup key: for a reference to a Contact that needs to be loaded after some period of time. Saving/restoring activity/fragment instance state. Saving to an external database, preferences, or files. Creating shortcuts. ID: for everything else. Performing read/write operations in the same function call or session in your app. Performing read/write operations that require ID (e.g. Contact photo and options). Another thing to check is what happens when associating a local RawContact to an Account (move from device to Account) and vice versa. Is the lookup key of the Contact affected? After associating the local RawContact to an Account... #### Contacts table Contact id: 58, lookupKey: 2059i4abd4a8f8ff89642 #### RawContacts table RawContact id: 55, contactId: 58 The lookup key changed but the Contact ID remained the same! In this case, loading a reference to the previously local Contact will fail! I verified that this is indeed the behavior of the native (AOSP) Contacts app. Moving the RawContact from device to Google using Google Contacts app while having Contact details activity opened in the AOSP Contacts app will result in \"error Contact does not exist\" message in the AOSP Contacts app! The RawContact and its Data also remained the same in this case. Removing the account from it results in... #### Contacts table Contact id: 59, lookupKey: 0r58-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 #### RawContacts table RawContact id: 58, contactId: 59 The Contact and RawContacts row have been deleted and new rows have been created to replace them! I also verified that the Data rows have also been deleted and new rows have been created to replace them! This stuff is not really relevant for lookup key but still good to know for implementing moving between accounts in the future.","title":"Developer notes (or for advanced users)"},{"location":"entities/about-local-contacts/","text":"Local (device-only) contacts \u00b6 Contacts, or more specifically RawContacts, that are not associated with an android.accounts.Account are local to each device and will not be synced across devices. This means that any RawContacts you create, update, or delete will NOT be synced on any device or remote service as it is not associated with any account. For more info, read Sync contact data across devices . Associating a local RawContact to an Account \u00b6 Local RawContacts can be associated to an Account to enable syncing. For more info, read Associate local RawContacts to an Account . Adding an Account to the device \u00b6 Depending on the API level, the Contacts Provider behaves differently when the user adds an account to the device. Lollipop (API 22) and below When an Account is added, from a state where no accounts have yet been added to the system, the Contacts Provider automatically sets all of the null accountName and accountType in the RawContacts table to that Account's name and type. RawContacts inserted without an associated account will automatically get assigned to an account if there are any available. This may take a few seconds, whenever the Contacts Provider decides to do it. Dissociating RawContacts from Accounts will result in the Contacts Provider associating those back to an Account. Marshmallow (API 23) and above The Contacts Provider no longer associates local RawContacts to an account when an account is or becomes available. Local contacts remain local. Account removal Removing the Account will remove all of the associated rows in the RawContact, Data, and Groups tables locally . This includes user Profile data in those tables. Note that when all RawContacts of a Contact is removed, the Contact is also automatically removed by the Contacts Provider. Data kinds Account restrictions \u00b6 Entries of some data kinds should not be allowed to exist for local RawContacts. The native Contacts app hides the following UI fields when inserting or updating local RawContacts. To enforce this behavior, this library ignores all of the above during inserts and updates for local raw contacts. These data kinds are; GroupMembership Groups can only exist if it is associated with an Account. Therefore, memberships to groups is not possible when there is no associated Account. Event It is not clear why this requires an associated Account. Maybe because these are typically birth dates that users expect to be synced with their calendar across devices? Relation It is not clear why this requires an associated Account... The Contacts Provider may or may not enforce these Account restrictions. However, the native Contacts app imposes these restrictions. Therefore, this library also imposes these restrictions and disables consumers from violating them.","title":"Local (device-only) contacts"},{"location":"entities/about-local-contacts/#local-device-only-contacts","text":"Contacts, or more specifically RawContacts, that are not associated with an android.accounts.Account are local to each device and will not be synced across devices. This means that any RawContacts you create, update, or delete will NOT be synced on any device or remote service as it is not associated with any account. For more info, read Sync contact data across devices .","title":"Local (device-only) contacts"},{"location":"entities/about-local-contacts/#associating-a-local-rawcontact-to-an-account","text":"Local RawContacts can be associated to an Account to enable syncing. For more info, read Associate local RawContacts to an Account .","title":"Associating a local RawContact to an Account"},{"location":"entities/about-local-contacts/#adding-an-account-to-the-device","text":"Depending on the API level, the Contacts Provider behaves differently when the user adds an account to the device. Lollipop (API 22) and below When an Account is added, from a state where no accounts have yet been added to the system, the Contacts Provider automatically sets all of the null accountName and accountType in the RawContacts table to that Account's name and type. RawContacts inserted without an associated account will automatically get assigned to an account if there are any available. This may take a few seconds, whenever the Contacts Provider decides to do it. Dissociating RawContacts from Accounts will result in the Contacts Provider associating those back to an Account. Marshmallow (API 23) and above The Contacts Provider no longer associates local RawContacts to an account when an account is or becomes available. Local contacts remain local. Account removal Removing the Account will remove all of the associated rows in the RawContact, Data, and Groups tables locally . This includes user Profile data in those tables. Note that when all RawContacts of a Contact is removed, the Contact is also automatically removed by the Contacts Provider.","title":"Adding an Account to the device"},{"location":"entities/about-local-contacts/#data-kinds-account-restrictions","text":"Entries of some data kinds should not be allowed to exist for local RawContacts. The native Contacts app hides the following UI fields when inserting or updating local RawContacts. To enforce this behavior, this library ignores all of the above during inserts and updates for local raw contacts. These data kinds are; GroupMembership Groups can only exist if it is associated with an Account. Therefore, memberships to groups is not possible when there is no associated Account. Event It is not clear why this requires an associated Account. Maybe because these are typically birth dates that users expect to be synced with their calendar across devices? Relation It is not clear why this requires an associated Account... The Contacts Provider may or may not enforce these Account restrictions. However, the native Contacts app imposes these restrictions. Therefore, this library also imposes these restrictions and disables consumers from violating them.","title":"Data kinds Account restrictions"},{"location":"entities/include-only-desired-data/","text":"Include only certain fields for read and write operations \u00b6 When using query APIs such as Query , BroadQuery , ProfileQuery , DataQuery , you are able to specify all or only some kinds of data that you want to be included in the returned results. When using insert APIs such as Insert and ProfileInsert , you are able to specify all or only some kinds of data that you want to be included in the insert operation. When using update APIs such as Update , ProfileUpdate , and DataUpdate , you are able to specify all or only some kinds of data that you want to be included in the update operation. Each field corresponds with an Entity property. For example, to include only the contact display name, organization company, and all phone number fields, query . include ( mutableSetOf < AbstractDataField > (). apply { add ( Fields . Contact . DisplayNamePrimary ) add ( Fields . Organization . Company ) addAll ( Fields . Phone . all ) }) The following properties are populated with non-blank data (or null if no data is found), Contact { displayNamePrimary RawContact { organization { company } phones { number normalizedNumber type label } } } To explicitly include everything, query . include ( Fields . all ) Not invoking the include function will default to including everything, including custom data. The above code will exclude custom data. Read the Custom data support section for more info. The matching contacts may have non-null data for each of the included fields. Fields that are included will not guarantee non-null data in the returned contact instances because some data may actually be null in the database. If no fields are specified, then all fields are included. Otherwise, only the specified fields will be included in addition to required API fields (e.g. IDs), which are always included. Note that this may affect performance. It is recommended to only include fields that will be used to save CPU and memory. Custom data support \u00b6 The include function supports registered custom data fields, which my be combined with native (non-custom) data fields. By default, not calling the include function will include all fields, including custom data. However, the below code will include all native fields but exclude custom data; . include ( Fields . all ) If you want to include everything, including custom data, and for some reason you must invoke the include function, . include ( Fields . all + contactsApi . customDataRegistry . allFields ()) Performing updates on entities with partial includes \u00b6 When the query include function is used, only certain data will be included in the returned entities. All other data are guaranteed to be null (except for those in Fields.Required ). When performing updates on entities that have only partial data included, make sure to use the same included fields in the update operation as the included fields used in the query. This will ensure that the set of data queried and updated are the same. For example, in order to get and set only email addresses and leave everything the same in the database... val contacts = query . include ( Fields . Email . Address ). find () val mutableContacts = setEmailAddresses ( contacts ) update . contacts ( mutableContacts ). include ( Fields . Email . Address ). commit () On the other hand, you may intentionally include only some data and perform updates on all data (not just the included ones) to effectively delete all non-included data. This is, currently, a feature- not a bug! For example, in order to get and set only email addresses and set all other data to null (such as phone numbers, name, etc) in the database... val contacts = query . include ( Fields . Email . Address ). find () val mutableContacts = setEmailAddresses ( contacts ) update . contacts ( mutableContacts ). include ( Fields . all ). commit () This gives you the most flexibility when it comes to specifying what fields to include/exclude in queries, inserts, and updates, which will allow you to do things beyond your wildest imagination!","title":"Include only certain fields for read and write operations"},{"location":"entities/include-only-desired-data/#include-only-certain-fields-for-read-and-write-operations","text":"When using query APIs such as Query , BroadQuery , ProfileQuery , DataQuery , you are able to specify all or only some kinds of data that you want to be included in the returned results. When using insert APIs such as Insert and ProfileInsert , you are able to specify all or only some kinds of data that you want to be included in the insert operation. When using update APIs such as Update , ProfileUpdate , and DataUpdate , you are able to specify all or only some kinds of data that you want to be included in the update operation. Each field corresponds with an Entity property. For example, to include only the contact display name, organization company, and all phone number fields, query . include ( mutableSetOf < AbstractDataField > (). apply { add ( Fields . Contact . DisplayNamePrimary ) add ( Fields . Organization . Company ) addAll ( Fields . Phone . all ) }) The following properties are populated with non-blank data (or null if no data is found), Contact { displayNamePrimary RawContact { organization { company } phones { number normalizedNumber type label } } } To explicitly include everything, query . include ( Fields . all ) Not invoking the include function will default to including everything, including custom data. The above code will exclude custom data. Read the Custom data support section for more info. The matching contacts may have non-null data for each of the included fields. Fields that are included will not guarantee non-null data in the returned contact instances because some data may actually be null in the database. If no fields are specified, then all fields are included. Otherwise, only the specified fields will be included in addition to required API fields (e.g. IDs), which are always included. Note that this may affect performance. It is recommended to only include fields that will be used to save CPU and memory.","title":"Include only certain fields for read and write operations"},{"location":"entities/include-only-desired-data/#custom-data-support","text":"The include function supports registered custom data fields, which my be combined with native (non-custom) data fields. By default, not calling the include function will include all fields, including custom data. However, the below code will include all native fields but exclude custom data; . include ( Fields . all ) If you want to include everything, including custom data, and for some reason you must invoke the include function, . include ( Fields . all + contactsApi . customDataRegistry . allFields ())","title":"Custom data support"},{"location":"entities/include-only-desired-data/#performing-updates-on-entities-with-partial-includes","text":"When the query include function is used, only certain data will be included in the returned entities. All other data are guaranteed to be null (except for those in Fields.Required ). When performing updates on entities that have only partial data included, make sure to use the same included fields in the update operation as the included fields used in the query. This will ensure that the set of data queried and updated are the same. For example, in order to get and set only email addresses and leave everything the same in the database... val contacts = query . include ( Fields . Email . Address ). find () val mutableContacts = setEmailAddresses ( contacts ) update . contacts ( mutableContacts ). include ( Fields . Email . Address ). commit () On the other hand, you may intentionally include only some data and perform updates on all data (not just the included ones) to effectively delete all non-included data. This is, currently, a feature- not a bug! For example, in order to get and set only email addresses and set all other data to null (such as phone numbers, name, etc) in the database... val contacts = query . include ( Fields . Email . Address ). find () val mutableContacts = setEmailAddresses ( contacts ) update . contacts ( mutableContacts ). include ( Fields . all ). commit () This gives you the most flexibility when it comes to specifying what fields to include/exclude in queries, inserts, and updates, which will allow you to do things beyond your wildest imagination!","title":"Performing updates on entities with partial includes"},{"location":"entities/redact-apis-and-entities/","text":"Redact entities and API input and output in production \u00b6 All of the entities and Create (Query), Read (Query), Update, and Delete APIs (a.k.a CRUD APIs) provided in this library are Redactable . Redactables indicates that there could be sensitive private user data that could be redacted, for legal purposes such as upholding GDPR guidelines. If you are logging contact data in production to remote data centers for analytics or crash reporting, then it is important to redact certain parts of every contact's data. For more info on logging, read Log API input and output . DISCLAIMER: This is NOT legal advice! \u00b6 This library is written and maintained purely by software developers with no official education or certifications in any facet of law. Please review the redacted outputs of the APIs and entities within this library with your legal team! This library will not be held liable for any privacy violations! With that out of the way, let's move on to the good stuff =) Redactable entities \u00b6 All Entity in this library are Redactable . For example, Contact: id=1, email { address=\"vestrel00@gmail.com\" }, phone { number=\"(555) 555-5555\" }, etc when redacted, Contact: id=1, email { address=\"*******************\" }, phone { number=\"************\" }, etc Notice that all characters in private user data are replaced with \"*\". Redacted strings are not as useful as the non-redacted counterpart. However, we still have the following valuable information; is the string null or not? how long is the string? Database row IDs (and typically non-string properties) do not have to be redacted unless they contain sensitive information. The redactedCopy function will return an actual copy of the entity, except with sensitive data redacted. In addition to logging, this will allow consumers to do cool things like implementing a redacted contact view! Imagine a button that the user can press to redact everything in their contact form. Cool? Yes! Useful? Maybe? :grin: Redacted copies have isRedacted set to true to indicate that data has already been redacted. Redactable APIs \u00b6 All Create (Query), Read (Query), Update, and Delete APIs (a.k.a CRUD APIs) provided in this library are Redactable . For example, Query { rawContactsWhere: (account_name LIKE 'test@gmail.com' ESCAPE '\\') AND (account_type LIKE 'com.google' ESCAPE '\\') where: data1 LIKE '%@gmail.com%' ESCAPE '\\' AND mimetype = 'vnd.android.cursor.item/email_v2' isRedacted: false // the rest is omitted for brevity } Query.Result { Number of contacts found: 2 First contact: Contact( id=46, rawContacts=[ RawContact( id=45, contactId=46, addresses=[Address(id=329, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=WORK, label=null, formattedAddress=1200 Park Ave, street=1200 Park Ave, poBox=null, neighborhood=null, city=null, region=null, postcode=null, country=null, isRedacted=false)], emails=[ Email(id=318, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=WORK, label=null, address=buzz.lightyear@pixar.com, isRedacted=false), Email(id=319, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=HOME, label=null, address=buzz@lightyear.net, isRedacted=false) ], events=[ Event(id=317, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=BIRTHDAY, label=null, date=EventDate(year=1995, month=10, dayOfMonth=22, isRedacted=false), isRedacted=false), Event(id=320, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=ANNIVERSARY, label=null, date=EventDate(year=2022, month=0, dayOfMonth=1, isRedacted=false), isRedacted=false) ], // the rest is omitted for brevity ) ] ) isRedacted: false } when redacted, Query { rawContactsWhere: (account_name LIKE '*******************' ESCAPE '\\') AND (account_type LIKE '**********' ESCAPE '\\') where: data1 LIKE '%**********%' ESCAPE '\\' AND mimetype = 'vnd.android.cursor.item/email_v2' isRedacted: true // the rest is omitted for brevity } Query.Result { Number of contacts found: 2 First contact: Contact( id=46, rawContacts=[ RawContact( id=45, contactId=46, addresses=[Address(id=329, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=WORK, label=null, formattedAddress=*************, street=*************, poBox=null, neighborhood=null, city=null, region=null, postcode=null, country=null, isRedacted=true)], emails=[ Email(id=318, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=WORK, label=null, address=************************, isRedacted=true), Email(id=319, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=HOME, label=null, address=******************, isRedacted=true) ], events=[ Event(id=317, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=BIRTHDAY, label=null, date=EventDate(year=null, month=0, dayOfMonth=1, isRedacted=true), isRedacted=true), Event(id=320, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=ANNIVERSARY, label=null, date=EventDate(year=null, month=0, dayOfMonth=1, isRedacted=true), isRedacted=true)], ], // the rest is omitted for brevity ) ] ) isRedacted: true } Insert and update operations on redacted entities \u00b6 This library will not stop you from using redacted copies in insert and update APIs. You could build some cool stuff using it. I'll let your imagination take over from here =) Logging API input and output \u00b6 All terminal API functions such as find and commit can be automatically logged prior and post execution to get visibility on input and output. All log outputs are also redactable! For more info on logging, read Log API input and output . Developer notes \u00b6 I know that we cannot prevent consumers of this API from violating privacy laws if they really want to. BUT, the library should provide consumers an easy way to be GDPR-compliant! This is not necessary for all libraries to implement but this library deals with sensitive, private user data. Therefore, we need to be extra careful and provide consumers a GDPR-compliant way to log everything in this library!","title":"Redact entities and API input and output in production"},{"location":"entities/redact-apis-and-entities/#redact-entities-and-api-input-and-output-in-production","text":"All of the entities and Create (Query), Read (Query), Update, and Delete APIs (a.k.a CRUD APIs) provided in this library are Redactable . Redactables indicates that there could be sensitive private user data that could be redacted, for legal purposes such as upholding GDPR guidelines. If you are logging contact data in production to remote data centers for analytics or crash reporting, then it is important to redact certain parts of every contact's data. For more info on logging, read Log API input and output .","title":"Redact entities and API input and output in production"},{"location":"entities/redact-apis-and-entities/#disclaimer-this-is-not-legal-advice","text":"This library is written and maintained purely by software developers with no official education or certifications in any facet of law. Please review the redacted outputs of the APIs and entities within this library with your legal team! This library will not be held liable for any privacy violations! With that out of the way, let's move on to the good stuff =)","title":"DISCLAIMER: This is NOT legal advice!"},{"location":"entities/redact-apis-and-entities/#redactable-entities","text":"All Entity in this library are Redactable . For example, Contact: id=1, email { address=\"vestrel00@gmail.com\" }, phone { number=\"(555) 555-5555\" }, etc when redacted, Contact: id=1, email { address=\"*******************\" }, phone { number=\"************\" }, etc Notice that all characters in private user data are replaced with \"*\". Redacted strings are not as useful as the non-redacted counterpart. However, we still have the following valuable information; is the string null or not? how long is the string? Database row IDs (and typically non-string properties) do not have to be redacted unless they contain sensitive information. The redactedCopy function will return an actual copy of the entity, except with sensitive data redacted. In addition to logging, this will allow consumers to do cool things like implementing a redacted contact view! Imagine a button that the user can press to redact everything in their contact form. Cool? Yes! Useful? Maybe? :grin: Redacted copies have isRedacted set to true to indicate that data has already been redacted.","title":"Redactable entities"},{"location":"entities/redact-apis-and-entities/#redactable-apis","text":"All Create (Query), Read (Query), Update, and Delete APIs (a.k.a CRUD APIs) provided in this library are Redactable . For example, Query { rawContactsWhere: (account_name LIKE 'test@gmail.com' ESCAPE '\\') AND (account_type LIKE 'com.google' ESCAPE '\\') where: data1 LIKE '%@gmail.com%' ESCAPE '\\' AND mimetype = 'vnd.android.cursor.item/email_v2' isRedacted: false // the rest is omitted for brevity } Query.Result { Number of contacts found: 2 First contact: Contact( id=46, rawContacts=[ RawContact( id=45, contactId=46, addresses=[Address(id=329, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=WORK, label=null, formattedAddress=1200 Park Ave, street=1200 Park Ave, poBox=null, neighborhood=null, city=null, region=null, postcode=null, country=null, isRedacted=false)], emails=[ Email(id=318, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=WORK, label=null, address=buzz.lightyear@pixar.com, isRedacted=false), Email(id=319, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=HOME, label=null, address=buzz@lightyear.net, isRedacted=false) ], events=[ Event(id=317, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=BIRTHDAY, label=null, date=EventDate(year=1995, month=10, dayOfMonth=22, isRedacted=false), isRedacted=false), Event(id=320, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=ANNIVERSARY, label=null, date=EventDate(year=2022, month=0, dayOfMonth=1, isRedacted=false), isRedacted=false) ], // the rest is omitted for brevity ) ] ) isRedacted: false } when redacted, Query { rawContactsWhere: (account_name LIKE '*******************' ESCAPE '\\') AND (account_type LIKE '**********' ESCAPE '\\') where: data1 LIKE '%**********%' ESCAPE '\\' AND mimetype = 'vnd.android.cursor.item/email_v2' isRedacted: true // the rest is omitted for brevity } Query.Result { Number of contacts found: 2 First contact: Contact( id=46, rawContacts=[ RawContact( id=45, contactId=46, addresses=[Address(id=329, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=WORK, label=null, formattedAddress=*************, street=*************, poBox=null, neighborhood=null, city=null, region=null, postcode=null, country=null, isRedacted=true)], emails=[ Email(id=318, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=WORK, label=null, address=************************, isRedacted=true), Email(id=319, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=HOME, label=null, address=******************, isRedacted=true) ], events=[ Event(id=317, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=BIRTHDAY, label=null, date=EventDate(year=null, month=0, dayOfMonth=1, isRedacted=true), isRedacted=true), Event(id=320, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=ANNIVERSARY, label=null, date=EventDate(year=null, month=0, dayOfMonth=1, isRedacted=true), isRedacted=true)], ], // the rest is omitted for brevity ) ] ) isRedacted: true }","title":"Redactable APIs"},{"location":"entities/redact-apis-and-entities/#insert-and-update-operations-on-redacted-entities","text":"This library will not stop you from using redacted copies in insert and update APIs. You could build some cool stuff using it. I'll let your imagination take over from here =)","title":"Insert and update operations on redacted entities"},{"location":"entities/redact-apis-and-entities/#logging-api-input-and-output","text":"All terminal API functions such as find and commit can be automatically logged prior and post execution to get visibility on input and output. All log outputs are also redactable! For more info on logging, read Log API input and output .","title":"Logging API input and output"},{"location":"entities/redact-apis-and-entities/#developer-notes","text":"I know that we cannot prevent consumers of this API from violating privacy laws if they really want to. BUT, the library should provide consumers an easy way to be GDPR-compliant! This is not necessary for all libraries to implement but this library deals with sensitive, private user data. Therefore, we need to be extra careful and provide consumers a GDPR-compliant way to log everything in this library!","title":"Developer notes"},{"location":"entities/sync-contact-data/","text":"Sync contact data across devices \u00b6 Syncing contact data, including groups, are done automatically by the Contacts Provider depending on the account sync settings. You can typically find these account sync settings via Settings -> Accounts -> -> Account sync -> \"Contacts\" . Of course, in addition to having Contacts syncing enabled in settings, you must also have network connection to sync between the device and remote servers. When you have Contacts syncing enabled, as long as the android.accounts.Account has active sync adapters and remote services and you have network connection, data belonging to that account (e.g. \"vestrel00@gmail.com\" is a Google account) are synced across devices and online. This means that any contacts you create, update, or delete will be synced on all devices and services associated with that account. Besides Google Accounts, there is also Samsung, Yahoo, MSN/Hotmail, etc. Syncing contacts across devices is possible with sync adapters and Contacts' lookup key. For more info, read about Contact lookup key vs ID . Adding or removing Accounts \u00b6 When an Account is added to the system and Contacts syncing is enabled and there is network connection, the Contacts Provider will automatically fetch all Contacts, RawContacts, Data, and Groups that belong to that Account. Similarly, when an Account is removed from the system though regardless of Contacts syncing enabled or network availability, the Contacts Provider will automatically remove Contacts, RawContacts, Data, and Groups that belong to that Account. Only contacts that are associated with an Account are synced \u00b6 More specifically, RawContacts that are not associated with an Account (local, device-only) are not synced. Syncing is account specific, which is why you must turn on Contact syncing in the system settings. For example, data belonging to a RawContact that is associated with a Google account (e.g. Gmail) will be available anywhere the Google account is used; in any Android or iOS device, a web browser, etc... Data is synced by Google\u2019s sync adapters between devices and their remote servers. Syncing depends on the account sync settings, which can be configured in the system settings app and possibly through some remote configuration. For more info, read about Local (device-only) contacts . When are changes synced? \u00b6 In general, the Contacts Provider and the registered sync adapters are responsible for triggering sync events as long as Contacts sync is enabled for the Account in the system settings. You can manually trigger a sync through the system sync settings. Some events that will probably trigger a sync are; Getting network connection from a state where there was not network connection (offline -> online). Adding an Account. Removing an Account Until changes are synced, local changes will not take effect. Some examples are; RawContact rows are marked for deletion but remain until synced. Group rows are marked for deletion but remain until synced. New lookup key is not assigned after associating a local RawContact to an Account. Some custom data provided in this library are not synced \u00b6 The Gender , HandleName , Pokemon , RpgStats , and RpgProfession custom data will not be synced because they are not account specific and they have no sync adapters and no remote service to interface with. For more info, read Integrate custom data . Custom data from other apps may be synced \u00b6 This library does not sync contact data that belongs to other apps and services. For example, Google Contacts , WhatsApp, and other apps define their own set of custom data that their own sync adapters sync with their own remote services, which requires authentication. For more info, read Integrate custom data from other apps . This library does not provide sync adapters \u00b6 This library does not have any APIs related to syncing. It is considered out of scope of this library as it requires access to remote databases and account-specific data. Let's talk about it though. However, it is good to know how it works if you just want more insight :grin:. https://developer.android.com/guide/topics/providers/contacts-provider#SyncAdapters The Contacts Provider is specifically designed for handling synchronization of contacts data between a device and an online service. This allows users to download existing data to a new device and upload existing data to a new account. Synchronization also ensures that users have the latest data at hand, regardless of the source of additions and changes. Another advantage of synchronization is that it makes contacts data available even when the device is not connected to the network. Although you can implement synchronization in a variety of ways, the Android system provides a plug-in synchronization framework that automates the following tasks: Checking network availability. Scheduling and executing synchronization, based on user preferences. Restarting synchronizations that have stopped. To use this framework, you supply a sync adapter plug-in. Each sync adapter is unique to a service and content provider, but can handle multiple account names for the same service. The framework also allows multiple sync adapters for the same service and provider. This library does not provide any sync adapters. Instead, it relies on existing sync adapters to do the syncing. Sync adapters and syncing are really out of scope of this library. Syncing is its own thing that typically happens outside of an application UI. This library is focused on Create, Read, Update, and Delete (CRUD) operations on native and custom data to and from the local database. Syncing the local database to and from a remote database in the background is a totally different story altogether :grin: For more info, read Integrate custom data .","title":"Sync contact data across devices"},{"location":"entities/sync-contact-data/#sync-contact-data-across-devices","text":"Syncing contact data, including groups, are done automatically by the Contacts Provider depending on the account sync settings. You can typically find these account sync settings via Settings -> Accounts -> -> Account sync -> \"Contacts\" . Of course, in addition to having Contacts syncing enabled in settings, you must also have network connection to sync between the device and remote servers. When you have Contacts syncing enabled, as long as the android.accounts.Account has active sync adapters and remote services and you have network connection, data belonging to that account (e.g. \"vestrel00@gmail.com\" is a Google account) are synced across devices and online. This means that any contacts you create, update, or delete will be synced on all devices and services associated with that account. Besides Google Accounts, there is also Samsung, Yahoo, MSN/Hotmail, etc. Syncing contacts across devices is possible with sync adapters and Contacts' lookup key. For more info, read about Contact lookup key vs ID .","title":"Sync contact data across devices"},{"location":"entities/sync-contact-data/#adding-or-removing-accounts","text":"When an Account is added to the system and Contacts syncing is enabled and there is network connection, the Contacts Provider will automatically fetch all Contacts, RawContacts, Data, and Groups that belong to that Account. Similarly, when an Account is removed from the system though regardless of Contacts syncing enabled or network availability, the Contacts Provider will automatically remove Contacts, RawContacts, Data, and Groups that belong to that Account.","title":"Adding or removing Accounts"},{"location":"entities/sync-contact-data/#only-contacts-that-are-associated-with-an-account-are-synced","text":"More specifically, RawContacts that are not associated with an Account (local, device-only) are not synced. Syncing is account specific, which is why you must turn on Contact syncing in the system settings. For example, data belonging to a RawContact that is associated with a Google account (e.g. Gmail) will be available anywhere the Google account is used; in any Android or iOS device, a web browser, etc... Data is synced by Google\u2019s sync adapters between devices and their remote servers. Syncing depends on the account sync settings, which can be configured in the system settings app and possibly through some remote configuration. For more info, read about Local (device-only) contacts .","title":"Only contacts that are associated with an Account are synced"},{"location":"entities/sync-contact-data/#when-are-changes-synced","text":"In general, the Contacts Provider and the registered sync adapters are responsible for triggering sync events as long as Contacts sync is enabled for the Account in the system settings. You can manually trigger a sync through the system sync settings. Some events that will probably trigger a sync are; Getting network connection from a state where there was not network connection (offline -> online). Adding an Account. Removing an Account Until changes are synced, local changes will not take effect. Some examples are; RawContact rows are marked for deletion but remain until synced. Group rows are marked for deletion but remain until synced. New lookup key is not assigned after associating a local RawContact to an Account.","title":"When are changes synced?"},{"location":"entities/sync-contact-data/#some-custom-data-provided-in-this-library-are-not-synced","text":"The Gender , HandleName , Pokemon , RpgStats , and RpgProfession custom data will not be synced because they are not account specific and they have no sync adapters and no remote service to interface with. For more info, read Integrate custom data .","title":"Some custom data provided in this library are not synced"},{"location":"entities/sync-contact-data/#custom-data-from-other-apps-may-be-synced","text":"This library does not sync contact data that belongs to other apps and services. For example, Google Contacts , WhatsApp, and other apps define their own set of custom data that their own sync adapters sync with their own remote services, which requires authentication. For more info, read Integrate custom data from other apps .","title":"Custom data from other apps may be synced"},{"location":"entities/sync-contact-data/#this-library-does-not-provide-sync-adapters","text":"This library does not have any APIs related to syncing. It is considered out of scope of this library as it requires access to remote databases and account-specific data. Let's talk about it though. However, it is good to know how it works if you just want more insight :grin:. https://developer.android.com/guide/topics/providers/contacts-provider#SyncAdapters The Contacts Provider is specifically designed for handling synchronization of contacts data between a device and an online service. This allows users to download existing data to a new device and upload existing data to a new account. Synchronization also ensures that users have the latest data at hand, regardless of the source of additions and changes. Another advantage of synchronization is that it makes contacts data available even when the device is not connected to the network. Although you can implement synchronization in a variety of ways, the Android system provides a plug-in synchronization framework that automates the following tasks: Checking network availability. Scheduling and executing synchronization, based on user preferences. Restarting synchronizations that have stopped. To use this framework, you supply a sync adapter plug-in. Each sync adapter is unique to a service and content provider, but can handle multiple account names for the same service. The framework also allows multiple sync adapters for the same service and provider. This library does not provide any sync adapters. Instead, it relies on existing sync adapters to do the syncing. Sync adapters and syncing are really out of scope of this library. Syncing is its own thing that typically happens outside of an application UI. This library is focused on Create, Read, Update, and Delete (CRUD) operations on native and custom data to and from the local database. Syncing the local database to and from a remote database in the background is a totally different story altogether :grin: For more info, read Integrate custom data .","title":"This library does not provide sync adapters"},{"location":"groups/delete-groups/","text":"Delete groups \u00b6 This library provides the GroupsDelete API that allows you to delete existing Groups. An instance of the GroupsDelete API is obtained by, val delete = Contacts ( context ). groups (). delete () A basic delete \u00b6 To delete a set of existing groups, val deleteResult = Contacts ( context ) . groups () . delete () . groups ( existingGroups ) . commit () Executing the delete \u00b6 To execute the delete, . commit () If you want to delete all given groups in a single atomic transaction, . commitInOneTransaction () The call to commitInOneTransaction will only succeed if ALL given groups are successfully deleted. If one delete fails, the entire operation will fail and everything will be reverted prior to the delete operation. In contrast, commit allows for some deletes to succeed and some to fail. Handling the delete result \u00b6 The commit and commitInOneTransaction functions returns a Result , To check if all deletes succeeded, val allDeletesSuccessful = deleteResult . isSuccessful To check if a particular delete succeeded, val firstDeleteSuccessful = deleteResult . isSuccessful ( group1 ) Performing the delete and result processing asynchronously \u00b6 Deletes are executed when the commit or commitInOneTransaction function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the delete with permission \u00b6 Deletes require the android.permission.WRITE_CONTACTS permissions. If not granted, the delete will do nothing and return a failed result. To perform the delete with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Read-only Groups \u00b6 Groups created by the system are typically read-only. You cannot delete them, even if you try! The Contacts Provider typically have the following system groups (for standard Google Accounts), systemId: Contacts, title: My Contacts systemId: null, title: Starred in Android systemId: Friends, title: Friends systemId: Family, title: Family systemId: Coworkers, title: Coworkers The above list may vary per account. The GroupsDelete API will not attempt to delete a read-only group and will simply result in failure. Group memberships are automatically deleted \u00b6 When a group is deleted, any membership to that group is deleted automatically by the Contacts Provider.","title":"Delete groups"},{"location":"groups/delete-groups/#delete-groups","text":"This library provides the GroupsDelete API that allows you to delete existing Groups. An instance of the GroupsDelete API is obtained by, val delete = Contacts ( context ). groups (). delete ()","title":"Delete groups"},{"location":"groups/delete-groups/#a-basic-delete","text":"To delete a set of existing groups, val deleteResult = Contacts ( context ) . groups () . delete () . groups ( existingGroups ) . commit ()","title":"A basic delete"},{"location":"groups/delete-groups/#executing-the-delete","text":"To execute the delete, . commit () If you want to delete all given groups in a single atomic transaction, . commitInOneTransaction () The call to commitInOneTransaction will only succeed if ALL given groups are successfully deleted. If one delete fails, the entire operation will fail and everything will be reverted prior to the delete operation. In contrast, commit allows for some deletes to succeed and some to fail.","title":"Executing the delete"},{"location":"groups/delete-groups/#handling-the-delete-result","text":"The commit and commitInOneTransaction functions returns a Result , To check if all deletes succeeded, val allDeletesSuccessful = deleteResult . isSuccessful To check if a particular delete succeeded, val firstDeleteSuccessful = deleteResult . isSuccessful ( group1 )","title":"Handling the delete result"},{"location":"groups/delete-groups/#performing-the-delete-and-result-processing-asynchronously","text":"Deletes are executed when the commit or commitInOneTransaction function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the delete and result processing asynchronously"},{"location":"groups/delete-groups/#performing-the-delete-with-permission","text":"Deletes require the android.permission.WRITE_CONTACTS permissions. If not granted, the delete will do nothing and return a failed result. To perform the delete with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the delete with permission"},{"location":"groups/delete-groups/#read-only-groups","text":"Groups created by the system are typically read-only. You cannot delete them, even if you try! The Contacts Provider typically have the following system groups (for standard Google Accounts), systemId: Contacts, title: My Contacts systemId: null, title: Starred in Android systemId: Friends, title: Friends systemId: Family, title: Family systemId: Coworkers, title: Coworkers The above list may vary per account. The GroupsDelete API will not attempt to delete a read-only group and will simply result in failure.","title":"Read-only Groups"},{"location":"groups/delete-groups/#group-memberships-are-automatically-deleted","text":"When a group is deleted, any membership to that group is deleted automatically by the Contacts Provider.","title":"Group memberships are automatically deleted"},{"location":"groups/insert-groups/","text":"Insert groups \u00b6 This library provides the GroupsInsert API that allows you to create/insert groups associated to an Account . An instance of the GroupsInsert API is obtained by, val insert = Contacts ( context ). groups (). insert () A basic insert \u00b6 To create/insert a new group for an Account, val insertResult = Contacts ( context ) . groups () . insert () . group ( title = \"Besties\" , account = Account ( \"john.doe@gmail.com\" , \"com.google\" ) ) . commit () If you need to insert multiple groups, val newGroup1 = NewGroup ( \"Goodies\" , Account ( \"john.doe@gmail.com\" , \"com.google\" )) val newGroup2 = NewGroup ( \"Baddies\" , Account ( \"john.doe@gmail.com\" , \"com.google\" )) val insertResult = Contacts ( context ) . groups () . insert () . groups ( newGroup1 , newGroup2 ) . commit () Groups and Accounts \u00b6 A set of groups exist for each Account. When there are no accounts in the system, there are no groups and inserting groups will fail. The get accounts permission is required here because this API retrieves all available accounts, if any, and does the following; if the account specified is found in the list of accounts returned by the system, then the account is used if the account specified is not found in the list of accounts returned by the system, then the insertion fails for that group if there are no accounts in the system, [commit] does nothing and fails immediately For more info on the relationship of Groups and Accounts, read Query groups . Groups and duplicate titles \u00b6 The Contacts Provider allows multiple groups with the same title (case-sensitive comparison) belonging to the same account to exist. In older versions of Android, the native Contacts app allows the creation of new groups with existing titles. In newer versions, duplicate titles are not allowed. Therefore, this library does not allow for duplicate titles. Executing the insert \u00b6 To execute the insert, . commit () Handling the insert result \u00b6 The commit function returns a Result . To check if all inserts succeeded, val allInsertsSuccessful = insertResult . isSuccessful To check if a particular insert succeeded, val firstInsertSuccessful = insertResult . isSuccessful ( newGroup1 ) To get the Group IDs of all the newly created Groups, val allGroupIds = insertResult . groupIds To get the Group ID of a particular Group, val secondGroupId = insertResult . groupId ( newGroup2 ) Once you have the Group IDs, you can retrieve the newly created Groups via the GroupsQuery API, val groups = contactsApi . groups () . query () . where { Id `in` allGroupIds } . find () For more info, read Query groups . Alternatively, you may use the extensions provided in GroupsInsertResult . To get all newly created Groups, val groups = insertResult . groups ( contactsApi ) To get a particular group, val group = insertResult . group ( contactsApi , newGroup1 ) Handling insert failure \u00b6 The insert may fail for a particular group for various reasons, insertResult . failureReason ( newGroup1 ) ?. let { when ( it ) { TITLE_ALREADY_EXIST -> promptUserToPickDifferentTitle () INVALID_ACCOUNT -> promptUserToPickDifferentAccount () UNKNOWN -> showGenericErrorMessage () } } Cancelling the insert \u00b6 To cancel an insert amid execution, . commit { returnTrueIfInsertShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel insert processing as soon as possible. The function is called numerous times during insert processing to check if processing should stop or continue. This gives you the option to cancel the insert. For example, to automatically cancel the insert inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val insertResult = insert . commit { ! isActive } } } Performing the insert and result processing asynchronously \u00b6 Inserts are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the insert with permission \u00b6 Inserts require the android.permission.WRITE_CONTACTS and android.permission.GET_ACCOUNTS permissions. If not granted, the insert will do nothing and return a failed result. To perform the insert with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Insert groups"},{"location":"groups/insert-groups/#insert-groups","text":"This library provides the GroupsInsert API that allows you to create/insert groups associated to an Account . An instance of the GroupsInsert API is obtained by, val insert = Contacts ( context ). groups (). insert ()","title":"Insert groups"},{"location":"groups/insert-groups/#a-basic-insert","text":"To create/insert a new group for an Account, val insertResult = Contacts ( context ) . groups () . insert () . group ( title = \"Besties\" , account = Account ( \"john.doe@gmail.com\" , \"com.google\" ) ) . commit () If you need to insert multiple groups, val newGroup1 = NewGroup ( \"Goodies\" , Account ( \"john.doe@gmail.com\" , \"com.google\" )) val newGroup2 = NewGroup ( \"Baddies\" , Account ( \"john.doe@gmail.com\" , \"com.google\" )) val insertResult = Contacts ( context ) . groups () . insert () . groups ( newGroup1 , newGroup2 ) . commit ()","title":"A basic insert"},{"location":"groups/insert-groups/#groups-and-accounts","text":"A set of groups exist for each Account. When there are no accounts in the system, there are no groups and inserting groups will fail. The get accounts permission is required here because this API retrieves all available accounts, if any, and does the following; if the account specified is found in the list of accounts returned by the system, then the account is used if the account specified is not found in the list of accounts returned by the system, then the insertion fails for that group if there are no accounts in the system, [commit] does nothing and fails immediately For more info on the relationship of Groups and Accounts, read Query groups .","title":"Groups and Accounts"},{"location":"groups/insert-groups/#groups-and-duplicate-titles","text":"The Contacts Provider allows multiple groups with the same title (case-sensitive comparison) belonging to the same account to exist. In older versions of Android, the native Contacts app allows the creation of new groups with existing titles. In newer versions, duplicate titles are not allowed. Therefore, this library does not allow for duplicate titles.","title":"Groups and duplicate titles"},{"location":"groups/insert-groups/#executing-the-insert","text":"To execute the insert, . commit ()","title":"Executing the insert"},{"location":"groups/insert-groups/#handling-the-insert-result","text":"The commit function returns a Result . To check if all inserts succeeded, val allInsertsSuccessful = insertResult . isSuccessful To check if a particular insert succeeded, val firstInsertSuccessful = insertResult . isSuccessful ( newGroup1 ) To get the Group IDs of all the newly created Groups, val allGroupIds = insertResult . groupIds To get the Group ID of a particular Group, val secondGroupId = insertResult . groupId ( newGroup2 ) Once you have the Group IDs, you can retrieve the newly created Groups via the GroupsQuery API, val groups = contactsApi . groups () . query () . where { Id `in` allGroupIds } . find () For more info, read Query groups . Alternatively, you may use the extensions provided in GroupsInsertResult . To get all newly created Groups, val groups = insertResult . groups ( contactsApi ) To get a particular group, val group = insertResult . group ( contactsApi , newGroup1 )","title":"Handling the insert result"},{"location":"groups/insert-groups/#handling-insert-failure","text":"The insert may fail for a particular group for various reasons, insertResult . failureReason ( newGroup1 ) ?. let { when ( it ) { TITLE_ALREADY_EXIST -> promptUserToPickDifferentTitle () INVALID_ACCOUNT -> promptUserToPickDifferentAccount () UNKNOWN -> showGenericErrorMessage () } }","title":"Handling insert failure"},{"location":"groups/insert-groups/#cancelling-the-insert","text":"To cancel an insert amid execution, . commit { returnTrueIfInsertShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel insert processing as soon as possible. The function is called numerous times during insert processing to check if processing should stop or continue. This gives you the option to cancel the insert. For example, to automatically cancel the insert inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val insertResult = insert . commit { ! isActive } } }","title":"Cancelling the insert"},{"location":"groups/insert-groups/#performing-the-insert-and-result-processing-asynchronously","text":"Inserts are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the insert and result processing asynchronously"},{"location":"groups/insert-groups/#performing-the-insert-with-permission","text":"Inserts require the android.permission.WRITE_CONTACTS and android.permission.GET_ACCOUNTS permissions. If not granted, the insert will do nothing and return a failed result. To perform the insert with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the insert with permission"},{"location":"groups/query-groups/","text":"Query groups \u00b6 This library provides the GroupsQuery API that allows you to get groups associated with an Account . An instance of the GroupsQuery API is obtained by, val query = Contacts ( context ). groups (). query () A basic query \u00b6 To get all of the groups for all accounts, val groupsFromAllAccounts = Contacts ( context ) . groups () . query () . find () Note that it is recommended to get sets of groups for a single account at a time to avoid confusion. Specifying Accounts \u00b6 To limit the search to only those Groups associated with one of the given accounts, . accounts ( accounts ) For example, to limit the search to groups belonging to only one account, . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) For more info, read Query for Accounts . If no accounts are specified (this function is not called or called with no Accounts), then all Groups of all accounts are included in the search. A null Account may NOT be provided here because no group can exist without an account. Groups are inextricably linked to an Account. Ordering \u00b6 To order resulting Groups using one or more fields, . orderBy ( fieldOrder ) For example, to order groups by account name, . orderBy ( GroupsFields . AccountName . asc ()) String comparisons ignores case by default. Each orderBys provides ignoreCase as an optional parameter. Use GroupsFields to construct the orderBys. Limiting and offsetting \u00b6 To limit the amount of groups returned and/or offset (skip) a specified number of groups, . limit ( limit ) . offset ( offset ) For example, to only get a maximum 20 groups, skipping the first 20, . limit ( 20 ) . offset ( 20 ) This is useful for pagination =) Note that it is recommended to limit the number of groups when querying to increase performance and decrease memory cost. Executing the query \u00b6 To execute the query, . find () Cancelling the query \u00b6 To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val groups = query . find { ! isActive } } } Performing the query asynchronously \u00b6 Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the query with permission \u00b6 Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Using the where function to specify matching criteria \u00b6 Use the contacts.core.GroupsFields combined with the extensions from contacts.core.Where to form WHERE clauses. This docs page will not provide a tutorial on database where clauses. It assumes that you know the basics. If you don't know the basics, then search for sqlite where clause . For example, to find groups with a specific title, . where { Title equalToIgnoreCase \"friends\" } To get a list of groups by IDs, . where { Id `in` groupIds } Different groups with the same titles \u00b6 Each account will have its own set of system and user-created groups. This means that there may be multiple groups with the same title belonging to different accounts. This is not a bug. This is why it is recommended to only get sets of groups per account, especially if there is more than one account in the system. Groups from more than one account in the same list \u00b6 When you perform a query that returns groups from more than one account, you will get everything in the same GroupsList . This list is just like any other List except it also provides an extra function that allows you to get a sublist with groups belonging only to a particular account. val groupsFromAccount = groupsList . from ( account ) This is equivalent to, val groupsFromAccount = groupsList . filter { it . account == account } It serves more as documentation and hint that you should really not be mixing groups from different accounts in the same list as it could cause confusion. However, if you know what you are doing and you are not confused, then do what you like :D This is also nice for Java users to not have to perform the filtering themselves.","title":"Query groups"},{"location":"groups/query-groups/#query-groups","text":"This library provides the GroupsQuery API that allows you to get groups associated with an Account . An instance of the GroupsQuery API is obtained by, val query = Contacts ( context ). groups (). query ()","title":"Query groups"},{"location":"groups/query-groups/#a-basic-query","text":"To get all of the groups for all accounts, val groupsFromAllAccounts = Contacts ( context ) . groups () . query () . find () Note that it is recommended to get sets of groups for a single account at a time to avoid confusion.","title":"A basic query"},{"location":"groups/query-groups/#specifying-accounts","text":"To limit the search to only those Groups associated with one of the given accounts, . accounts ( accounts ) For example, to limit the search to groups belonging to only one account, . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) For more info, read Query for Accounts . If no accounts are specified (this function is not called or called with no Accounts), then all Groups of all accounts are included in the search. A null Account may NOT be provided here because no group can exist without an account. Groups are inextricably linked to an Account.","title":"Specifying Accounts"},{"location":"groups/query-groups/#ordering","text":"To order resulting Groups using one or more fields, . orderBy ( fieldOrder ) For example, to order groups by account name, . orderBy ( GroupsFields . AccountName . asc ()) String comparisons ignores case by default. Each orderBys provides ignoreCase as an optional parameter. Use GroupsFields to construct the orderBys.","title":"Ordering"},{"location":"groups/query-groups/#limiting-and-offsetting","text":"To limit the amount of groups returned and/or offset (skip) a specified number of groups, . limit ( limit ) . offset ( offset ) For example, to only get a maximum 20 groups, skipping the first 20, . limit ( 20 ) . offset ( 20 ) This is useful for pagination =) Note that it is recommended to limit the number of groups when querying to increase performance and decrease memory cost.","title":"Limiting and offsetting"},{"location":"groups/query-groups/#executing-the-query","text":"To execute the query, . find ()","title":"Executing the query"},{"location":"groups/query-groups/#cancelling-the-query","text":"To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val groups = query . find { ! isActive } } }","title":"Cancelling the query"},{"location":"groups/query-groups/#performing-the-query-asynchronously","text":"Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the query asynchronously"},{"location":"groups/query-groups/#performing-the-query-with-permission","text":"Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the query with permission"},{"location":"groups/query-groups/#using-the-where-function-to-specify-matching-criteria","text":"Use the contacts.core.GroupsFields combined with the extensions from contacts.core.Where to form WHERE clauses. This docs page will not provide a tutorial on database where clauses. It assumes that you know the basics. If you don't know the basics, then search for sqlite where clause . For example, to find groups with a specific title, . where { Title equalToIgnoreCase \"friends\" } To get a list of groups by IDs, . where { Id `in` groupIds }","title":"Using the where function to specify matching criteria"},{"location":"groups/query-groups/#different-groups-with-the-same-titles","text":"Each account will have its own set of system and user-created groups. This means that there may be multiple groups with the same title belonging to different accounts. This is not a bug. This is why it is recommended to only get sets of groups per account, especially if there is more than one account in the system.","title":"Different groups with the same titles"},{"location":"groups/query-groups/#groups-from-more-than-one-account-in-the-same-list","text":"When you perform a query that returns groups from more than one account, you will get everything in the same GroupsList . This list is just like any other List except it also provides an extra function that allows you to get a sublist with groups belonging only to a particular account. val groupsFromAccount = groupsList . from ( account ) This is equivalent to, val groupsFromAccount = groupsList . filter { it . account == account } It serves more as documentation and hint that you should really not be mixing groups from different accounts in the same list as it could cause confusion. However, if you know what you are doing and you are not confused, then do what you like :D This is also nice for Java users to not have to perform the filtering themselves.","title":"Groups from more than one account in the same list"},{"location":"groups/update-groups/","text":"Update groups \u00b6 This library provides the GroupsUpdate API that allows you to update existing Groups. An instance of the GroupsUpdate API is obtained by, val update = Contacts ( context ). groups (). update () A basic update \u00b6 To update an existing group, val updateResult = Contacts ( context ) . groups () . update () . groups ( existingGroup ?. mutableCopy { title = \"Best Friends\" }) . commit () If you need to update multiple groups, val mutableGroup1 = group1 . mutableCopy { ... } val mutableGroup2 = group2 . mutableCopy { ... } val updateResult = Contacts ( context ) . groups () . update () . groups ( mutableGroup1 , mutableGroup2 ) . commit () Read-only Groups \u00b6 Groups created by the system are typically read-only. You cannot modify them, even if you try! The Contacts Provider typically have the following system groups (for standard Google Accounts), systemId: Contacts, title: My Contacts systemId: null, title: Starred in Android systemId: Friends, title: Friends systemId: Family, title: Family systemId: Coworkers, title: Coworkers The above list may vary per account. To prevent attempting to modify/update read-only groups, the Group.mutableCopy() function will return null if the group is read-only. You can try and hack your way around this limitation that this library imposes but you will still not be able to change read-only groups. This library is just trying to save you the pain and suffering caused by trying and failing XD Groups and duplicate titles \u00b6 The Contacts Provider allows multiple groups with the same title (case-sensitive comparison) belonging to the same account to exist. In older versions of Android, the native Contacts app allows the creation of new groups with existing titles. In newer versions, duplicate titles are not allowed. Therefore, this library does not allow for duplicate titles. Executing the update \u00b6 To execute the update, . commit () Handling the update result \u00b6 The commit function returns a Result . To check if all updates succeeded, val allUpdatesSuccessful = updateResult . isSuccessful To check if a particular update succeeded, val firstUpdateSuccessful = updateResult . isSuccessful ( mutableGroup1 ) Handling update failure \u00b6 The update may fail for a particular group for various reasons, updateResult . failureReason ( mutableGroup1 ) ?. let { when ( it ) { TITLE_ALREADY_EXIST -> promptUserToPickDifferentTitle () UNKNOWN -> showGenericErrorMessage () } } Cancelling the update \u00b6 To cancel an update amid execution, . commit { returnTrueIfUpdateShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel update processing as soon as possible. The function is called numerous times during update processing to check if processing should stop or continue. This gives you the option to cancel the update. For example, to automatically cancel the update inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val updateResult = update . commit { ! isActive } } } Performing the update and result processing asynchronously \u00b6 Updates are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the update with permission \u00b6 Updates require the android.permission.WRITE_CONTACTS . If not granted, the update will do nothing and return a failed result. To perform the update with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Starred in Android (Favorites) \u00b6 When a Contact is starred, the Contacts Provider automatically adds a group membership to the favorites group for all RawContacts linked to the Contact. Setting the Contact starred to false removes all group memberships to the favorites group. For more info, read Get set Contact options .","title":"Update groups"},{"location":"groups/update-groups/#update-groups","text":"This library provides the GroupsUpdate API that allows you to update existing Groups. An instance of the GroupsUpdate API is obtained by, val update = Contacts ( context ). groups (). update ()","title":"Update groups"},{"location":"groups/update-groups/#a-basic-update","text":"To update an existing group, val updateResult = Contacts ( context ) . groups () . update () . groups ( existingGroup ?. mutableCopy { title = \"Best Friends\" }) . commit () If you need to update multiple groups, val mutableGroup1 = group1 . mutableCopy { ... } val mutableGroup2 = group2 . mutableCopy { ... } val updateResult = Contacts ( context ) . groups () . update () . groups ( mutableGroup1 , mutableGroup2 ) . commit ()","title":"A basic update"},{"location":"groups/update-groups/#read-only-groups","text":"Groups created by the system are typically read-only. You cannot modify them, even if you try! The Contacts Provider typically have the following system groups (for standard Google Accounts), systemId: Contacts, title: My Contacts systemId: null, title: Starred in Android systemId: Friends, title: Friends systemId: Family, title: Family systemId: Coworkers, title: Coworkers The above list may vary per account. To prevent attempting to modify/update read-only groups, the Group.mutableCopy() function will return null if the group is read-only. You can try and hack your way around this limitation that this library imposes but you will still not be able to change read-only groups. This library is just trying to save you the pain and suffering caused by trying and failing XD","title":"Read-only Groups"},{"location":"groups/update-groups/#groups-and-duplicate-titles","text":"The Contacts Provider allows multiple groups with the same title (case-sensitive comparison) belonging to the same account to exist. In older versions of Android, the native Contacts app allows the creation of new groups with existing titles. In newer versions, duplicate titles are not allowed. Therefore, this library does not allow for duplicate titles.","title":"Groups and duplicate titles"},{"location":"groups/update-groups/#executing-the-update","text":"To execute the update, . commit ()","title":"Executing the update"},{"location":"groups/update-groups/#handling-the-update-result","text":"The commit function returns a Result . To check if all updates succeeded, val allUpdatesSuccessful = updateResult . isSuccessful To check if a particular update succeeded, val firstUpdateSuccessful = updateResult . isSuccessful ( mutableGroup1 )","title":"Handling the update result"},{"location":"groups/update-groups/#handling-update-failure","text":"The update may fail for a particular group for various reasons, updateResult . failureReason ( mutableGroup1 ) ?. let { when ( it ) { TITLE_ALREADY_EXIST -> promptUserToPickDifferentTitle () UNKNOWN -> showGenericErrorMessage () } }","title":"Handling update failure"},{"location":"groups/update-groups/#cancelling-the-update","text":"To cancel an update amid execution, . commit { returnTrueIfUpdateShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel update processing as soon as possible. The function is called numerous times during update processing to check if processing should stop or continue. This gives you the option to cancel the update. For example, to automatically cancel the update inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val updateResult = update . commit { ! isActive } } }","title":"Cancelling the update"},{"location":"groups/update-groups/#performing-the-update-and-result-processing-asynchronously","text":"Updates are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the update and result processing asynchronously"},{"location":"groups/update-groups/#performing-the-update-with-permission","text":"Updates require the android.permission.WRITE_CONTACTS . If not granted, the update will do nothing and return a failed result. To perform the update with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the update with permission"},{"location":"groups/update-groups/#starred-in-android-favorites","text":"When a Contact is starred, the Contacts Provider automatically adds a group membership to the favorites group for all RawContacts linked to the Contact. Setting the Contact starred to false removes all group memberships to the favorites group. For more info, read Get set Contact options .","title":"Starred in Android (Favorites)"},{"location":"logging/log-api-input-output/","text":"Log API input and output \u00b6 By default the all APIs provided in this library does not log anything at all. To enable logging all API input/output using the android.util.Log , specify the Logger when constructing an instance of Contacts ; val contactsApi = Contacts ( context , logger = AndroidLogger () ) For more info on Contacts API setup, read Contacts API Setup . Invoking the find or commit functions in query, insert, update, and delete APIs will result in the following output in the Logcat, Query { rawContactsWhere: (account_name LIKE 'test@gmail.com' ESCAPE '\\') AND (account_type LIKE 'com.google' ESCAPE '\\') where: data1 LIKE '%@gmail.com%' ESCAPE '\\' AND mimetype = 'vnd.android.cursor.item/email_v2' isRedacted: false // the rest is omitted for brevity } Query.Result { Number of contacts found: 2 First contact: Contact( id=46, rawContacts=[ RawContact( id=45, contactId=46, addresses=[Address(id=329, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=WORK, label=null, formattedAddress=1200 Park Ave, street=1200 Park Ave, poBox=null, neighborhood=null, city=null, region=null, postcode=null, country=null, isRedacted=false)], emails=[ Email(id=318, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=WORK, label=null, address=buzz.lightyear@pixar.com, isRedacted=false), Email(id=319, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=HOME, label=null, address=buzz@lightyear.net, isRedacted=false) ], events=[ Event(id=317, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=BIRTHDAY, label=null, date=EventDate(year=1995, month=10, dayOfMonth=22, isRedacted=false), isRedacted=false), Event(id=320, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=ANNIVERSARY, label=null, date=EventDate(year=2022, month=0, dayOfMonth=1, isRedacted=false), isRedacted=false) ], // the rest is omitted for brevity ) ] ) isRedacted: false } This is very useful during development. If you have any issues with the library, maintainers will most likely ask you for these logs to help debug your issues. Custom loggers \u00b6 The library provides the AndroidLogger . However, if you want to use your own logging/tracking functions, you may create your own logger by providing an implementation of Logger . For example, to use Timber instead of android.util.Log , class TimberLogger : Logger { override val redactMessages : Boolean = true override fun log ( message : String ) { Timber . d ( message ) } } val contactsApi = Contacts ( context , logger = AndroidLogger () ) Redacting log messages \u00b6 The messages that are logged may contain private user data (contact data). Depending on how you log these messages in production, you may end up violating privacy laws such as GDPR guidelines. To ensure that you are not violating any privacy laws in your production apps when using this library, make sure to set Logger.redactMessages to true . val contactsApi = Contacts ( context , logger = AndroidLogger ( redactMessages = true ) ) Redacted messages are not as useful when debugging so you should set it to false during development. A common way to redact messages in release builds but not debug builds is to, AndroidLogger ( redactMessages = ! BuildConfig . DEBUG ) For more info on redaction, read Redact entities and API input and output in production .","title":"Log API input and output"},{"location":"logging/log-api-input-output/#log-api-input-and-output","text":"By default the all APIs provided in this library does not log anything at all. To enable logging all API input/output using the android.util.Log , specify the Logger when constructing an instance of Contacts ; val contactsApi = Contacts ( context , logger = AndroidLogger () ) For more info on Contacts API setup, read Contacts API Setup . Invoking the find or commit functions in query, insert, update, and delete APIs will result in the following output in the Logcat, Query { rawContactsWhere: (account_name LIKE 'test@gmail.com' ESCAPE '\\') AND (account_type LIKE 'com.google' ESCAPE '\\') where: data1 LIKE '%@gmail.com%' ESCAPE '\\' AND mimetype = 'vnd.android.cursor.item/email_v2' isRedacted: false // the rest is omitted for brevity } Query.Result { Number of contacts found: 2 First contact: Contact( id=46, rawContacts=[ RawContact( id=45, contactId=46, addresses=[Address(id=329, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=WORK, label=null, formattedAddress=1200 Park Ave, street=1200 Park Ave, poBox=null, neighborhood=null, city=null, region=null, postcode=null, country=null, isRedacted=false)], emails=[ Email(id=318, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=WORK, label=null, address=buzz.lightyear@pixar.com, isRedacted=false), Email(id=319, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=HOME, label=null, address=buzz@lightyear.net, isRedacted=false) ], events=[ Event(id=317, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=BIRTHDAY, label=null, date=EventDate(year=1995, month=10, dayOfMonth=22, isRedacted=false), isRedacted=false), Event(id=320, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=ANNIVERSARY, label=null, date=EventDate(year=2022, month=0, dayOfMonth=1, isRedacted=false), isRedacted=false) ], // the rest is omitted for brevity ) ] ) isRedacted: false } This is very useful during development. If you have any issues with the library, maintainers will most likely ask you for these logs to help debug your issues.","title":"Log API input and output"},{"location":"logging/log-api-input-output/#custom-loggers","text":"The library provides the AndroidLogger . However, if you want to use your own logging/tracking functions, you may create your own logger by providing an implementation of Logger . For example, to use Timber instead of android.util.Log , class TimberLogger : Logger { override val redactMessages : Boolean = true override fun log ( message : String ) { Timber . d ( message ) } } val contactsApi = Contacts ( context , logger = AndroidLogger () )","title":"Custom loggers"},{"location":"logging/log-api-input-output/#redacting-log-messages","text":"The messages that are logged may contain private user data (contact data). Depending on how you log these messages in production, you may end up violating privacy laws such as GDPR guidelines. To ensure that you are not violating any privacy laws in your production apps when using this library, make sure to set Logger.redactMessages to true . val contactsApi = Contacts ( context , logger = AndroidLogger ( redactMessages = true ) ) Redacted messages are not as useful when debugging so you should set it to false during development. A common way to redact messages in release builds but not debug builds is to, AndroidLogger ( redactMessages = ! BuildConfig . DEBUG ) For more info on redaction, read Redact entities and API input and output in production .","title":"Redacting log messages"},{"location":"other/convenience-functions/","text":"Convenience functions \u00b6 This library provides some nice-to-have extensions in the contacts.core.utils package. I will be going over some of them in this page. Note that functions in the util package that are used directly by other APIs such as result APIs are not discussed here. Contact data getter and setters \u00b6 Contacts can be made up of one or more RawContacts. In the case that a Contact has two or more RawContacts, getting/setting RawContact data may be a bit of a hassle, requiring loops or iterators, // get all emails from all RawContacts belonging to the Contact val contactEmails = contact . rawContacts . flatMap { it . emails } // add an email to the first RawContact belonging to the Contact contact . mutableCopy (). rawContacts . first (). emails . add ( NewEmail ()) For more info, read about API Entities . To simplify things, getter/setter extensions are provided in the ContactData.kt file, // get all emails from all RawContacts belonging to the Contact val contactEmailSequence = contact . emails () val contactEmailList = contact . emailList () // add an email to the first RawContact belonging to the Contact contact . mutableCopy (). addEmail ( NewEmail ()) Newer versions of the Android Open Source Project Contacts app and the Google Contacts app shows data coming from all RawContacts in a Contact details screen. However, they only allow editing a single RawContact and not the aggregate Contact in a single screen to avoid confusion. With this in mind, feel free to use the getter extensions but be very careful with using the setters! Mutable and New RawContact data setters \u00b6 Getting data from RawContacts is straightforward. You have direct access to their properties. The same goes for setting data. val rawContactEmails = rawContact . emails rawContact . mutableCopy (). addEmail ( NewEmail (). apply { address = \"abc@alphabet.com\" type = EmailEntity . Type . WORK } ) Still, there are some setter extensions provided in MutableRawContactData.kt and NewRawContactData.kt that can add some sugar to your syntax. rawContact . mutableCopy (). addEmail { address = \"abc@alphabet.com\" type = EmailEntity . Type . WORK } The setter functions in this section and in the \"Contact data getter and setters\" section also uphold the redacted state of the mutable Contact/RawContact. We setting or adding a property using these extensions, the property being passed will be redacted if the Contact/RawContact it is being added to is redacted. For more info, read Redact entities and API input and output in production . Getting the parent Contact of a RawContact or Data \u00b6 Using the Query API, it is easy to get the parent Contact of a RawContact or Data, val contactOfRawContact = contactsApi . query (). where { Contact . Id equalTo rawContact . contactId }. find (). firstOrNull () val contactOfData = contactsApi . query (). where { Contact . Id equalTo data . contactId }. find (). firstOrNull () For more info, read Query contacts (advanced) . To shorten things, you can use the extensions in RawContactContact.kt and DataContact.kt , val contactOfRawContact = rawContact . contact ( contactsApi ) val contactOfData = data . contact ( contactsApi ) On a similar note, to get the parent RawContact of a Data using the extensions in DataRawContact.kt , val rawContactOfData = data . rawContact ( contactsApi ) These are blocking calls so you might want to do them outside the UI thread. For more info, read Execute work outside of the UI thread using coroutines . Refresh Contact, RawContact, and Data references \u00b6 In-memory references to these entities could become inaccurate due to changes in the database that could occur in your app, other apps, or by the Contacts Provider. If you need to get the most up-to-date reference of an entity from the database, you could do it using the Query and DataQuery APIs, val contactFromDb = contactsApi . query (). where { Contact . Id equalTo contactInMemory . id }. find (). firstOrNull () val rawContactFromDb = contactsApi . query (). where { RawContact . Id equalTo rawContactInMemory . id }. find () . firstOrNull () ?. rawContacts ?. find { it . id == rawContactInMemory . id } val dataFromDb = contactsApi . data (). query (). where { DataId equalTo dataInMemory . id }. find (). firstOrNull () To shorten things, you can use extensions in ContactRefresh.kt , RawContactRefresh.kt , and DataRefresh.kt , val contactFromDb = contactInMemory . refresh ( contactsApi ) val rawContactFromDb = rawContactInMemory . refresh ( contactsApi ) val dataFromDb = dataInMemory . refresh ( contactsApi ) These are blocking calls so you might want to do them outside the UI thread. For more info, read Execute work outside of the UI thread using coroutines . Sort Contacts by data fields \u00b6 The Query and BroadQuery APIs allows you to sort Contacts based on fields in the Contacts table such as Id and DisplayNamePrimary , val sortedContacts = query . orderBy ( ContactsFields . DisplayNamePrimary . desc ( ignoreCase = true )) If you want to sort Contacts based on data fields (e.g. email), you are unable to use the query APIs provided in this library to do so. However, if you have a list of Contacts in memory, you can use the extensions in ContactsComparator.kt to build a Comparator to use for sorting, val sortedContacts = unsortedContacts . sortedWith ( Fields . Email . Address . desc ( ignoreCase = true ). contactsComparator () ) You can also specify multiple fields for sorting, val sortedContacts = unsortedContacts . sortedWith ( setOf ( Fields . Contact . Options . Starred . desc (), Fields . Contact . DisplayNamePrimary . asc ( ignoreCase = false ), Fields . Email . Address . asc () ). contactsComparator () ) Get the Group of a GroupMembership \u00b6 The GroupsQuery allows you to get groups from a set of group Ids, val group = contactsApi . groups (). query (). where { Id equalTo groupMembership . groupId }. find (). firstOrNull () val groups = contactsApi . groups (). query (). where { Id `in` groupMemberships . map { it . groupId } }. find () To shorten things, you can use the extensions in GroupMembershipGroup.kt , val group = groupMembership . group ( contactsApi ) val groups = groupMemberships . groups ( contactsApi ) These are blocking calls so you might want to do them outside the UI thread. For more info, read Execute work outside of the UI thread using coroutines . Get the RawContact of a BlankRawContact \u00b6 The Query API allows you to get the RawContact version of a BlankRawContact , val rawContact = contactsApi . query (). where { RawContact . Id equalTo blankRawContact . id }. find () . firstOrNull () ?. rawContacts ?. find { it . id == blankRawContact . id } To shorten things, you can use the extensions in BlankRawContactToRawContact.kt , val rawContact = blankRawContact . toRawContact ( contactsApi ) These are blocking calls so you might want to do them outside the UI thread. For more info, read Execute work outside of the UI thread using coroutines .","title":"Convenience functions"},{"location":"other/convenience-functions/#convenience-functions","text":"This library provides some nice-to-have extensions in the contacts.core.utils package. I will be going over some of them in this page. Note that functions in the util package that are used directly by other APIs such as result APIs are not discussed here.","title":"Convenience functions"},{"location":"other/convenience-functions/#contact-data-getter-and-setters","text":"Contacts can be made up of one or more RawContacts. In the case that a Contact has two or more RawContacts, getting/setting RawContact data may be a bit of a hassle, requiring loops or iterators, // get all emails from all RawContacts belonging to the Contact val contactEmails = contact . rawContacts . flatMap { it . emails } // add an email to the first RawContact belonging to the Contact contact . mutableCopy (). rawContacts . first (). emails . add ( NewEmail ()) For more info, read about API Entities . To simplify things, getter/setter extensions are provided in the ContactData.kt file, // get all emails from all RawContacts belonging to the Contact val contactEmailSequence = contact . emails () val contactEmailList = contact . emailList () // add an email to the first RawContact belonging to the Contact contact . mutableCopy (). addEmail ( NewEmail ()) Newer versions of the Android Open Source Project Contacts app and the Google Contacts app shows data coming from all RawContacts in a Contact details screen. However, they only allow editing a single RawContact and not the aggregate Contact in a single screen to avoid confusion. With this in mind, feel free to use the getter extensions but be very careful with using the setters!","title":"Contact data getter and setters"},{"location":"other/convenience-functions/#mutable-and-new-rawcontact-data-setters","text":"Getting data from RawContacts is straightforward. You have direct access to their properties. The same goes for setting data. val rawContactEmails = rawContact . emails rawContact . mutableCopy (). addEmail ( NewEmail (). apply { address = \"abc@alphabet.com\" type = EmailEntity . Type . WORK } ) Still, there are some setter extensions provided in MutableRawContactData.kt and NewRawContactData.kt that can add some sugar to your syntax. rawContact . mutableCopy (). addEmail { address = \"abc@alphabet.com\" type = EmailEntity . Type . WORK } The setter functions in this section and in the \"Contact data getter and setters\" section also uphold the redacted state of the mutable Contact/RawContact. We setting or adding a property using these extensions, the property being passed will be redacted if the Contact/RawContact it is being added to is redacted. For more info, read Redact entities and API input and output in production .","title":"Mutable and New RawContact data setters"},{"location":"other/convenience-functions/#getting-the-parent-contact-of-a-rawcontact-or-data","text":"Using the Query API, it is easy to get the parent Contact of a RawContact or Data, val contactOfRawContact = contactsApi . query (). where { Contact . Id equalTo rawContact . contactId }. find (). firstOrNull () val contactOfData = contactsApi . query (). where { Contact . Id equalTo data . contactId }. find (). firstOrNull () For more info, read Query contacts (advanced) . To shorten things, you can use the extensions in RawContactContact.kt and DataContact.kt , val contactOfRawContact = rawContact . contact ( contactsApi ) val contactOfData = data . contact ( contactsApi ) On a similar note, to get the parent RawContact of a Data using the extensions in DataRawContact.kt , val rawContactOfData = data . rawContact ( contactsApi ) These are blocking calls so you might want to do them outside the UI thread. For more info, read Execute work outside of the UI thread using coroutines .","title":"Getting the parent Contact of a RawContact or Data"},{"location":"other/convenience-functions/#refresh-contact-rawcontact-and-data-references","text":"In-memory references to these entities could become inaccurate due to changes in the database that could occur in your app, other apps, or by the Contacts Provider. If you need to get the most up-to-date reference of an entity from the database, you could do it using the Query and DataQuery APIs, val contactFromDb = contactsApi . query (). where { Contact . Id equalTo contactInMemory . id }. find (). firstOrNull () val rawContactFromDb = contactsApi . query (). where { RawContact . Id equalTo rawContactInMemory . id }. find () . firstOrNull () ?. rawContacts ?. find { it . id == rawContactInMemory . id } val dataFromDb = contactsApi . data (). query (). where { DataId equalTo dataInMemory . id }. find (). firstOrNull () To shorten things, you can use extensions in ContactRefresh.kt , RawContactRefresh.kt , and DataRefresh.kt , val contactFromDb = contactInMemory . refresh ( contactsApi ) val rawContactFromDb = rawContactInMemory . refresh ( contactsApi ) val dataFromDb = dataInMemory . refresh ( contactsApi ) These are blocking calls so you might want to do them outside the UI thread. For more info, read Execute work outside of the UI thread using coroutines .","title":"Refresh Contact, RawContact, and Data references"},{"location":"other/convenience-functions/#sort-contacts-by-data-fields","text":"The Query and BroadQuery APIs allows you to sort Contacts based on fields in the Contacts table such as Id and DisplayNamePrimary , val sortedContacts = query . orderBy ( ContactsFields . DisplayNamePrimary . desc ( ignoreCase = true )) If you want to sort Contacts based on data fields (e.g. email), you are unable to use the query APIs provided in this library to do so. However, if you have a list of Contacts in memory, you can use the extensions in ContactsComparator.kt to build a Comparator to use for sorting, val sortedContacts = unsortedContacts . sortedWith ( Fields . Email . Address . desc ( ignoreCase = true ). contactsComparator () ) You can also specify multiple fields for sorting, val sortedContacts = unsortedContacts . sortedWith ( setOf ( Fields . Contact . Options . Starred . desc (), Fields . Contact . DisplayNamePrimary . asc ( ignoreCase = false ), Fields . Email . Address . asc () ). contactsComparator () )","title":"Sort Contacts by data fields"},{"location":"other/convenience-functions/#get-the-group-of-a-groupmembership","text":"The GroupsQuery allows you to get groups from a set of group Ids, val group = contactsApi . groups (). query (). where { Id equalTo groupMembership . groupId }. find (). firstOrNull () val groups = contactsApi . groups (). query (). where { Id `in` groupMemberships . map { it . groupId } }. find () To shorten things, you can use the extensions in GroupMembershipGroup.kt , val group = groupMembership . group ( contactsApi ) val groups = groupMemberships . groups ( contactsApi ) These are blocking calls so you might want to do them outside the UI thread. For more info, read Execute work outside of the UI thread using coroutines .","title":"Get the Group of a GroupMembership"},{"location":"other/convenience-functions/#get-the-rawcontact-of-a-blankrawcontact","text":"The Query API allows you to get the RawContact version of a BlankRawContact , val rawContact = contactsApi . query (). where { RawContact . Id equalTo blankRawContact . id }. find () . firstOrNull () ?. rawContacts ?. find { it . id == blankRawContact . id } To shorten things, you can use the extensions in BlankRawContactToRawContact.kt , val rawContact = blankRawContact . toRawContact ( contactsApi ) These are blocking calls so you might want to do them outside the UI thread. For more info, read Execute work outside of the UI thread using coroutines .","title":"Get the RawContact of a BlankRawContact"},{"location":"other/get-set-clear-contact-raw-contact-options/","text":"Get set Contact options \u00b6 This library provides several functions to interact with Contact and RawContact options; starred, send to voicemail, and ringtone. Contact and RawContact options affect each other \u00b6 Changes to the options of the parent Contact will be propagated to all child RawContact options. Changes to the options of a RawContact may or may not affect the options of the parent Contact. Getting contact options \u00b6 To get Contact options, val options = contact . options ( contactsApi ) To get RawContact options, val options = rawContact . options ( contactsApi ) Setting contact options \u00b6 To set Contact options, contact . setOptions ( contactsApi , mutableOptions ) To set RawContact options, rawContact . setOptions ( contactsApi , mutableOptions ) For example, to set a contact to be starred (favorited), contact . setOptions ( contactsApi , mutableOptions . apply { starred = true }) The setOption function takes in an arbitrary Options instance. If you instead want to modify the options of a Contact or RawContact retrieved from the database, contact . updateOptions ( contactsApi ) { starred = true } This is useful if you only want to set certain properties and keep other properties the same. Changes are immediate and are not applied to the receiver \u00b6 These apply to set and update functions. Changes are immediate. These functions will make the changes to the Contacts Provider database immediately. You do not need to use update APIs to commit the changes. Changes are not applied to the receiver. This function call does NOT mutate immutable or mutable receivers. Therefore, you should use query APIs or refresh extensions or process the result of this function call to get the most up-to-date reference to mutable or immutable entity that contains the changes in the Contacts Provider database. Using the ui RingtonePicker extensions \u00b6 The contacts.ui.util.RingtonePicker.kt in the ui module` provides extension functions to make selecting existing ringtones easier. It provides you the same UX as the native Contacts app. To use it, Activity { fun onSelectRingtoneClicked () { selectRingtone ( contact . options ?. customRingtone ) } override fun onActivityResult ( requestCode : Int , resultCode : Int , data : Intent?) { super . onActivityResult ( requestCode , resultCode , data ) onRingtoneSelected ( requestCode , resultCode , data ) { ringtoneUri -> contact . updateOptions ( contactsApi ) { customRingtone = ringtoneUri } } } } Starting with Android 11 (API 30), you must include the following to your manifest in order to successfully use the above functions. Performing options management asynchronously \u00b6 All of the code shown in this guide are done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing options management with permission \u00b6 Getting and setting options require the android.permission.READ_CONTACTS and android.permission.WRITE_CONTACTS permissions respectively. If not granted, getting and setting options will fail. TODO Update this section as part of issue #120 . Starred in Android (Favorites) \u00b6 When a Contact is starred, the Contacts Provider automatically adds a group membership to the favorites group for all RawContacts linked to the Contact. Setting the Contact starred to false removes all group memberships to the favorites group. The Contact's \"starred\" value is interdependent with group memberships to the favorites group. Adding a group membership to the favorites group results in starred being set to true. Removing the membership sets it to false. Raw contacts that are not associated with an account do not have any group memberships. Even though these RawContacts may not have a membership to the favorites group, they may still be \"starred\" (favorited), which is not dependent on the existence of a favorites group membership. Refresh RawContact instances after changing the starred value. Otherwise, performing an update on the RawContact with a stale set of group memberships may revert the star/unstar operation. For example, -> query returns a starred RawContact -> set starred to false -> update RawContact (still containing a group membership to the favorites group) -> starred will be set back to true. FAQs \u00b6 Can contacts be inserted with options \u00b6 Related issues; #120 You cannot get/set/update options for Contacts/RawContacts that have not yet been inserted in the Contacts Provider database. In other words, only Contacts/RawContacts retrieved via query or result APIs can use the extension functions in contacts.core.util.ContactOptions.kt and contacts.core.util.RawContactOptions.kt . To insert a new contact \"with options\", you should insert the contact first. Then, if the insert succeeds, proceed to set the options. For more info about insert, read Insert contacts .","title":"Get set Contact options"},{"location":"other/get-set-clear-contact-raw-contact-options/#get-set-contact-options","text":"This library provides several functions to interact with Contact and RawContact options; starred, send to voicemail, and ringtone.","title":"Get set Contact options"},{"location":"other/get-set-clear-contact-raw-contact-options/#contact-and-rawcontact-options-affect-each-other","text":"Changes to the options of the parent Contact will be propagated to all child RawContact options. Changes to the options of a RawContact may or may not affect the options of the parent Contact.","title":"Contact and RawContact options affect each other"},{"location":"other/get-set-clear-contact-raw-contact-options/#getting-contact-options","text":"To get Contact options, val options = contact . options ( contactsApi ) To get RawContact options, val options = rawContact . options ( contactsApi )","title":"Getting contact options"},{"location":"other/get-set-clear-contact-raw-contact-options/#setting-contact-options","text":"To set Contact options, contact . setOptions ( contactsApi , mutableOptions ) To set RawContact options, rawContact . setOptions ( contactsApi , mutableOptions ) For example, to set a contact to be starred (favorited), contact . setOptions ( contactsApi , mutableOptions . apply { starred = true }) The setOption function takes in an arbitrary Options instance. If you instead want to modify the options of a Contact or RawContact retrieved from the database, contact . updateOptions ( contactsApi ) { starred = true } This is useful if you only want to set certain properties and keep other properties the same.","title":"Setting contact options"},{"location":"other/get-set-clear-contact-raw-contact-options/#changes-are-immediate-and-are-not-applied-to-the-receiver","text":"These apply to set and update functions. Changes are immediate. These functions will make the changes to the Contacts Provider database immediately. You do not need to use update APIs to commit the changes. Changes are not applied to the receiver. This function call does NOT mutate immutable or mutable receivers. Therefore, you should use query APIs or refresh extensions or process the result of this function call to get the most up-to-date reference to mutable or immutable entity that contains the changes in the Contacts Provider database.","title":"Changes are immediate and are not applied to the receiver"},{"location":"other/get-set-clear-contact-raw-contact-options/#using-the-ui-ringtonepicker-extensions","text":"The contacts.ui.util.RingtonePicker.kt in the ui module` provides extension functions to make selecting existing ringtones easier. It provides you the same UX as the native Contacts app. To use it, Activity { fun onSelectRingtoneClicked () { selectRingtone ( contact . options ?. customRingtone ) } override fun onActivityResult ( requestCode : Int , resultCode : Int , data : Intent?) { super . onActivityResult ( requestCode , resultCode , data ) onRingtoneSelected ( requestCode , resultCode , data ) { ringtoneUri -> contact . updateOptions ( contactsApi ) { customRingtone = ringtoneUri } } } } Starting with Android 11 (API 30), you must include the following to your manifest in order to successfully use the above functions. ","title":"Using the ui RingtonePicker extensions"},{"location":"other/get-set-clear-contact-raw-contact-options/#performing-options-management-asynchronously","text":"All of the code shown in this guide are done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing options management asynchronously"},{"location":"other/get-set-clear-contact-raw-contact-options/#performing-options-management-with-permission","text":"Getting and setting options require the android.permission.READ_CONTACTS and android.permission.WRITE_CONTACTS permissions respectively. If not granted, getting and setting options will fail. TODO Update this section as part of issue #120 .","title":"Performing options management with permission"},{"location":"other/get-set-clear-contact-raw-contact-options/#starred-in-android-favorites","text":"When a Contact is starred, the Contacts Provider automatically adds a group membership to the favorites group for all RawContacts linked to the Contact. Setting the Contact starred to false removes all group memberships to the favorites group. The Contact's \"starred\" value is interdependent with group memberships to the favorites group. Adding a group membership to the favorites group results in starred being set to true. Removing the membership sets it to false. Raw contacts that are not associated with an account do not have any group memberships. Even though these RawContacts may not have a membership to the favorites group, they may still be \"starred\" (favorited), which is not dependent on the existence of a favorites group membership. Refresh RawContact instances after changing the starred value. Otherwise, performing an update on the RawContact with a stale set of group memberships may revert the star/unstar operation. For example, -> query returns a starred RawContact -> set starred to false -> update RawContact (still containing a group membership to the favorites group) -> starred will be set back to true.","title":"Starred in Android (Favorites)"},{"location":"other/get-set-clear-contact-raw-contact-options/#faqs","text":"","title":"FAQs"},{"location":"other/get-set-clear-contact-raw-contact-options/#can-contacts-be-inserted-with-options","text":"Related issues; #120 You cannot get/set/update options for Contacts/RawContacts that have not yet been inserted in the Contacts Provider database. In other words, only Contacts/RawContacts retrieved via query or result APIs can use the extension functions in contacts.core.util.ContactOptions.kt and contacts.core.util.RawContactOptions.kt . To insert a new contact \"with options\", you should insert the contact first. Then, if the insert succeeds, proceed to set the options. For more info about insert, read Insert contacts .","title":"Can contacts be inserted with options"},{"location":"other/get-set-clear-default-data/","text":"Get set clear default Contact data \u00b6 Default contact data are instances of common data kinds that are marked as the default. The two most common data kinds that use this mechanism are emails and phones. In the native Contacts app Contact details activity, long pressing an email or phone shows a popup menu with an option to set it as default. When a particular email or phone is set as default, sending an email and making a phone call to that contact will use that default email and phone respectively. For more info on the common data kinds, read about API Entities . Getting default data \u00b6 To get the default Contact email and phone from all RawContacts, val defaultContactEmail : Email? = contact . emails (). default () val defaultContactPhone : Phone? = contact . phones (). default () To get the default RawContact email and phone, val defaultRawContactEmail : Email? = rawContact . emails . default () val defaultRawContactPhone : Phone? = rawContact . phones . default () To get the first default data out of a generic list of data, val defaultData = dataList . default () Note that the most common use of defaults is with Contacts, not RawContacts. You typically do not need to worry about defaults at a RawContact level. Setting default data \u00b6 To set a particular data as the default for the set of data of the same type (e.g. email) for the aggregate Contact, email . setAsDefault ( contactsApi ) If a default data of the same type for the aggregate Contact already exist before this call, then it will no longer be the default. For example, these emails belong to the same aggregate Contact; x@x.com (default) y@y.com z@z.com Calling this function on a non-default data (e.g. y@y.com) will remove the default status for data that was previously set as the default. This data will then be set as the default. This results in; x@x.com y@y.com (default) z@z.com Clearing default data \u00b6 To remove the default status of any data of the same type (e.g. email), if any, for the aggregate Contact, email . clearDefault ( contactsApi ) For example, these emails belong to the same aggregate Contact; x@x.com y@y.com (default) z@z.com Calling this function on any data of the same kind for the aggregate contact (default or not) will remove the default status on all data of the same kind for the aggregate Contact. This results in; x@x.com y@y.com z@z.com Changes are immediate and are not applied to the receiver \u00b6 These apply to set and clear functions. Changes are immediate. These functions will make the changes to the Contacts Provider database immediately. You do not need to use update APIs to commit the changes. Changes are not applied to the receiver. This function call does NOT mutate immutable or mutable receivers. Therefore, you should use query APIs or refresh extensions or process the result of this function call to get the most up-to-date reference to mutable or immutable entity that contains the changes in the Contacts Provider database. Performing default data management asynchronously \u00b6 Setting or clearing default data is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing default data management with permission \u00b6 Getting and setting/clearing default data require the android.permission.READ_CONTACTS and android.permission.WRITE_CONTACTS permissions respectively. If not granted, getting and setting/clearing default data will fail. Developer notes (or for advanced users) \u00b6 The following section are note from developers of this library for other developers. It is copied from the DEV_NOTES . You may still read the following as a consumer of the library in case you need deeper insight. As per documentation, for a set of data rows with the same mimetype (e.g. a set of emails), there should only be one primary data row (e.g. email) per RawContact and one super primary data row per Contact. Furthermore, a data row that is super primary must also be primary. Unfortunately, the Contacts Provider does not do any data set validation for the Data columns IS_PRIMARY and IS_SUPER_PRIMARY . This means that it is possible to set more than one data row of the same mimetype as primary for the same RawContact and super primary for the same aggregate Contact. It is also possible to set a data row as super primary but not primary. Upholding the the contract is left to the us (the library). For example, given this relationship; Contact RawContact X Email A Email B RawContact Y Email C Email D When Emails A, B, C, and D are inserted with the RawContacts or after the RawContacts have been created, we get the following state; Email Primary Super Primary A 0 0 B 0 0 C 0 0 D 0 0 The state does not change when RawContact X is linked with RawContact Y. After setting Email A as the \"default\" email, it becomes primary and super primary; Email Primary Super Primary A 1 1 B 0 0 C 0 0 D 0 0 Then setting Email B as the default email, it becomes primary and super primary. Email A is no longer primary or super primary. Email Primary Super Primary A 0 0 B 1 1 C 0 0 D 0 0 Then setting Email C as the default email, it becomes primary and super primary. Email B is still primary because it belongs to a different RawContact than Email C. However, Email B is no longer the super primary as there can only be one per aggregate Contact. Email Primary Super Primary A 0 0 B 1 0 C 1 1 D 0 0 Then setting Email D as the default email, it becomes primary and super primary. Email C is no longer primary or super primary. Email Primary Super Primary A 0 0 B 1 0 C 0 0 D 1 1 Then clearing the default email D, removes its primary and super primary status. However, email B remains a primary but not a super primary. Email Primary Super Primary A 0 0 B 1 0 C 0 0 D 0 0 The above behavior is observed from the native Contacts app. The \"super primary\" data of an aggregate Contact is referred to as the \"default\".","title":"Get set clear default Contact data"},{"location":"other/get-set-clear-default-data/#get-set-clear-default-contact-data","text":"Default contact data are instances of common data kinds that are marked as the default. The two most common data kinds that use this mechanism are emails and phones. In the native Contacts app Contact details activity, long pressing an email or phone shows a popup menu with an option to set it as default. When a particular email or phone is set as default, sending an email and making a phone call to that contact will use that default email and phone respectively. For more info on the common data kinds, read about API Entities .","title":"Get set clear default Contact data"},{"location":"other/get-set-clear-default-data/#getting-default-data","text":"To get the default Contact email and phone from all RawContacts, val defaultContactEmail : Email? = contact . emails (). default () val defaultContactPhone : Phone? = contact . phones (). default () To get the default RawContact email and phone, val defaultRawContactEmail : Email? = rawContact . emails . default () val defaultRawContactPhone : Phone? = rawContact . phones . default () To get the first default data out of a generic list of data, val defaultData = dataList . default () Note that the most common use of defaults is with Contacts, not RawContacts. You typically do not need to worry about defaults at a RawContact level.","title":"Getting default data"},{"location":"other/get-set-clear-default-data/#setting-default-data","text":"To set a particular data as the default for the set of data of the same type (e.g. email) for the aggregate Contact, email . setAsDefault ( contactsApi ) If a default data of the same type for the aggregate Contact already exist before this call, then it will no longer be the default. For example, these emails belong to the same aggregate Contact; x@x.com (default) y@y.com z@z.com Calling this function on a non-default data (e.g. y@y.com) will remove the default status for data that was previously set as the default. This data will then be set as the default. This results in; x@x.com y@y.com (default) z@z.com","title":"Setting default data"},{"location":"other/get-set-clear-default-data/#clearing-default-data","text":"To remove the default status of any data of the same type (e.g. email), if any, for the aggregate Contact, email . clearDefault ( contactsApi ) For example, these emails belong to the same aggregate Contact; x@x.com y@y.com (default) z@z.com Calling this function on any data of the same kind for the aggregate contact (default or not) will remove the default status on all data of the same kind for the aggregate Contact. This results in; x@x.com y@y.com z@z.com","title":"Clearing default data"},{"location":"other/get-set-clear-default-data/#changes-are-immediate-and-are-not-applied-to-the-receiver","text":"These apply to set and clear functions. Changes are immediate. These functions will make the changes to the Contacts Provider database immediately. You do not need to use update APIs to commit the changes. Changes are not applied to the receiver. This function call does NOT mutate immutable or mutable receivers. Therefore, you should use query APIs or refresh extensions or process the result of this function call to get the most up-to-date reference to mutable or immutable entity that contains the changes in the Contacts Provider database.","title":"Changes are immediate and are not applied to the receiver"},{"location":"other/get-set-clear-default-data/#performing-default-data-management-asynchronously","text":"Setting or clearing default data is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing default data management asynchronously"},{"location":"other/get-set-clear-default-data/#performing-default-data-management-with-permission","text":"Getting and setting/clearing default data require the android.permission.READ_CONTACTS and android.permission.WRITE_CONTACTS permissions respectively. If not granted, getting and setting/clearing default data will fail.","title":"Performing default data management with permission"},{"location":"other/get-set-clear-default-data/#developer-notes-or-for-advanced-users","text":"The following section are note from developers of this library for other developers. It is copied from the DEV_NOTES . You may still read the following as a consumer of the library in case you need deeper insight. As per documentation, for a set of data rows with the same mimetype (e.g. a set of emails), there should only be one primary data row (e.g. email) per RawContact and one super primary data row per Contact. Furthermore, a data row that is super primary must also be primary. Unfortunately, the Contacts Provider does not do any data set validation for the Data columns IS_PRIMARY and IS_SUPER_PRIMARY . This means that it is possible to set more than one data row of the same mimetype as primary for the same RawContact and super primary for the same aggregate Contact. It is also possible to set a data row as super primary but not primary. Upholding the the contract is left to the us (the library). For example, given this relationship; Contact RawContact X Email A Email B RawContact Y Email C Email D When Emails A, B, C, and D are inserted with the RawContacts or after the RawContacts have been created, we get the following state; Email Primary Super Primary A 0 0 B 0 0 C 0 0 D 0 0 The state does not change when RawContact X is linked with RawContact Y. After setting Email A as the \"default\" email, it becomes primary and super primary; Email Primary Super Primary A 1 1 B 0 0 C 0 0 D 0 0 Then setting Email B as the default email, it becomes primary and super primary. Email A is no longer primary or super primary. Email Primary Super Primary A 0 0 B 1 1 C 0 0 D 0 0 Then setting Email C as the default email, it becomes primary and super primary. Email B is still primary because it belongs to a different RawContact than Email C. However, Email B is no longer the super primary as there can only be one per aggregate Contact. Email Primary Super Primary A 0 0 B 1 0 C 1 1 D 0 0 Then setting Email D as the default email, it becomes primary and super primary. Email C is no longer primary or super primary. Email Primary Super Primary A 0 0 B 1 0 C 0 0 D 1 1 Then clearing the default email D, removes its primary and super primary status. However, email B remains a primary but not a super primary. Email Primary Super Primary A 0 0 B 1 0 C 0 0 D 0 0 The above behavior is observed from the native Contacts app. The \"super primary\" data of an aggregate Contact is referred to as the \"default\".","title":"Developer notes (or for advanced users)"},{"location":"other/get-set-remove-contact-raw-contact-photo/","text":"Get set remove full-sized and thumbnail contact photos \u00b6 This library provides several functions to interact with Contact and RawContact full-sized and thumbnail photos. Contact and RawContact photos \u00b6 The photo assigned to a Contact is just a reference to a photo assigned to a RawContact. If a Contact consists of more than one RawContact, only the photo from one of the RawContacts will be used by the Contact. Setting/removing the (main) RawContact's photo will in turn change the Contact photo because the Contact photo is just a reference to the RawContact photo. The inverse is also true. RawContact photos are retained when linking and unlinking. For more info, read Link unlink Contacts . Full-sized photos and thumbnails \u00b6 Each RawContact may be assigned one photo. The thumbnail is just a downsized version of the full-sized photo. The full-sized photo is typically displayed in a large view, such as in a contact detail screen. The thumbnail is typically displayed in small views, such as in a contacts list view. Setting the full-sized photo will automatically set the thumbnail. The Contacts Provider automatically creates a downsized version of the full-sized photo. Getting contact photo \u00b6 There are several ways to do this. Using query APIs to get a list of Contact s with photo uris, val contacts = Contacts ( context ) . query () // if you only want to include photo data in the returned Contacts . include ( Fields . Contact . PhotoUri , Fields . Contact . PhotoThumbnailUri ) . find () for ( contact in contacts ) { Log . d ( \"Contact\" , \"\"\" Photo Uri: ${ contact . photoUri } Thumbnail Uri: ${ contact . photoThumbnailUri } \"\"\" . trimIndent () ) } For more info, read Query contacts and Query contacts (advanced) . Using one of the extension functions in contacts.core.util.ContactPhoto.kt to get photo data, val photoInputStream = contact . photoInputStream ( contactsApi ) val photoBytes = contact . photoBytes ( contactsApi ) val photoBitmap = contact . photoBitmap ( contactsApi ) val photoBitmapDrawable = contact . photoBitmapDrawable ( contactsApi ) val photoThumbnailInputStream = contact . photoThumbnailInputStream ( contactsApi ) val photoThumbnailBytes = contact . photoThumbnailBytes ( contactsApi ) val photoThumbnailBitmap = contact . photoThumbnailBitmap ( contactsApi ) val photoThumbnailBitmapDrawable = contact . photoThumbnailBitmapDrawable ( contactsApi ) To get RawContact photos directly, use one of the extension functions in contacts.core.util.RawContactPhoto.kt , val photoInputStream = rawContact . photoInputStream ( contactsApi ) val photoBytes = rawContact . photoBytes ( contactsApi ) val photoBitmap = rawContact . photoBitmap ( contactsApi ) val photoBitmapDrawable = rawContact . photoBitmapDrawable ( contactsApi ) val photoThumbnailInputStream = rawContact . photoThumbnailInputStream ( contactsApi ) val photoThumbnailBytes = rawContact . photoThumbnailBytes ( contactsApi ) val photoThumbnailBitmap = rawContact . photoThumbnailBitmap ( contactsApi ) val photoThumbnailBitmapDrawable = rawContact . photoThumbnailBitmapDrawable ( contactsApi ) Keep in mind that the Contact photo is just a reference to one of its RawContact's photo. Setting contact photo \u00b6 Setting the photo can only be done after the Contact or RawContact has been inserted. In other words, photo management can only be done for existing Contacts/RawContacts. To set the Contact photo, use one of the extension functions in contacts.core.util.ContactPhoto.kt , contact . setPhoto ( contactsApi , photoInputStream ) contact . setPhoto ( contactsApi , photoBytes ) contact . setPhoto ( contactsApi , photoBitmap ) contact . setPhoto ( contactsApi , photoBitmapDrawable ) Setting the full-sized photo will automatically set the thumbnail. The Contacts Provider automatically creates a downsized version of the full-sized photo. To set a RawContact photo, use one of the extension functions in contacts.core.util.RawContactPhoto.kt , rawContact . setPhoto ( contactsApi , photoInputStream ) rawContact . setPhoto ( contactsApi , photoBytes ) rawContact . setPhoto ( contactsApi , photoBitmap ) rawContact . setPhoto ( contactsApi , photoBitmapDrawable ) Keep in mind that the Contact photo is just a reference to one of its RawContact's photo. Removing contact photo \u00b6 To remove the Contact (and corresponding RawContact) photo (full-sized and thumbnail), contact . removePhoto ( contactsApi ) To remove a specific RawContact's photo (full-sized and thumbnail), rawContact . removePhoto ( contactsApi ) Keep in mind that the Contact photo is just a reference to one of its RawContact's photo. A few things to keep in mind. Changes are immediate and are not applied to the receiver \u00b6 These apply to set and remove functions. Changes are immediate. These functions will make the changes to the Contacts Provider database immediately. You do not need to use update APIs to commit the changes. Changes are not applied to the receiver. This function call does NOT mutate immutable or mutable receivers. Therefore, you should use query APIs or refresh extensions or process the result of this function call to get the most up-to-date reference to mutable or immutable entity that contains the changes in the Contacts Provider database. Using the ui PhotoPicker extensions \u00b6 The contacts.ui.util.PhotoPicker.kt in the ui module` provides extension functions to make selecting existing photos, taking new photos, and removing photos easier. It provides you the same UX as the native Contacts app. To use it, Activity { fun onPhotoViewClicked () { showPhotoPickerDialog ( withRemovePhotoOption = true , removePhoto = { contact . removePhoto ( contactsApi ) } ) } override fun onActivityResult ( requestCode : Int , resultCode : Int , data : Intent?) { super . onActivityResult ( requestCode , resultCode , data ) onPhotoPicked ( requestCode , resultCode , data , photoBitmapPicked = { photoBitmap -> contact . setPhoto ( contactsApi , photoBitmap ) }, photoUriPicked = { uri -> // Note that bitmap decoding should be done in a non-UI thread. Threading has been // left out of this example for brevity. val photoBitmap = if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . P ) { ImageDecoder . decodeBitmap ( ImageDecoder . createSource ( context . contentResolver , uri )) } else { MediaStore . Images . Media . getBitmap ( context . contentResolver , uri ) } contact . setPhoto ( contactsApi , photoBitmap ) } ) } } Starting with Android 11 (API 30), you must include the following to your manifest in order to successfully use the above functions. Performing photo management asynchronously \u00b6 All of the code shown in this guide are done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing photo management with permission \u00b6 Getting and setting photos require the android.permission.READ_CONTACTS and android.permission.WRITE_CONTACTS permissions respectively. If not granted, getting/setting photos will fail. TODO Update this section as part of issue #119 . FAQs \u00b6 Can contacts be insert with photo? \u00b6 Related issues; #116 and #119 You cannot get/set/remove photos for Contacts/RawContacts that have not yet been inserted in the Contacts Provider database. In other words, only Contacts/RawContacts retrieved via query or result APIs can use the extension functions in contacts.core.util.ContactPhoto.kt and contacts.core.util.RawContactPhoto.kt . To insert a new contact \"with photo\", you should insert the contact first. Then, if the insert succeeds, proceed to set the photo. For more info about insert, read Insert contacts . Note for contributors; It is possible to include photo thumbnail data as part of the insertion of a new RawContact using ContactsContract.CommonDataKinds.Photo.PHOTO . The Contacts Provider will use the thumbnail as the full-sized photo as well. However, this is not good practice as the full-sized photo will have a really low resolution. Showing the full-sized photo in a big view will not look good. Therefore, this library does not allow this. Consumers must first insert their new RawContact so that they can set the full-sized photo. Can photo be set using a uri instead of bytes and bitmaps? \u00b6 Related issues; #109 No and yes. The core APIs provided in this library only provides functions that the Contacts Provider natively supports. This means setting Contact or RawContact photo only using bytes (and other similar types). See documentation in ContactsContract.RawContacts.DisplayPhoto . Photos are stored and managed by the Contacts Provider, which in turn provides specific URIs for RawContacts and Contacts for read/write access to those photos. We cannot simply just pass in our own URIs. The Contacts Provider will not accept it. The Contacts Provider will only accept raw photo data. It will then generate and manage URIs on its own automatically to enforce data integrity. Consumers may write their own functions to convert a URI to a byte array or bitmap using whatever imaging libraries they want. Certain URIs/URLs may require networking and heavy image processing, which this Contacts library will not cover! URI/URL to image conversion simply does not belong in this library!","title":"Get set remove full-sized and thumbnail contact photos"},{"location":"other/get-set-remove-contact-raw-contact-photo/#get-set-remove-full-sized-and-thumbnail-contact-photos","text":"This library provides several functions to interact with Contact and RawContact full-sized and thumbnail photos.","title":"Get set remove full-sized and thumbnail contact photos"},{"location":"other/get-set-remove-contact-raw-contact-photo/#contact-and-rawcontact-photos","text":"The photo assigned to a Contact is just a reference to a photo assigned to a RawContact. If a Contact consists of more than one RawContact, only the photo from one of the RawContacts will be used by the Contact. Setting/removing the (main) RawContact's photo will in turn change the Contact photo because the Contact photo is just a reference to the RawContact photo. The inverse is also true. RawContact photos are retained when linking and unlinking. For more info, read Link unlink Contacts .","title":"Contact and RawContact photos"},{"location":"other/get-set-remove-contact-raw-contact-photo/#full-sized-photos-and-thumbnails","text":"Each RawContact may be assigned one photo. The thumbnail is just a downsized version of the full-sized photo. The full-sized photo is typically displayed in a large view, such as in a contact detail screen. The thumbnail is typically displayed in small views, such as in a contacts list view. Setting the full-sized photo will automatically set the thumbnail. The Contacts Provider automatically creates a downsized version of the full-sized photo.","title":"Full-sized photos and thumbnails"},{"location":"other/get-set-remove-contact-raw-contact-photo/#getting-contact-photo","text":"There are several ways to do this. Using query APIs to get a list of Contact s with photo uris, val contacts = Contacts ( context ) . query () // if you only want to include photo data in the returned Contacts . include ( Fields . Contact . PhotoUri , Fields . Contact . PhotoThumbnailUri ) . find () for ( contact in contacts ) { Log . d ( \"Contact\" , \"\"\" Photo Uri: ${ contact . photoUri } Thumbnail Uri: ${ contact . photoThumbnailUri } \"\"\" . trimIndent () ) } For more info, read Query contacts and Query contacts (advanced) . Using one of the extension functions in contacts.core.util.ContactPhoto.kt to get photo data, val photoInputStream = contact . photoInputStream ( contactsApi ) val photoBytes = contact . photoBytes ( contactsApi ) val photoBitmap = contact . photoBitmap ( contactsApi ) val photoBitmapDrawable = contact . photoBitmapDrawable ( contactsApi ) val photoThumbnailInputStream = contact . photoThumbnailInputStream ( contactsApi ) val photoThumbnailBytes = contact . photoThumbnailBytes ( contactsApi ) val photoThumbnailBitmap = contact . photoThumbnailBitmap ( contactsApi ) val photoThumbnailBitmapDrawable = contact . photoThumbnailBitmapDrawable ( contactsApi ) To get RawContact photos directly, use one of the extension functions in contacts.core.util.RawContactPhoto.kt , val photoInputStream = rawContact . photoInputStream ( contactsApi ) val photoBytes = rawContact . photoBytes ( contactsApi ) val photoBitmap = rawContact . photoBitmap ( contactsApi ) val photoBitmapDrawable = rawContact . photoBitmapDrawable ( contactsApi ) val photoThumbnailInputStream = rawContact . photoThumbnailInputStream ( contactsApi ) val photoThumbnailBytes = rawContact . photoThumbnailBytes ( contactsApi ) val photoThumbnailBitmap = rawContact . photoThumbnailBitmap ( contactsApi ) val photoThumbnailBitmapDrawable = rawContact . photoThumbnailBitmapDrawable ( contactsApi ) Keep in mind that the Contact photo is just a reference to one of its RawContact's photo.","title":"Getting contact photo"},{"location":"other/get-set-remove-contact-raw-contact-photo/#setting-contact-photo","text":"Setting the photo can only be done after the Contact or RawContact has been inserted. In other words, photo management can only be done for existing Contacts/RawContacts. To set the Contact photo, use one of the extension functions in contacts.core.util.ContactPhoto.kt , contact . setPhoto ( contactsApi , photoInputStream ) contact . setPhoto ( contactsApi , photoBytes ) contact . setPhoto ( contactsApi , photoBitmap ) contact . setPhoto ( contactsApi , photoBitmapDrawable ) Setting the full-sized photo will automatically set the thumbnail. The Contacts Provider automatically creates a downsized version of the full-sized photo. To set a RawContact photo, use one of the extension functions in contacts.core.util.RawContactPhoto.kt , rawContact . setPhoto ( contactsApi , photoInputStream ) rawContact . setPhoto ( contactsApi , photoBytes ) rawContact . setPhoto ( contactsApi , photoBitmap ) rawContact . setPhoto ( contactsApi , photoBitmapDrawable ) Keep in mind that the Contact photo is just a reference to one of its RawContact's photo.","title":"Setting contact photo"},{"location":"other/get-set-remove-contact-raw-contact-photo/#removing-contact-photo","text":"To remove the Contact (and corresponding RawContact) photo (full-sized and thumbnail), contact . removePhoto ( contactsApi ) To remove a specific RawContact's photo (full-sized and thumbnail), rawContact . removePhoto ( contactsApi ) Keep in mind that the Contact photo is just a reference to one of its RawContact's photo. A few things to keep in mind.","title":"Removing contact photo"},{"location":"other/get-set-remove-contact-raw-contact-photo/#changes-are-immediate-and-are-not-applied-to-the-receiver","text":"These apply to set and remove functions. Changes are immediate. These functions will make the changes to the Contacts Provider database immediately. You do not need to use update APIs to commit the changes. Changes are not applied to the receiver. This function call does NOT mutate immutable or mutable receivers. Therefore, you should use query APIs or refresh extensions or process the result of this function call to get the most up-to-date reference to mutable or immutable entity that contains the changes in the Contacts Provider database.","title":"Changes are immediate and are not applied to the receiver"},{"location":"other/get-set-remove-contact-raw-contact-photo/#using-the-ui-photopicker-extensions","text":"The contacts.ui.util.PhotoPicker.kt in the ui module` provides extension functions to make selecting existing photos, taking new photos, and removing photos easier. It provides you the same UX as the native Contacts app. To use it, Activity { fun onPhotoViewClicked () { showPhotoPickerDialog ( withRemovePhotoOption = true , removePhoto = { contact . removePhoto ( contactsApi ) } ) } override fun onActivityResult ( requestCode : Int , resultCode : Int , data : Intent?) { super . onActivityResult ( requestCode , resultCode , data ) onPhotoPicked ( requestCode , resultCode , data , photoBitmapPicked = { photoBitmap -> contact . setPhoto ( contactsApi , photoBitmap ) }, photoUriPicked = { uri -> // Note that bitmap decoding should be done in a non-UI thread. Threading has been // left out of this example for brevity. val photoBitmap = if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . P ) { ImageDecoder . decodeBitmap ( ImageDecoder . createSource ( context . contentResolver , uri )) } else { MediaStore . Images . Media . getBitmap ( context . contentResolver , uri ) } contact . setPhoto ( contactsApi , photoBitmap ) } ) } } Starting with Android 11 (API 30), you must include the following to your manifest in order to successfully use the above functions. ","title":"Using the ui PhotoPicker extensions"},{"location":"other/get-set-remove-contact-raw-contact-photo/#performing-photo-management-asynchronously","text":"All of the code shown in this guide are done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing photo management asynchronously"},{"location":"other/get-set-remove-contact-raw-contact-photo/#performing-photo-management-with-permission","text":"Getting and setting photos require the android.permission.READ_CONTACTS and android.permission.WRITE_CONTACTS permissions respectively. If not granted, getting/setting photos will fail. TODO Update this section as part of issue #119 .","title":"Performing photo management with permission"},{"location":"other/get-set-remove-contact-raw-contact-photo/#faqs","text":"","title":"FAQs"},{"location":"other/get-set-remove-contact-raw-contact-photo/#can-contacts-be-insert-with-photo","text":"Related issues; #116 and #119 You cannot get/set/remove photos for Contacts/RawContacts that have not yet been inserted in the Contacts Provider database. In other words, only Contacts/RawContacts retrieved via query or result APIs can use the extension functions in contacts.core.util.ContactPhoto.kt and contacts.core.util.RawContactPhoto.kt . To insert a new contact \"with photo\", you should insert the contact first. Then, if the insert succeeds, proceed to set the photo. For more info about insert, read Insert contacts . Note for contributors; It is possible to include photo thumbnail data as part of the insertion of a new RawContact using ContactsContract.CommonDataKinds.Photo.PHOTO . The Contacts Provider will use the thumbnail as the full-sized photo as well. However, this is not good practice as the full-sized photo will have a really low resolution. Showing the full-sized photo in a big view will not look good. Therefore, this library does not allow this. Consumers must first insert their new RawContact so that they can set the full-sized photo.","title":"Can contacts be insert with photo?"},{"location":"other/get-set-remove-contact-raw-contact-photo/#can-photo-be-set-using-a-uri-instead-of-bytes-and-bitmaps","text":"Related issues; #109 No and yes. The core APIs provided in this library only provides functions that the Contacts Provider natively supports. This means setting Contact or RawContact photo only using bytes (and other similar types). See documentation in ContactsContract.RawContacts.DisplayPhoto . Photos are stored and managed by the Contacts Provider, which in turn provides specific URIs for RawContacts and Contacts for read/write access to those photos. We cannot simply just pass in our own URIs. The Contacts Provider will not accept it. The Contacts Provider will only accept raw photo data. It will then generate and manage URIs on its own automatically to enforce data integrity. Consumers may write their own functions to convert a URI to a byte array or bitmap using whatever imaging libraries they want. Certain URIs/URLs may require networking and heavy image processing, which this Contacts library will not cover! URI/URL to image conversion simply does not belong in this library!","title":"Can photo be set using a uri instead of bytes and bitmaps?"},{"location":"other/link-unlink-contacts/","text":"Link unlink Contacts \u00b6 The Contacts Provider automatically aggregates similar RawContacts into a single Contact when it determines that they reference the same person. However, the Contacts Provider's aggregation algorithms are only as accurate as the Data belonging to these RawContacts. Sometimes, they are not enough to determine if they indeed are the same person. With this in mind, the Contacts Provider allows us to explicitly and forcefully specify whether two or more RawContacts reference the same person or not. Hence, this library provides extensions in contacts.core.util.ContactLinks.kt to allow for linking and unlinking two or more Contacts (and their constituent RawContacts). Linking \u00b6 To link three Contacts and all of their constituent RawContacts into a single Contact, val linkResult = contact1 . link ( contactsApi , contact2 , contact3 ) The above links (keep together) all RawContacts belonging to contact1 , contact2 , and contact3 into a single Contact. Aggregation is done by the Contacts Provider. For example, Contact (id: 1, display name: A) RawContact A Contact (id: 2, display name: B) RawContact B RawContact C Linking Contact 1 with Contact 2 results in; Contact (id: 1, display name: A) RawContact A RawContact B RawContact C Contact 2 no longer exists and all of the Data belonging to RawContact B and C are now associated with Contact 1. If instead Contact 2 is linked with Contact 1; Contact (id: 1, display name: B) RawContact A RawContact B RawContact C The same thing occurs except the display name has been set to the display name of RawContact B. This function only instructs the Contacts Provider which RawContacts should be aggregated to a single Contact. Details on how RawContacts are aggregated into a single Contact are left to the Contacts Provider. Profile Contact/RawContacts are not supported! This operation will fail if given any profile Contact/RawContacts . Handling the link result \u00b6 To check if the link succeeded, val linkSuccessful = linkResult . isSuccessful To get the ID of the parent Contact of all linked RawContacts, val contactId : Long? = linkResult . contactId Note that the contactId will belong to one of the linked Contacts. Once you have the Contact ID, you can retrieve the Contact via the Query API, val contact = contactsApi . query () . where { Contact . Id equalTo contactId } . find () For more info, read Query contacts (advanced) . Alternatively, you may use the extensions provided in ContactLikResult . To get the parent Contact of all linked RawContacts, val contact = linkResult . contact ( contactsApi ) Unlinking \u00b6 To unlink a Contacts with more than one RawContact into a separate Contacts, val unlinkResult = contact . unlink ( contactsApi ) The above unlinks (keep separate) all RawContacts belonging to the contact into separate Contacts. The above does nothing and will fail if the Contact only has one constituent RawContact. Profile Contact/RawContacts are not supported! This operation will fail if given any profile Contact/RawContacts . Handling the unlink result \u00b6 To check if the unlink succeeded, val unlinkSuccessful = unlinkResult . isSuccessful To get the IDs of the constituent RawContact of of the Contact that has been unlinked, val rawContactIds = unlinkResult . rawContactIds Once you have the RawContact IDs, you can retrieve the corresponding Contacts via the Query API, val contacts = contactsApi . query () . where { RawContact . Id `in` rawContactIds } . find () For more info, read Query contacts (advanced) . Alternatively, you may use the extensions provided in ContactLikResult . To get the Contacts of all unlinked RawContacts, val contacts = unlinkResult . contacts ( contactsApi ) Changes are immediate and are not applied to the receiver \u00b6 These apply to set and clear functions. Changes are immediate. These functions will make the changes to the Contacts Provider database immediately. You do not need to use update APIs to commit the changes. Changes are not applied to the receiver. This function call does NOT mutate immutable or mutable receivers. Therefore, you should use query APIs or refresh extensions or process the result of this function call to get the most up-to-date reference to mutable or immutable entity that contains the changes in the Contacts Provider database. Performing linking/unlinking asynchronously \u00b6 Linking or unlinking contacts is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing linking/unlinking with permission \u00b6 Getting and setting/clearing default data require the android.permission.WRITE_CONTACTS permission. If not granted, linking/unlinking data will fail. TODO Update this section as part of issue #138 . Syncing is done at the RawContact level \u00b6 You may link Contacts with RawContacts that belong to different Accounts. Any RawContact Data modifications are synced per Account sync settings. For more info, read Sync contact data across devices . RawContacts that are not associated with an Account are local to the device and therefore will not be synced even if it is linked to a Contact with a RawContact that is associated with an Account. For more info, read about Local (device-only) contacts . Developer notes (or for advanced users) \u00b6 The following section are note from developers of this library for other developers. It is copied from the DEV_NOTES . You may still read the following as a consumer of the library in case you need deeper insight. Behavior of linking/merging/joining contacts (AggregationExceptions) \u00b6 The native Contacts app terminology has changed over time; API 22 and below; join / separate API 23; merge / unmerge API 24 and above; link / unlink However, the internals have not changed; KEEP_TOGETHER / KEEP_SEPARATE . These operations are supported by the ContactsContract.AggregationExceptions . For example, given the following tables, ### Contacts table Contact id: 32, displayName: X, starred: 0, timesContacted: 1, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 Contact id: 33, displayName: Y, starred: 1, timesContacted: 2, lastTimeContacted: 1573071750624, customRingtone: content://media/internal/audio/media/115, sendToVoicemail: 1 ### RawContacts table RawContact id: 30, contactId: 32, displayName: X, accountName: x@x.com, accountType: com.google, starred: 0, timesContacted: 1, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 RawContact id: 31, contactId: 33, displayName: Y, accountName: y@y.com, accountType: com.google, starred: 1, timesContacted: 2, lastTimeContacted: 1573071750624, customRingtone: content://media/internal/audio/media/115, sendToVoicemail: 1 ### Data table Data id: 57, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/group_membership, data1: 18 Data id: 58, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/name, data1: X, isPrimary: 1, isSuperPrimary: 1 Data id: 59, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: x@x.com Data id: 60, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: xx@x.com, isPrimary: 1, isSuperPrimary: 1 Data id: 63, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/group_membership, data1: 6 Data id: 64, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/name, data1: Y, isPrimary: 1, isSuperPrimary: 1 Data id: 65, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/email_v2, data1: y@y.com Data id: 66, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/email_v2, data1: yy@y.com, isPrimary: 1, isSuperPrimary: 1 When Contact X links/merges/joins Contact Y , the tables becomes; ### Contacts table Contact id: 32, displayName: X, starred: 1, timesContacted: 2, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 ### RawContacts table RawContact id: 30, contactId: 32, displayName: X, accountName: x@x.com, accountType: com.google, starred: 0, timesContacted: 1, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 RawContact id: 31, contactId: 32, displayName: Y, accountName: y@y.com, accountType: com.google, starred: 1, timesContacted: 2, lastTimeContacted: 1573071750624, customRingtone: content://media/internal/audio/media/115, sendToVoicemail: 1 ### Data table Data id: 57, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/group_membership, data1: 18 Data id: 58, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/name, data1: X, isPrimary: 1, isSuperPrimary: 1 Data id: 59, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: x@x.com Data id: 60, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: xx@x.com, isPrimary: 1, isSuperPrimary: 0 Data id: 63, rawContactId: 31, contactId: 32, mimeType: vnd.android.cursor.item/group_membership, data1: 6 Data id: 64, rawContactId: 31, contactId: 32, mimeType: vnd.android.cursor.item/name, data1: Y, isPrimary: 1, isSuperPrimary: 0 Data id: 65, rawContactId: 31, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: y@y.com Data id: 66, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/email_v2, data1: yy@y.com, isPrimary: 1, isSuperPrimary: 0 What changed? Contact Y's row has been deleted and its column values have been merged into Contact X row. If the reverse occurred (Contact Y merged with Contact X), Contact Y's row would still be deleted. The difference is that Contact X's display name will be set to Contact Y's display name, which is done by the native Contacts app manually by setting Contact Y's Data name row to be the \"default\" (isPrimary and isSuperPrimary both set to 1). The AggregationExceptions table records the linked RawContacts's IDs in ascending order regardless of the order used in RAW_CONTACT_ID1 and RAW_CONTACT_ID2 at the time of merging. The RawContacts and Data table remains the same except the joined contactId column values have now been changed to the id of Contact X. All Data rows' isSuperPrimary value has been set to 0 though the isPrimary columns remain the same. In other words, this clears any \"default\" set before the link. These are done automatically by the Contacts Provider during the link operation. What is not done automatically by the Contacts Provider is that the name row of former Contact X is set as the default. The native Contacts app does this manually. The Contacts Providers automatically sets the Contact display name to whatever the default name row is for the Contact, if available. For more info on Contact display name resolution, read the Contact Display Name and Default Name Rows section. Note that display name resolution is different for APIs below 21 (pre-lollipop). The display name of the RawContacts remain the same. The Groups table remains unmodified. Options updates Changes to the options (starred, timesContacted, lastTimeContacted, customRingtone, and sendToVoicemail) of a RawContact may affect the options of the parent Contact. On the other hand, changes to the options of the parent Contact will be propagated to all child RawContact options. Photo updates A RawContact may have a full-sized photo saved as a file and a thumbnail version of that saved in the Data table in a photo mimetype row. A Contact's full-sized photo and thumbnail are simply references to the \"chosen\" RawContact's full-sized photo and thumbnail (though the URIs may differ). Note that when removing the photo in the native contacts app, the photo data row is not immediately deleted, though the PHOTO_FILE_ID is immediately set to null. This may result in the PHOTO_URI and PHOTO_THUMBNAIL_URI to still have a valid image uri even though the photo has been \"removed\". This library immediately deletes the photo data row, which seems to work perfectly. Data inserts In the native Contacts app, Data inserted in combined (raw) contacts mode will be associated to the first RawContact in the list sorted by the RawContact ID. This may not be the same as the RawContact referenced by ContactsColumns.NAME_RAW_CONTACT_ID . UI changes? The native Contacts App does not display the groups field when displaying / editing Contacts that have multiple RawContacts (linked/merged/joined) in combined mode. However, it does allow editing individual RawContact Data rows in which case the groups field is displayed and editable. In the native Contacts app, the name attribute used comes from the name row with IS_SUPER_PRIMARY set to true. This and all other \"unique\" mimetypes (organization) and non-unique mimetypes (email) per RawContact are shown only if they are not blank. Showing multiple RawContact's data in the same edit screen (combined mode) In older version of the native, Android Open Source Project (AOSP) Contacts app, data from multiple RawContacts was being shown in the same edit screen. This caused a lot of confusion about which data belonged to which RawContact. Newer versions of AOSP Contacts only allow editing one RawContact at a time to avoid confusion. Though, several RawContacts' data are still shown (not-editable) in the same screen. AggregationExceptions table \u00b6 Given the following Contacts and their RawContacts; Contact A RawContact 1 Contact B RawContact 2 Contact C RawContact 3 Contact D RawContact 4 Linking one by one in this order; Contact B link Contact A Contact C link Contact D Contact C link Contact B Results in the following AggregationExceptions rows respectively; Aggregation exception id: 430, type: 1, rawContactId1: 1, rawContactId2: 2 Aggregation exception id: 430, type: 1, rawContactId1: 1, rawContactId2: 2 Aggregation exception id: 432, type: 1, rawContactId1: 3, rawContactId2: 4 Aggregation exception id: 436, type: 1, rawContactId1: 1, rawContactId2: 2 Aggregation exception id: 439, type: 1, rawContactId1: 1, rawContactId2: 3 Aggregation exception id: 442, type: 1, rawContactId1: 1, rawContactId2: 4 Aggregation exception id: 440, type: 1, rawContactId1: 2, rawContactId2: 3 Aggregation exception id: 443, type: 1, rawContactId1: 2, rawContactId2: 4 Aggregation exception id: 444, type: 1, rawContactId1: 3, rawContactId2: 4 There is a pattern here. RawContact ids are sorted in ascending order and linked from least to greatest exhaustively but no double links (1-2 is the same as 2-1). RawContact 1 has a row with RawContact 2, 3, and 4. RawContact 2 has a row with RawContact 3 and 4. RawContact 3 has a row with RawContact 4. Linking all in one go; Contact C link Contact A, B, D Results in the same AggregationExceptions rows. Unlinking results in the same AggregationExceptions rows except the type is 2 ( TYPE_KEEP_SEPARATE ). Contact Display Name and Default Name Rows \u00b6 If available, the \"default\" (isPrimary and isSuperPrimary set to 1) name row for a Contact is automatically set as the Contact display name by the Contacts Provider. Otherwise, the Contacts Provider chooses from any of the other suitable data from the aggregate Contact. The ContactsColumns.NAME_RAW_CONTACT_ID is automatically updated by the Contacts Provider along with the display name. The default status of other sources (e.g. email) does not affect the Contact display name. The native Contacts app also sets the most recently updated name as the default at every update. This results in the Contact display name changing to the most recently updated name from one of the associated RawContacts. The \"most recently updated name\" is the name field that was last updated by the user when editing in the Contacts app, which is irrelevant to its value. It does not matter if the user deleted the last character of the name, added the same character back, and then saved. It still counts as the most recently updated. All of the above only applies to API 21 and above. Display name resolution is different for APIs below 21 (pre-Lollipop)! The ContactsColumns.NAME_RAW_CONTACT_ID was added in API 21. It changed the way display names are resolved for Contacts with more than one constituent RawContacts, which is what has been described so far. Before this change (APIs 20 and below), the native Contacts app is still able to set the Contact display name somehow. I'm not sure how. If someone figures it out, please let me know. I tried updating the Contact DISPLAY_NAME directly but it does not work. Setting a name row as default also does not affect the Contact DISPLAY_NAME . Effects of linking/unlinking contacts \u00b6 When two or more Contacts (along with their constituent RawContacts) are linked into a single Contact those Contacts will be merged into one of the existing Contact row. The Contacts that have been merged into the single Contact will have their entries/rows in the Contacts table deleted. Unlinking will result in the original Contacts prior to linking to have new rows in the Contacts table with different IDs because the previously deleted row IDs cannot be reused. Getting Contacts that have been linked into a single Contact or Contacts whose row IDs have change after unlinking is still possible using the Contact lookup key. For more info, read about Contact lookup key vs ID .","title":"Link unlink Contacts"},{"location":"other/link-unlink-contacts/#link-unlink-contacts","text":"The Contacts Provider automatically aggregates similar RawContacts into a single Contact when it determines that they reference the same person. However, the Contacts Provider's aggregation algorithms are only as accurate as the Data belonging to these RawContacts. Sometimes, they are not enough to determine if they indeed are the same person. With this in mind, the Contacts Provider allows us to explicitly and forcefully specify whether two or more RawContacts reference the same person or not. Hence, this library provides extensions in contacts.core.util.ContactLinks.kt to allow for linking and unlinking two or more Contacts (and their constituent RawContacts).","title":"Link unlink Contacts"},{"location":"other/link-unlink-contacts/#linking","text":"To link three Contacts and all of their constituent RawContacts into a single Contact, val linkResult = contact1 . link ( contactsApi , contact2 , contact3 ) The above links (keep together) all RawContacts belonging to contact1 , contact2 , and contact3 into a single Contact. Aggregation is done by the Contacts Provider. For example, Contact (id: 1, display name: A) RawContact A Contact (id: 2, display name: B) RawContact B RawContact C Linking Contact 1 with Contact 2 results in; Contact (id: 1, display name: A) RawContact A RawContact B RawContact C Contact 2 no longer exists and all of the Data belonging to RawContact B and C are now associated with Contact 1. If instead Contact 2 is linked with Contact 1; Contact (id: 1, display name: B) RawContact A RawContact B RawContact C The same thing occurs except the display name has been set to the display name of RawContact B. This function only instructs the Contacts Provider which RawContacts should be aggregated to a single Contact. Details on how RawContacts are aggregated into a single Contact are left to the Contacts Provider. Profile Contact/RawContacts are not supported! This operation will fail if given any profile Contact/RawContacts .","title":"Linking"},{"location":"other/link-unlink-contacts/#handling-the-link-result","text":"To check if the link succeeded, val linkSuccessful = linkResult . isSuccessful To get the ID of the parent Contact of all linked RawContacts, val contactId : Long? = linkResult . contactId Note that the contactId will belong to one of the linked Contacts. Once you have the Contact ID, you can retrieve the Contact via the Query API, val contact = contactsApi . query () . where { Contact . Id equalTo contactId } . find () For more info, read Query contacts (advanced) . Alternatively, you may use the extensions provided in ContactLikResult . To get the parent Contact of all linked RawContacts, val contact = linkResult . contact ( contactsApi )","title":"Handling the link result"},{"location":"other/link-unlink-contacts/#unlinking","text":"To unlink a Contacts with more than one RawContact into a separate Contacts, val unlinkResult = contact . unlink ( contactsApi ) The above unlinks (keep separate) all RawContacts belonging to the contact into separate Contacts. The above does nothing and will fail if the Contact only has one constituent RawContact. Profile Contact/RawContacts are not supported! This operation will fail if given any profile Contact/RawContacts .","title":"Unlinking"},{"location":"other/link-unlink-contacts/#handling-the-unlink-result","text":"To check if the unlink succeeded, val unlinkSuccessful = unlinkResult . isSuccessful To get the IDs of the constituent RawContact of of the Contact that has been unlinked, val rawContactIds = unlinkResult . rawContactIds Once you have the RawContact IDs, you can retrieve the corresponding Contacts via the Query API, val contacts = contactsApi . query () . where { RawContact . Id `in` rawContactIds } . find () For more info, read Query contacts (advanced) . Alternatively, you may use the extensions provided in ContactLikResult . To get the Contacts of all unlinked RawContacts, val contacts = unlinkResult . contacts ( contactsApi )","title":"Handling the unlink result"},{"location":"other/link-unlink-contacts/#changes-are-immediate-and-are-not-applied-to-the-receiver","text":"These apply to set and clear functions. Changes are immediate. These functions will make the changes to the Contacts Provider database immediately. You do not need to use update APIs to commit the changes. Changes are not applied to the receiver. This function call does NOT mutate immutable or mutable receivers. Therefore, you should use query APIs or refresh extensions or process the result of this function call to get the most up-to-date reference to mutable or immutable entity that contains the changes in the Contacts Provider database.","title":"Changes are immediate and are not applied to the receiver"},{"location":"other/link-unlink-contacts/#performing-linkingunlinking-asynchronously","text":"Linking or unlinking contacts is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing linking/unlinking asynchronously"},{"location":"other/link-unlink-contacts/#performing-linkingunlinking-with-permission","text":"Getting and setting/clearing default data require the android.permission.WRITE_CONTACTS permission. If not granted, linking/unlinking data will fail. TODO Update this section as part of issue #138 .","title":"Performing linking/unlinking with permission"},{"location":"other/link-unlink-contacts/#syncing-is-done-at-the-rawcontact-level","text":"You may link Contacts with RawContacts that belong to different Accounts. Any RawContact Data modifications are synced per Account sync settings. For more info, read Sync contact data across devices . RawContacts that are not associated with an Account are local to the device and therefore will not be synced even if it is linked to a Contact with a RawContact that is associated with an Account. For more info, read about Local (device-only) contacts .","title":"Syncing is done at the RawContact level"},{"location":"other/link-unlink-contacts/#developer-notes-or-for-advanced-users","text":"The following section are note from developers of this library for other developers. It is copied from the DEV_NOTES . You may still read the following as a consumer of the library in case you need deeper insight.","title":"Developer notes (or for advanced users)"},{"location":"other/link-unlink-contacts/#behavior-of-linkingmergingjoining-contacts-aggregationexceptions","text":"The native Contacts app terminology has changed over time; API 22 and below; join / separate API 23; merge / unmerge API 24 and above; link / unlink However, the internals have not changed; KEEP_TOGETHER / KEEP_SEPARATE . These operations are supported by the ContactsContract.AggregationExceptions . For example, given the following tables, ### Contacts table Contact id: 32, displayName: X, starred: 0, timesContacted: 1, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 Contact id: 33, displayName: Y, starred: 1, timesContacted: 2, lastTimeContacted: 1573071750624, customRingtone: content://media/internal/audio/media/115, sendToVoicemail: 1 ### RawContacts table RawContact id: 30, contactId: 32, displayName: X, accountName: x@x.com, accountType: com.google, starred: 0, timesContacted: 1, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 RawContact id: 31, contactId: 33, displayName: Y, accountName: y@y.com, accountType: com.google, starred: 1, timesContacted: 2, lastTimeContacted: 1573071750624, customRingtone: content://media/internal/audio/media/115, sendToVoicemail: 1 ### Data table Data id: 57, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/group_membership, data1: 18 Data id: 58, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/name, data1: X, isPrimary: 1, isSuperPrimary: 1 Data id: 59, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: x@x.com Data id: 60, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: xx@x.com, isPrimary: 1, isSuperPrimary: 1 Data id: 63, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/group_membership, data1: 6 Data id: 64, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/name, data1: Y, isPrimary: 1, isSuperPrimary: 1 Data id: 65, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/email_v2, data1: y@y.com Data id: 66, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/email_v2, data1: yy@y.com, isPrimary: 1, isSuperPrimary: 1 When Contact X links/merges/joins Contact Y , the tables becomes; ### Contacts table Contact id: 32, displayName: X, starred: 1, timesContacted: 2, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 ### RawContacts table RawContact id: 30, contactId: 32, displayName: X, accountName: x@x.com, accountType: com.google, starred: 0, timesContacted: 1, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 RawContact id: 31, contactId: 32, displayName: Y, accountName: y@y.com, accountType: com.google, starred: 1, timesContacted: 2, lastTimeContacted: 1573071750624, customRingtone: content://media/internal/audio/media/115, sendToVoicemail: 1 ### Data table Data id: 57, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/group_membership, data1: 18 Data id: 58, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/name, data1: X, isPrimary: 1, isSuperPrimary: 1 Data id: 59, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: x@x.com Data id: 60, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: xx@x.com, isPrimary: 1, isSuperPrimary: 0 Data id: 63, rawContactId: 31, contactId: 32, mimeType: vnd.android.cursor.item/group_membership, data1: 6 Data id: 64, rawContactId: 31, contactId: 32, mimeType: vnd.android.cursor.item/name, data1: Y, isPrimary: 1, isSuperPrimary: 0 Data id: 65, rawContactId: 31, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: y@y.com Data id: 66, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/email_v2, data1: yy@y.com, isPrimary: 1, isSuperPrimary: 0 What changed? Contact Y's row has been deleted and its column values have been merged into Contact X row. If the reverse occurred (Contact Y merged with Contact X), Contact Y's row would still be deleted. The difference is that Contact X's display name will be set to Contact Y's display name, which is done by the native Contacts app manually by setting Contact Y's Data name row to be the \"default\" (isPrimary and isSuperPrimary both set to 1). The AggregationExceptions table records the linked RawContacts's IDs in ascending order regardless of the order used in RAW_CONTACT_ID1 and RAW_CONTACT_ID2 at the time of merging. The RawContacts and Data table remains the same except the joined contactId column values have now been changed to the id of Contact X. All Data rows' isSuperPrimary value has been set to 0 though the isPrimary columns remain the same. In other words, this clears any \"default\" set before the link. These are done automatically by the Contacts Provider during the link operation. What is not done automatically by the Contacts Provider is that the name row of former Contact X is set as the default. The native Contacts app does this manually. The Contacts Providers automatically sets the Contact display name to whatever the default name row is for the Contact, if available. For more info on Contact display name resolution, read the Contact Display Name and Default Name Rows section. Note that display name resolution is different for APIs below 21 (pre-lollipop). The display name of the RawContacts remain the same. The Groups table remains unmodified. Options updates Changes to the options (starred, timesContacted, lastTimeContacted, customRingtone, and sendToVoicemail) of a RawContact may affect the options of the parent Contact. On the other hand, changes to the options of the parent Contact will be propagated to all child RawContact options. Photo updates A RawContact may have a full-sized photo saved as a file and a thumbnail version of that saved in the Data table in a photo mimetype row. A Contact's full-sized photo and thumbnail are simply references to the \"chosen\" RawContact's full-sized photo and thumbnail (though the URIs may differ). Note that when removing the photo in the native contacts app, the photo data row is not immediately deleted, though the PHOTO_FILE_ID is immediately set to null. This may result in the PHOTO_URI and PHOTO_THUMBNAIL_URI to still have a valid image uri even though the photo has been \"removed\". This library immediately deletes the photo data row, which seems to work perfectly. Data inserts In the native Contacts app, Data inserted in combined (raw) contacts mode will be associated to the first RawContact in the list sorted by the RawContact ID. This may not be the same as the RawContact referenced by ContactsColumns.NAME_RAW_CONTACT_ID . UI changes? The native Contacts App does not display the groups field when displaying / editing Contacts that have multiple RawContacts (linked/merged/joined) in combined mode. However, it does allow editing individual RawContact Data rows in which case the groups field is displayed and editable. In the native Contacts app, the name attribute used comes from the name row with IS_SUPER_PRIMARY set to true. This and all other \"unique\" mimetypes (organization) and non-unique mimetypes (email) per RawContact are shown only if they are not blank. Showing multiple RawContact's data in the same edit screen (combined mode) In older version of the native, Android Open Source Project (AOSP) Contacts app, data from multiple RawContacts was being shown in the same edit screen. This caused a lot of confusion about which data belonged to which RawContact. Newer versions of AOSP Contacts only allow editing one RawContact at a time to avoid confusion. Though, several RawContacts' data are still shown (not-editable) in the same screen.","title":"Behavior of linking/merging/joining contacts (AggregationExceptions)"},{"location":"other/link-unlink-contacts/#aggregationexceptions-table","text":"Given the following Contacts and their RawContacts; Contact A RawContact 1 Contact B RawContact 2 Contact C RawContact 3 Contact D RawContact 4 Linking one by one in this order; Contact B link Contact A Contact C link Contact D Contact C link Contact B Results in the following AggregationExceptions rows respectively; Aggregation exception id: 430, type: 1, rawContactId1: 1, rawContactId2: 2 Aggregation exception id: 430, type: 1, rawContactId1: 1, rawContactId2: 2 Aggregation exception id: 432, type: 1, rawContactId1: 3, rawContactId2: 4 Aggregation exception id: 436, type: 1, rawContactId1: 1, rawContactId2: 2 Aggregation exception id: 439, type: 1, rawContactId1: 1, rawContactId2: 3 Aggregation exception id: 442, type: 1, rawContactId1: 1, rawContactId2: 4 Aggregation exception id: 440, type: 1, rawContactId1: 2, rawContactId2: 3 Aggregation exception id: 443, type: 1, rawContactId1: 2, rawContactId2: 4 Aggregation exception id: 444, type: 1, rawContactId1: 3, rawContactId2: 4 There is a pattern here. RawContact ids are sorted in ascending order and linked from least to greatest exhaustively but no double links (1-2 is the same as 2-1). RawContact 1 has a row with RawContact 2, 3, and 4. RawContact 2 has a row with RawContact 3 and 4. RawContact 3 has a row with RawContact 4. Linking all in one go; Contact C link Contact A, B, D Results in the same AggregationExceptions rows. Unlinking results in the same AggregationExceptions rows except the type is 2 ( TYPE_KEEP_SEPARATE ).","title":"AggregationExceptions table"},{"location":"other/link-unlink-contacts/#contact-display-name-and-default-name-rows","text":"If available, the \"default\" (isPrimary and isSuperPrimary set to 1) name row for a Contact is automatically set as the Contact display name by the Contacts Provider. Otherwise, the Contacts Provider chooses from any of the other suitable data from the aggregate Contact. The ContactsColumns.NAME_RAW_CONTACT_ID is automatically updated by the Contacts Provider along with the display name. The default status of other sources (e.g. email) does not affect the Contact display name. The native Contacts app also sets the most recently updated name as the default at every update. This results in the Contact display name changing to the most recently updated name from one of the associated RawContacts. The \"most recently updated name\" is the name field that was last updated by the user when editing in the Contacts app, which is irrelevant to its value. It does not matter if the user deleted the last character of the name, added the same character back, and then saved. It still counts as the most recently updated. All of the above only applies to API 21 and above. Display name resolution is different for APIs below 21 (pre-Lollipop)! The ContactsColumns.NAME_RAW_CONTACT_ID was added in API 21. It changed the way display names are resolved for Contacts with more than one constituent RawContacts, which is what has been described so far. Before this change (APIs 20 and below), the native Contacts app is still able to set the Contact display name somehow. I'm not sure how. If someone figures it out, please let me know. I tried updating the Contact DISPLAY_NAME directly but it does not work. Setting a name row as default also does not affect the Contact DISPLAY_NAME .","title":"Contact Display Name and Default Name Rows"},{"location":"other/link-unlink-contacts/#effects-of-linkingunlinking-contacts","text":"When two or more Contacts (along with their constituent RawContacts) are linked into a single Contact those Contacts will be merged into one of the existing Contact row. The Contacts that have been merged into the single Contact will have their entries/rows in the Contacts table deleted. Unlinking will result in the original Contacts prior to linking to have new rows in the Contacts table with different IDs because the previously deleted row IDs cannot be reused. Getting Contacts that have been linked into a single Contact or Contacts whose row IDs have change after unlinking is still possible using the Contact lookup key. For more info, read about Contact lookup key vs ID .","title":"Effects of linking/unlinking contacts"},{"location":"permissions/permissions-handling-coroutines/","text":"Permissions handling using coroutines \u00b6 This library provides extensions in the permissions module that allow you to prompt users for required permissions before executing a core API function. These extensions use Kotlin Coroutines . For all core API functions that requires certain permissions to be granted (e.g. query, insert, update, and deletes), there is a corresponding xxxWithPermission extension function. Using withPermission extensions \u00b6 To perform an query, insert, update, and delete with permission , launch { val contactsApi = Contacts ( context ) val query = contactsApi . queryWithPermission () val insert = contactsApi . insertWithPermission () val update = contactsApi . updateWithPermission () val delete = contactsApi . deleteWithPermission () } For each invocation of xxxWithPermission , if the required permission(s) are not yet granted, the current coroutine is suspended, user is prompted to grant permissions, and then an operation instance is returned (which may then be executed to get a result). If permission(s) are already granted, then an operation instance is returned immediately without suspending the coroutine and prompting the user for permission. If permission(s) are not granted, then the operation will immediately fail and the result you get is incorrect (usually null or empty when it should not be). Prior to Android 6.0 Marshmallow (API level 23), users are NOT prompted for permission at runtime because users must already grant all permissions prior to app install. Not compatible with Java \u00b6 Unlike the core module, the permissions module is not compatible with Java because it requires Kotlin Coroutines. These extensions are optional \u00b6 You are free to use the core APIs however you want with whatever libraries or frameworks you want that works with Java or use your own DIY solution.","title":"Permissions handling using coroutines"},{"location":"permissions/permissions-handling-coroutines/#permissions-handling-using-coroutines","text":"This library provides extensions in the permissions module that allow you to prompt users for required permissions before executing a core API function. These extensions use Kotlin Coroutines . For all core API functions that requires certain permissions to be granted (e.g. query, insert, update, and deletes), there is a corresponding xxxWithPermission extension function.","title":"Permissions handling using coroutines"},{"location":"permissions/permissions-handling-coroutines/#using-withpermission-extensions","text":"To perform an query, insert, update, and delete with permission , launch { val contactsApi = Contacts ( context ) val query = contactsApi . queryWithPermission () val insert = contactsApi . insertWithPermission () val update = contactsApi . updateWithPermission () val delete = contactsApi . deleteWithPermission () } For each invocation of xxxWithPermission , if the required permission(s) are not yet granted, the current coroutine is suspended, user is prompted to grant permissions, and then an operation instance is returned (which may then be executed to get a result). If permission(s) are already granted, then an operation instance is returned immediately without suspending the coroutine and prompting the user for permission. If permission(s) are not granted, then the operation will immediately fail and the result you get is incorrect (usually null or empty when it should not be). Prior to Android 6.0 Marshmallow (API level 23), users are NOT prompted for permission at runtime because users must already grant all permissions prior to app install.","title":"Using withPermission extensions"},{"location":"permissions/permissions-handling-coroutines/#not-compatible-with-java","text":"Unlike the core module, the permissions module is not compatible with Java because it requires Kotlin Coroutines.","title":"Not compatible with Java"},{"location":"permissions/permissions-handling-coroutines/#these-extensions-are-optional","text":"You are free to use the core APIs however you want with whatever libraries or frameworks you want that works with Java or use your own DIY solution.","title":"These extensions are optional"},{"location":"profile/delete-profile/","text":"Delete device owner Contact profile \u00b6 This library provides the ProfileDelete API, which allows you to delete the device owner Profile Contact or only some of its constituent RawContacts. Note that there can be only one device owner Contact, which is either set (not null) or not yet set (null). However, like other regular Contacts, the Profile Contact may have one or more RawContacts. An instance of the ProfileDelete API is obtained by, val delete = Contacts ( context ). profile (). delete () If you want to delete non-Profile Contacts, read Delete Contacts A basic delete \u00b6 To delete a the profile Contact (if it exist) and all of its RawContacts, val deleteResult = delete . contact () . commit () If you want to delete a set of RawContacts belonging to the profile Contact, val deleteResult = delete . rawContacts ( contactToDelete ) . commit () Note that the profile Contact is deleted automatically when all constituent RawContacts are deleted. Executing the delete \u00b6 To execute the delete, . commit () If you want to delete all given RawContacts in a single atomic transaction, . commitInOneTransaction () The call to commitInOneTransaction will only succeed if ALL given RawContacts are successfully deleted. If one delete fails, the entire operation will fail and everything will be reverted prior to the delete operation. In contrast, commit allows for some deletes to succeed and some to fail. This really only applies to when only rawContacts are specified. Performing the delete and result processing asynchronously \u00b6 Deletes are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the delete with permission \u00b6 Deletes require the android.permission.WRITE_CONTACTS permissions. If not granted, the delete will do nothing and return a failed result. For API 22 and below, the permission \"android.permission.WRITE_PROFILE\" is also required but only at the manifest level. Prior to API 23 (Marshmallow), permissions needed to be granted prior to installation instead of at runtime. To perform the delete with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Custom data support \u00b6 The ProfileDelete API supports custom data. For more info, read Delete custom data .","title":"Delete device owner Contact profile"},{"location":"profile/delete-profile/#delete-device-owner-contact-profile","text":"This library provides the ProfileDelete API, which allows you to delete the device owner Profile Contact or only some of its constituent RawContacts. Note that there can be only one device owner Contact, which is either set (not null) or not yet set (null). However, like other regular Contacts, the Profile Contact may have one or more RawContacts. An instance of the ProfileDelete API is obtained by, val delete = Contacts ( context ). profile (). delete () If you want to delete non-Profile Contacts, read Delete Contacts","title":"Delete device owner Contact profile"},{"location":"profile/delete-profile/#a-basic-delete","text":"To delete a the profile Contact (if it exist) and all of its RawContacts, val deleteResult = delete . contact () . commit () If you want to delete a set of RawContacts belonging to the profile Contact, val deleteResult = delete . rawContacts ( contactToDelete ) . commit () Note that the profile Contact is deleted automatically when all constituent RawContacts are deleted.","title":"A basic delete"},{"location":"profile/delete-profile/#executing-the-delete","text":"To execute the delete, . commit () If you want to delete all given RawContacts in a single atomic transaction, . commitInOneTransaction () The call to commitInOneTransaction will only succeed if ALL given RawContacts are successfully deleted. If one delete fails, the entire operation will fail and everything will be reverted prior to the delete operation. In contrast, commit allows for some deletes to succeed and some to fail. This really only applies to when only rawContacts are specified.","title":"Executing the delete"},{"location":"profile/delete-profile/#performing-the-delete-and-result-processing-asynchronously","text":"Deletes are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the delete and result processing asynchronously"},{"location":"profile/delete-profile/#performing-the-delete-with-permission","text":"Deletes require the android.permission.WRITE_CONTACTS permissions. If not granted, the delete will do nothing and return a failed result. For API 22 and below, the permission \"android.permission.WRITE_PROFILE\" is also required but only at the manifest level. Prior to API 23 (Marshmallow), permissions needed to be granted prior to installation instead of at runtime. To perform the delete with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the delete with permission"},{"location":"profile/delete-profile/#custom-data-support","text":"The ProfileDelete API supports custom data. For more info, read Delete custom data .","title":"Custom data support"},{"location":"profile/insert-profile/","text":"Insert the device owner Contact profile \u00b6 This library provides the ProfileInsert API that allows you to insert one or more RawContacts and Data. Note that there can be only one device owner Contact, which is either set (not null) or not yet set (null). However, like other regular Contacts, the Profile Contact may have one or more RawContacts. An instance of the ProfileInsert API is obtained by, val insert = Contacts ( context ). profile (). insert () If you want to create/insert non-Profile Contacts, read Insert contacts . A basic insert \u00b6 To create/insert a raw contact with a name of \"John Doe\" who works at Amazon with a work email of \"john.doe@amazon.com\" (in Kotlin), val insertResult = Contacts ( context ) . profile () . insert () . rawContacts ( NewRawContact (). apply { name = NewName (). apply { givenName = \"John\" familyName = \"Doe\" } organization = NewOrganization (). apply { company = \"Amazon\" title = \"Superstar\" } emails . add ( NewEmail (). apply { address = \"john.doe@amazon.com\" type = EmailEntity . Type . WORK }) }) . commit () Or alternatively, in a more Kotlinized style using named arguments, val insertResult = Contacts ( context ) . profile () . insert () . rawContacts ( NewRawContact ( name = NewName ( givenName = \"John\" , familyName = \"Doe\" ), organization = NewOrganization ( company = \"Amazon\" , title = \"Superstar\" ), emails = mutableListOf ( NewEmail ( address = \"john.doe@amazon.com\" , type = EmailEntity . Type . WORK )) )) . commit () Or alternatively, using extension functions, val insertResult = Contacts ( context ) . profile () . insert () . rawContact { setName { givenName = \"John\" familyName = \"Doe\" } setOrganization { company = \"Amazon\" title = \"Superstar\" } addEmail { address = \"john.doe@amazon.com\" type = EmailEntity . Type . WORK } } . commit () Allowing blanks \u00b6 The API allows you to specify if you want to be able to insert blank contacts or not, . allowBlanks ( true | false ) For more info, read about Blank contacts . Blank data are not inserted \u00b6 Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are ignored and are not inserted by insert APIs. For more info, read about Blank data . Allowing multiple RawContacts per Account \u00b6 The API allows you to insert a profile RawContact with an Account that already has a profile RawContact, . allowMultipleRawContactsPerAccount ( true | false ) According to the ContactsContract.Profile documentation; ... each account (including data set, if applicable) on the device may contribute a single raw contact representing the user's personal profile data from that source. In other words, one account can have one profile RawContact. However, despite the documentation of \"one profile RawContact per one Account\", the Contacts Provider allows for multiple RawContacts per Account, including multiple local RawContacts (no Account). Associating an Account \u00b6 New RawContacts can be associated with an Account in order to enable syncing, . forAccount ( account ) For example, to associated the new RawContact to an account, . forAccount ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) For more info, read Query for Accounts . Local RawContacts \u00b6 If no Account is provided, or null is provided, or if an incorrect account is provided, the RawContacts inserted will not be associated with an Account. RawContacts inserted without an associated account are considered local or device-only contacts, which are not synced. For more info, read Sync contact data across devices . There are also certain data kinds that are ignored on insert or update if the RawContact is local. For more info, read about Local (device-only) contacts . Including only specific data \u00b6 To include only the given set of fields (data) in each of the insert operation, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations . Executing the insert \u00b6 To execute the insert, . commit () Handling the insert result \u00b6 The commit function returns a Result , val contactsApi = Contacts ( context ) val newRawContact = NewRawContact (...) val insertResult = contactsApi . profile () . insert () . rawContact ( newRawContact ) . commit () To check if the insert succeeded, val insertSucess = insertResult . isSuccessful To get the RawContact IDs of the newly created RawContact, val rawContactId = insertResult . rawContactId Once you have the RawContact ID, you can retrieve the newly created Contact via the Query API, val contacts = contactsApi . query () . where { RawContact . Id equalTo rawContactId } . find () For more info, read Query contacts (advanced) . Alternatively, you may use the extensions provided in ProfileInsertResult . To get the newly created Contact, val contact = insertResult . contact ( contactsApi ) To instead get the RawContact directly, val rawContacts = insertResult . rawContact ( contactsApi ) Cancelling the insert \u00b6 To cancel an insert amid execution, . commit { returnTrueIfInsertShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel insert processing as soon as possible. The function is called numerous times during insert processing to check if processing should stop or continue. This gives you the option to cancel the insert. For example, to automatically cancel the insert inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val insertResult = insert . commit { ! isActive } } } Performing the insert and result processing asynchronously \u00b6 Inserts are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the insert with permission \u00b6 Inserts require the android.permission.WRITE_CONTACTS and android.permission.GET_ACCOUNTS permissions. If not granted, the insert will do nothing and return a failed result. For API 22 and below, the permission \"android.permission.WRITE_PROFILE\" is also required but only at the manifest level. Prior to API 23 (Marshmallow), permissions needed to be granted prior to installation instead of at runtime. To perform the insert with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Custom data support \u00b6 The ProfileInsert API supports custom data. For more info, read Insert custom data into new or existing contacts . RawContact and Contact aggregation \u00b6 As per documentation in android.provider.ContactsContract.Profile , The user's profile entry cannot be created explicitly (attempting to do so will throw an exception). When a raw contact is inserted into the profile, the provider will check for the existence of a profile on the device. If one is found, the raw contact's RawContacts.CONTACT_ID column gets the _ID of the profile Contact. If no match is found, the profile Contact is created and its _ID is put into the RawContacts.CONTACT_ID column of the newly inserted raw contact.","title":"Insert device owner Contact profile"},{"location":"profile/insert-profile/#insert-the-device-owner-contact-profile","text":"This library provides the ProfileInsert API that allows you to insert one or more RawContacts and Data. Note that there can be only one device owner Contact, which is either set (not null) or not yet set (null). However, like other regular Contacts, the Profile Contact may have one or more RawContacts. An instance of the ProfileInsert API is obtained by, val insert = Contacts ( context ). profile (). insert () If you want to create/insert non-Profile Contacts, read Insert contacts .","title":"Insert the device owner Contact profile"},{"location":"profile/insert-profile/#a-basic-insert","text":"To create/insert a raw contact with a name of \"John Doe\" who works at Amazon with a work email of \"john.doe@amazon.com\" (in Kotlin), val insertResult = Contacts ( context ) . profile () . insert () . rawContacts ( NewRawContact (). apply { name = NewName (). apply { givenName = \"John\" familyName = \"Doe\" } organization = NewOrganization (). apply { company = \"Amazon\" title = \"Superstar\" } emails . add ( NewEmail (). apply { address = \"john.doe@amazon.com\" type = EmailEntity . Type . WORK }) }) . commit () Or alternatively, in a more Kotlinized style using named arguments, val insertResult = Contacts ( context ) . profile () . insert () . rawContacts ( NewRawContact ( name = NewName ( givenName = \"John\" , familyName = \"Doe\" ), organization = NewOrganization ( company = \"Amazon\" , title = \"Superstar\" ), emails = mutableListOf ( NewEmail ( address = \"john.doe@amazon.com\" , type = EmailEntity . Type . WORK )) )) . commit () Or alternatively, using extension functions, val insertResult = Contacts ( context ) . profile () . insert () . rawContact { setName { givenName = \"John\" familyName = \"Doe\" } setOrganization { company = \"Amazon\" title = \"Superstar\" } addEmail { address = \"john.doe@amazon.com\" type = EmailEntity . Type . WORK } } . commit ()","title":"A basic insert"},{"location":"profile/insert-profile/#allowing-blanks","text":"The API allows you to specify if you want to be able to insert blank contacts or not, . allowBlanks ( true | false ) For more info, read about Blank contacts .","title":"Allowing blanks"},{"location":"profile/insert-profile/#blank-data-are-not-inserted","text":"Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are ignored and are not inserted by insert APIs. For more info, read about Blank data .","title":"Blank data are not inserted"},{"location":"profile/insert-profile/#allowing-multiple-rawcontacts-per-account","text":"The API allows you to insert a profile RawContact with an Account that already has a profile RawContact, . allowMultipleRawContactsPerAccount ( true | false ) According to the ContactsContract.Profile documentation; ... each account (including data set, if applicable) on the device may contribute a single raw contact representing the user's personal profile data from that source. In other words, one account can have one profile RawContact. However, despite the documentation of \"one profile RawContact per one Account\", the Contacts Provider allows for multiple RawContacts per Account, including multiple local RawContacts (no Account).","title":"Allowing multiple RawContacts per Account"},{"location":"profile/insert-profile/#associating-an-account","text":"New RawContacts can be associated with an Account in order to enable syncing, . forAccount ( account ) For example, to associated the new RawContact to an account, . forAccount ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) For more info, read Query for Accounts .","title":"Associating an Account"},{"location":"profile/insert-profile/#local-rawcontacts","text":"If no Account is provided, or null is provided, or if an incorrect account is provided, the RawContacts inserted will not be associated with an Account. RawContacts inserted without an associated account are considered local or device-only contacts, which are not synced. For more info, read Sync contact data across devices . There are also certain data kinds that are ignored on insert or update if the RawContact is local. For more info, read about Local (device-only) contacts .","title":"Local RawContacts"},{"location":"profile/insert-profile/#including-only-specific-data","text":"To include only the given set of fields (data) in each of the insert operation, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations .","title":"Including only specific data"},{"location":"profile/insert-profile/#executing-the-insert","text":"To execute the insert, . commit ()","title":"Executing the insert"},{"location":"profile/insert-profile/#handling-the-insert-result","text":"The commit function returns a Result , val contactsApi = Contacts ( context ) val newRawContact = NewRawContact (...) val insertResult = contactsApi . profile () . insert () . rawContact ( newRawContact ) . commit () To check if the insert succeeded, val insertSucess = insertResult . isSuccessful To get the RawContact IDs of the newly created RawContact, val rawContactId = insertResult . rawContactId Once you have the RawContact ID, you can retrieve the newly created Contact via the Query API, val contacts = contactsApi . query () . where { RawContact . Id equalTo rawContactId } . find () For more info, read Query contacts (advanced) . Alternatively, you may use the extensions provided in ProfileInsertResult . To get the newly created Contact, val contact = insertResult . contact ( contactsApi ) To instead get the RawContact directly, val rawContacts = insertResult . rawContact ( contactsApi )","title":"Handling the insert result"},{"location":"profile/insert-profile/#cancelling-the-insert","text":"To cancel an insert amid execution, . commit { returnTrueIfInsertShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel insert processing as soon as possible. The function is called numerous times during insert processing to check if processing should stop or continue. This gives you the option to cancel the insert. For example, to automatically cancel the insert inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val insertResult = insert . commit { ! isActive } } }","title":"Cancelling the insert"},{"location":"profile/insert-profile/#performing-the-insert-and-result-processing-asynchronously","text":"Inserts are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the insert and result processing asynchronously"},{"location":"profile/insert-profile/#performing-the-insert-with-permission","text":"Inserts require the android.permission.WRITE_CONTACTS and android.permission.GET_ACCOUNTS permissions. If not granted, the insert will do nothing and return a failed result. For API 22 and below, the permission \"android.permission.WRITE_PROFILE\" is also required but only at the manifest level. Prior to API 23 (Marshmallow), permissions needed to be granted prior to installation instead of at runtime. To perform the insert with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the insert with permission"},{"location":"profile/insert-profile/#custom-data-support","text":"The ProfileInsert API supports custom data. For more info, read Insert custom data into new or existing contacts .","title":"Custom data support"},{"location":"profile/insert-profile/#rawcontact-and-contact-aggregation","text":"As per documentation in android.provider.ContactsContract.Profile , The user's profile entry cannot be created explicitly (attempting to do so will throw an exception). When a raw contact is inserted into the profile, the provider will check for the existence of a profile on the device. If one is found, the raw contact's RawContacts.CONTACT_ID column gets the _ID of the profile Contact. If no match is found, the profile Contact is created and its _ID is put into the RawContacts.CONTACT_ID column of the newly inserted raw contact.","title":"RawContact and Contact aggregation"},{"location":"profile/query-profile/","text":"Query device owner Contact profile \u00b6 This library provides the ProfileQuery API that allows you to get the device owner Profile Contact. Note that there can be only one device owner Contact, which is either set (not null) or not yet set (null). However, like other regular Contacts, the Profile Contact may have one or more RawContacts. An instance of the ProfileQuery API is obtained by, val query = Contacts ( context ). profile (). query () If you want to get non-Profile Contacts, read Query contacts and Query contacts (advanced) . A basic query \u00b6 To get the profile Contact, val profileContact = Contacts ( context ). profile (). query (). find (). contact Including blank (raw) contacts \u00b6 The API allows you to specify if you want to include blank (raw) contacts or not, . includeBlanks ( true | false ) For more info, read about Blank contacts . Specifying Accounts \u00b6 To only include RawContacts associated with one of the given accounts, . accounts ( accounts ) For example, to include only RawContacts belonging to only one account, . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) For more info, read Query for Accounts . The RawContacts returned will only belong to the specified accounts. If no accounts are specified (this function is not called or called with no Accounts), then all RawContacts are included in the returned Contact. A null Account may be provided here, which results in RawContacts with no associated Account to be included. RawContacts without an associated account are considered local contacts or device-only contacts, which are not synced. For more info, read about Local (device-only) contacts . Note that this may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it. Including only specific data \u00b6 To include only the given set of fields (data) in each of the Profile Contact, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations . Cancelling the query \u00b6 To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val profile = query . find { ! isActive } } } Performing the query asynchronously \u00b6 Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes. Performing the query with permission \u00b6 Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return null. For API 22 and below, the permission \"android.permission.READ_PROFILE\" is also required but only at the manifest level. Prior to API 23 (Marshmallow), permissions needed to be granted prior to installation instead of at runtime. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Custom data support \u00b6 The ProfilQuery API supports custom data. For more info, read Query custom data .","title":"Query device owner Contact profile"},{"location":"profile/query-profile/#query-device-owner-contact-profile","text":"This library provides the ProfileQuery API that allows you to get the device owner Profile Contact. Note that there can be only one device owner Contact, which is either set (not null) or not yet set (null). However, like other regular Contacts, the Profile Contact may have one or more RawContacts. An instance of the ProfileQuery API is obtained by, val query = Contacts ( context ). profile (). query () If you want to get non-Profile Contacts, read Query contacts and Query contacts (advanced) .","title":"Query device owner Contact profile"},{"location":"profile/query-profile/#a-basic-query","text":"To get the profile Contact, val profileContact = Contacts ( context ). profile (). query (). find (). contact","title":"A basic query"},{"location":"profile/query-profile/#including-blank-raw-contacts","text":"The API allows you to specify if you want to include blank (raw) contacts or not, . includeBlanks ( true | false ) For more info, read about Blank contacts .","title":"Including blank (raw) contacts"},{"location":"profile/query-profile/#specifying-accounts","text":"To only include RawContacts associated with one of the given accounts, . accounts ( accounts ) For example, to include only RawContacts belonging to only one account, . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) For more info, read Query for Accounts . The RawContacts returned will only belong to the specified accounts. If no accounts are specified (this function is not called or called with no Accounts), then all RawContacts are included in the returned Contact. A null Account may be provided here, which results in RawContacts with no associated Account to be included. RawContacts without an associated account are considered local contacts or device-only contacts, which are not synced. For more info, read about Local (device-only) contacts . Note that this may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it.","title":"Specifying Accounts"},{"location":"profile/query-profile/#including-only-specific-data","text":"To include only the given set of fields (data) in each of the Profile Contact, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations .","title":"Including only specific data"},{"location":"profile/query-profile/#cancelling-the-query","text":"To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val profile = query . find { ! isActive } } }","title":"Cancelling the query"},{"location":"profile/query-profile/#performing-the-query-asynchronously","text":"Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes.","title":"Performing the query asynchronously"},{"location":"profile/query-profile/#performing-the-query-with-permission","text":"Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return null. For API 22 and below, the permission \"android.permission.READ_PROFILE\" is also required but only at the manifest level. Prior to API 23 (Marshmallow), permissions needed to be granted prior to installation instead of at runtime. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the query with permission"},{"location":"profile/query-profile/#custom-data-support","text":"The ProfilQuery API supports custom data. For more info, read Query custom data .","title":"Custom data support"},{"location":"profile/update-profile/","text":"Update device owner Contact profile \u00b6 This library provides the ProfileUpdate API that allows you to update the device owner Profile Contact. Note that there can be only one device owner Contact, which is either set (not null) or not yet set (null). However, like other regular Contacts, the Profile Contact may have one or more RawContacts. An instance of the ProfileUpdate API is obtained by, val update = Contacts ( context ). profile (). update () If you want to update non-Profile Contacts, read Update contacts . A basic update \u00b6 To update the profile Contact and all of its RawContacts, val updateResult = Contacts ( context ) . profile () . update () . contact ( profile . mutableCopy { // make changes }) . commit () To update a profile RawContact directly, val updateResult = Contacts ( context ) . update () . rawContacts ( profile . rawContacts . first (). mutableCopy { // make changes }) . commit () Deleting blanks \u00b6 The API allows you to specify if you want the update operation to delete blank RawContacts or not, . deleteBlanks ( true | false ) For more info, read about Blank contacts . Blank data are deleted \u00b6 Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are deleted by update APIs. For more info, read about Blank data . Including only specific data \u00b6 To include only the given set of fields (data) in each of the update operation, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations . Executing the update \u00b6 To execute the update, . commit () Handling the update result \u00b6 The commit function returns a Result , val contactsApi = Contacts ( context ) val mutableProfile = profile . mutableCopy { ... } val updateResult = contactsApi . profile () . update () . contact ( mutableProfile ) . commit () To check if all updates succeeded, val allUpdatesSuccessful = updateResult . isSuccessful To check if a particular update succeeded, val firstUpdateSuccessful = updateResult . isSuccessful ( mutableProfile . rawContacts . first ()) Once you have performed the updates, you can retrieve the updated profile Contact reference via the Query API, val updatedProfile = Contacts ( context ). profile (). query (). find () For more info, read Query device owner Contact profile . Alternatively, you may use the extensions provided in ContactRefresh and RawContactRefresh . To get the updated profile Contact and all of its RawContacts and Data, val updatedProfile = profile . refresh ( contactsApi ) To get an updated RawContact and Data, val updatedProfileRawContact = profile . rawContacts . first (). refresh ( contactsApi ) Cancelling the update \u00b6 To cancel an update amid execution, . commit { returnTrueIfUpdateShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel update processing as soon as possible. The function is called numerous times during update processing to check if processing should stop or continue. This gives you the option to cancel the update. For example, to automatically cancel the update inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val updateResult = update . commit { ! isActive } } } Performing the update and result processing asynchronously \u00b6 Updates are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the update with permission \u00b6 Updates require the android.permission.WRITE_CONTACTS permissions. If not granted, the update will do nothing and return a failed result. For API 22 and below, the permission \"android.permission.WRITE_PROFILE\" is also required but only at the manifest level. Prior to API 23 (Marshmallow), permissions needed to be granted prior to installation instead of at runtime. To perform the update with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Custom data support \u00b6 The ProfileUpdate API supports custom data. For more info, read Update custom data . Modifiable Contact fields \u00b6 As per documentation in android.provider.ContactsContract.Profile , The profile Contact has the same update restrictions as Contacts in general... Only certain columns of Contact are modifiable: STARRED, CUSTOM_RINGTONE, SEND_TO_VOICEMAIL. Changing any of these columns on the Contact also changes them on all constituent raw contacts.","title":"Update device owner Contact profile"},{"location":"profile/update-profile/#update-device-owner-contact-profile","text":"This library provides the ProfileUpdate API that allows you to update the device owner Profile Contact. Note that there can be only one device owner Contact, which is either set (not null) or not yet set (null). However, like other regular Contacts, the Profile Contact may have one or more RawContacts. An instance of the ProfileUpdate API is obtained by, val update = Contacts ( context ). profile (). update () If you want to update non-Profile Contacts, read Update contacts .","title":"Update device owner Contact profile"},{"location":"profile/update-profile/#a-basic-update","text":"To update the profile Contact and all of its RawContacts, val updateResult = Contacts ( context ) . profile () . update () . contact ( profile . mutableCopy { // make changes }) . commit () To update a profile RawContact directly, val updateResult = Contacts ( context ) . update () . rawContacts ( profile . rawContacts . first (). mutableCopy { // make changes }) . commit ()","title":"A basic update"},{"location":"profile/update-profile/#deleting-blanks","text":"The API allows you to specify if you want the update operation to delete blank RawContacts or not, . deleteBlanks ( true | false ) For more info, read about Blank contacts .","title":"Deleting blanks"},{"location":"profile/update-profile/#blank-data-are-deleted","text":"Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are deleted by update APIs. For more info, read about Blank data .","title":"Blank data are deleted"},{"location":"profile/update-profile/#including-only-specific-data","text":"To include only the given set of fields (data) in each of the update operation, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations .","title":"Including only specific data"},{"location":"profile/update-profile/#executing-the-update","text":"To execute the update, . commit ()","title":"Executing the update"},{"location":"profile/update-profile/#handling-the-update-result","text":"The commit function returns a Result , val contactsApi = Contacts ( context ) val mutableProfile = profile . mutableCopy { ... } val updateResult = contactsApi . profile () . update () . contact ( mutableProfile ) . commit () To check if all updates succeeded, val allUpdatesSuccessful = updateResult . isSuccessful To check if a particular update succeeded, val firstUpdateSuccessful = updateResult . isSuccessful ( mutableProfile . rawContacts . first ()) Once you have performed the updates, you can retrieve the updated profile Contact reference via the Query API, val updatedProfile = Contacts ( context ). profile (). query (). find () For more info, read Query device owner Contact profile . Alternatively, you may use the extensions provided in ContactRefresh and RawContactRefresh . To get the updated profile Contact and all of its RawContacts and Data, val updatedProfile = profile . refresh ( contactsApi ) To get an updated RawContact and Data, val updatedProfileRawContact = profile . rawContacts . first (). refresh ( contactsApi )","title":"Handling the update result"},{"location":"profile/update-profile/#cancelling-the-update","text":"To cancel an update amid execution, . commit { returnTrueIfUpdateShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel update processing as soon as possible. The function is called numerous times during update processing to check if processing should stop or continue. This gives you the option to cancel the update. For example, to automatically cancel the update inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val updateResult = update . commit { ! isActive } } }","title":"Cancelling the update"},{"location":"profile/update-profile/#performing-the-update-and-result-processing-asynchronously","text":"Updates are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the update and result processing asynchronously"},{"location":"profile/update-profile/#performing-the-update-with-permission","text":"Updates require the android.permission.WRITE_CONTACTS permissions. If not granted, the update will do nothing and return a failed result. For API 22 and below, the permission \"android.permission.WRITE_PROFILE\" is also required but only at the manifest level. Prior to API 23 (Marshmallow), permissions needed to be granted prior to installation instead of at runtime. To perform the update with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the update with permission"},{"location":"profile/update-profile/#custom-data-support","text":"The ProfileUpdate API supports custom data. For more info, read Update custom data .","title":"Custom data support"},{"location":"profile/update-profile/#modifiable-contact-fields","text":"As per documentation in android.provider.ContactsContract.Profile , The profile Contact has the same update restrictions as Contacts in general... Only certain columns of Contact are modifiable: STARRED, CUSTOM_RINGTONE, SEND_TO_VOICEMAIL. Changing any of these columns on the Contact also changes them on all constituent raw contacts.","title":"Modifiable Contact fields"},{"location":"setup/installation/","text":"Installation guide \u00b6 This library is a multi-module project published with JitPack First, include JitPack in the repositories list, repositories { maven { url \"https://jitpack.io\" } } To install individual modules, dependencies { implementation 'com.github.vestrel00.contacts-android:core:' implementation 'com.github.vestrel00.contacts-android:async:' implementation 'com.github.vestrel00.contacts-android:customdata-gender:' implementation 'com.github.vestrel00.contacts-android:customdata-googlecontacts:' implementation 'com.github.vestrel00.contacts-android:customdata-handlename:' implementation 'com.github.vestrel00.contacts-android:customdata-pokemon:' implementation 'com.github.vestrel00.contacts-android:customdata-rpg:' implementation 'com.github.vestrel00.contacts-android:debug:' implementation 'com.github.vestrel00.contacts-android:permissions:' implementation 'com.github.vestrel00.contacts-android:test:' implementation 'com.github.vestrel00.contacts-android:ui:' // Notice that when installing individual modules, the first \":\" comes after \"contacts-android\". } The core module is really all you need. All other modules are optional. It is recommended that you install individual modules to make sure that unused code is not included in your application, which will increase your app's APK size. If you still want to install all modules in a single line, read the Installing all modules in one line section below. Modules \u00b6 Here is a brief description of the individual modules you can install. core : All of the contacts management APIs the library has to offer. This is the only required module. All other modules are optional . async : Extension functions for executing core API functions asynchronously using Kotlin Coroutines . permissions : Extension functions for executing core API functions with permissions granted using Kotlin Coroutines . test : APIs for mocking core APIs during tests or at production runtime. debug : Extension functions for logging internal database tables into the Logcat and other debugging related stuff. This is only meant for development use . ui : Rudimentary UI views and functions that are already integrated with the core APIs. You may use these for rapid prototyping or just for reference . customdata-gender : Custom data for gender . customdata-googlecontacts : Custom data managed by the Google Contacts app . customdata-handlename : Custom data for handle . customdata-pokemon : Custom data for pokemon . customdata-rpg : Custom data for role playing games (RPG) . Installing all modules in one line \u00b6 To install all modules in a single line, dependencies { implementation 'com.github.vestrel00:contacts-android:' // Notice that when installing all modules, the first \":\" comes after \"vestrel00\". } Starting with version 0.2.0 , installing all modules in a single line is only supported when using the dependencyResolutionManagement in settings.gradle . In your settings.gradle , dependencyResolutionManagement { repositoriesMode . set ( RepositoriesMode . FAIL_ON_PROJECT_REPOS ) repositories { maven { url \"https://jitpack.io\" } } } For versions 0.1.10 and below, you can still install all modules in a single line using the old common method of dependency resolution. In your root build.gradle , allprojects { repositories { maven { url \"https://jitpack.io\" } } }","title":"Installation"},{"location":"setup/installation/#installation-guide","text":"This library is a multi-module project published with JitPack First, include JitPack in the repositories list, repositories { maven { url \"https://jitpack.io\" } } To install individual modules, dependencies { implementation 'com.github.vestrel00.contacts-android:core:' implementation 'com.github.vestrel00.contacts-android:async:' implementation 'com.github.vestrel00.contacts-android:customdata-gender:' implementation 'com.github.vestrel00.contacts-android:customdata-googlecontacts:' implementation 'com.github.vestrel00.contacts-android:customdata-handlename:' implementation 'com.github.vestrel00.contacts-android:customdata-pokemon:' implementation 'com.github.vestrel00.contacts-android:customdata-rpg:' implementation 'com.github.vestrel00.contacts-android:debug:' implementation 'com.github.vestrel00.contacts-android:permissions:' implementation 'com.github.vestrel00.contacts-android:test:' implementation 'com.github.vestrel00.contacts-android:ui:' // Notice that when installing individual modules, the first \":\" comes after \"contacts-android\". } The core module is really all you need. All other modules are optional. It is recommended that you install individual modules to make sure that unused code is not included in your application, which will increase your app's APK size. If you still want to install all modules in a single line, read the Installing all modules in one line section below.","title":"Installation guide"},{"location":"setup/installation/#modules","text":"Here is a brief description of the individual modules you can install. core : All of the contacts management APIs the library has to offer. This is the only required module. All other modules are optional . async : Extension functions for executing core API functions asynchronously using Kotlin Coroutines . permissions : Extension functions for executing core API functions with permissions granted using Kotlin Coroutines . test : APIs for mocking core APIs during tests or at production runtime. debug : Extension functions for logging internal database tables into the Logcat and other debugging related stuff. This is only meant for development use . ui : Rudimentary UI views and functions that are already integrated with the core APIs. You may use these for rapid prototyping or just for reference . customdata-gender : Custom data for gender . customdata-googlecontacts : Custom data managed by the Google Contacts app . customdata-handlename : Custom data for handle . customdata-pokemon : Custom data for pokemon . customdata-rpg : Custom data for role playing games (RPG) .","title":"Modules"},{"location":"setup/installation/#installing-all-modules-in-one-line","text":"To install all modules in a single line, dependencies { implementation 'com.github.vestrel00:contacts-android:' // Notice that when installing all modules, the first \":\" comes after \"vestrel00\". } Starting with version 0.2.0 , installing all modules in a single line is only supported when using the dependencyResolutionManagement in settings.gradle . In your settings.gradle , dependencyResolutionManagement { repositoriesMode . set ( RepositoriesMode . FAIL_ON_PROJECT_REPOS ) repositories { maven { url \"https://jitpack.io\" } } } For versions 0.1.10 and below, you can still install all modules in a single line using the old common method of dependency resolution. In your root build.gradle , allprojects { repositories { maven { url \"https://jitpack.io\" } } }","title":"Installing all modules in one line"},{"location":"setup/setup-contacts-api/","text":"Contacts API Setup \u00b6 The main library functions are all accessible via the contacts.core.Contacts API. There's no setup required. Just create an instance of Contacts and the world of contacts is at your disposal =) In Kotlin, Contacts ( context ) in Java, ContactsFactory . create ( context ); The context parameter can come from anywhere; Application, Activity, Fragment, or View. It does not matter what context you pass in. The API will only use and store the Application context, to avoid leaks :D It's up to you if you just want to create instances on demand. Or, hold on to instances as a singleton that is injected to your dependency graph (via something like dagger , hilt , or koin ), which will make black box testing and white box testing a walk in the park! Logging support \u00b6 Instances of Contacts hold on to a Logger for logging support. For more info, read Log API input and output . Custom data integration \u00b6 Instances of Contacts hold on to an instance of CustomDataRegistry for custom data integration. For more info, read Integrate custom data . Optional, but recommended setup \u00b6 It is recommended to use a single instance of the Contacts API throughout your application using dependency injection . This will allow you to; Use the same Contacts API instance throughout your app. This especially important when integrating custom data. Easily substitute your Contacts API instance with an instance of TestContacts . This is useful in black box testing (UI instrumentation tests; androidTest/ ). It may also be used in your production apps \"test mode\". Easily substitute your Contacts API instance with an instance of MockContacts This is useful in white box testing (unit & integration tests; test/ ). For more info, read Contacts API Testing . Of course, this library does not (and will not) force you to do things you don't want. If you don't care about all of the above and just want to get out a quick prototype of a feature in your app or an entire app, then go right ahead!","title":"Contacts API Setup"},{"location":"setup/setup-contacts-api/#contacts-api-setup","text":"The main library functions are all accessible via the contacts.core.Contacts API. There's no setup required. Just create an instance of Contacts and the world of contacts is at your disposal =) In Kotlin, Contacts ( context ) in Java, ContactsFactory . create ( context ); The context parameter can come from anywhere; Application, Activity, Fragment, or View. It does not matter what context you pass in. The API will only use and store the Application context, to avoid leaks :D It's up to you if you just want to create instances on demand. Or, hold on to instances as a singleton that is injected to your dependency graph (via something like dagger , hilt , or koin ), which will make black box testing and white box testing a walk in the park!","title":"Contacts API Setup"},{"location":"setup/setup-contacts-api/#logging-support","text":"Instances of Contacts hold on to a Logger for logging support. For more info, read Log API input and output .","title":"Logging support"},{"location":"setup/setup-contacts-api/#custom-data-integration","text":"Instances of Contacts hold on to an instance of CustomDataRegistry for custom data integration. For more info, read Integrate custom data .","title":"Custom data integration"},{"location":"setup/setup-contacts-api/#optional-but-recommended-setup","text":"It is recommended to use a single instance of the Contacts API throughout your application using dependency injection . This will allow you to; Use the same Contacts API instance throughout your app. This especially important when integrating custom data. Easily substitute your Contacts API instance with an instance of TestContacts . This is useful in black box testing (UI instrumentation tests; androidTest/ ). It may also be used in your production apps \"test mode\". Easily substitute your Contacts API instance with an instance of MockContacts This is useful in white box testing (unit & integration tests; test/ ). For more info, read Contacts API Testing . Of course, this library does not (and will not) force you to do things you don't want. If you don't care about all of the above and just want to get out a quick prototype of a feature in your app or an entire app, then go right ahead!","title":"Optional, but recommended setup"},{"location":"sim/about-sim-contacts/","text":"SIM Contacts \u00b6 This library gives you APIs that allow you to read and write Contacts stored in the SIM card. SimContactsQuery SimContactsInsert SimContactsUpdate SimContactsDelete SIM Contact data \u00b6 SIM Contact data consists of the name and number . Support for email was recently added in Android 12. I don't think it is stable yet. Regardless, it is too new so this library will wait a bit before adding support for it. Character limits \u00b6 The name and number are subject to the SIM card's maximum character limit, which is typically around 20-30 characters (in modern times). This may vary per SIM card. Inserts or updates will fail if the limit is breached. SIM Contact row ID \u00b6 The SIM contact that an ID is pointing to may change if the contact is deleted in the database and another contact is inserted. The inserted contact may be assigned the ID of the deleted contact. DO NOT RELY ON THIS TO MATCH VALUES IN THE DATABASE! The SIM table does not support selection by ID so you can't use this for anything anyways. Some OEMs automatically sync SIM card data with Contacts Provider data \u00b6 Samsung phones import contacts from SIM into the Contacts Provider. When using the builtin Samsung Contacts app, modifications made to the SIM contacts from the Contacts Provider are propagated to the SIM card and vice versa. Samsung is most likely syncing the SIM contacts with the copy in the Contacts Provider via SyncAdapters. The RawContacts created in the Contacts Provider have a non-remote account name and type (pointing to the SIM card), accountName: primary.sim.account_name, accountType: vnd.sec.contact.sim Furthermore, SIM contacts imported into the Contacts Provider have the same restrictions as the SIM card in that only columns available in the SIM are editable (_id, name, number, emails). Editing SIM contacts using 3rd party apps such as the Google Contacts app are not supported. If you find any issues when using the SimContacts APIs, please raise an issue if you find any bugs or start a discussion and share your thoughts or knowledge =) Multi SIM card support \u00b6 Android 5.1 adds support for using more than one cellular carrier SIM card at a time . This feature lets users activate and use additional SIMs on devices that have two or more SIM card slots. The APIs in this library have not been tested against dual SIM card configurations. It should still work, at the very least the current active SIM card should be accessible. Please raise an issue if you find any bugs or start a discussion and share your thoughts or knowledge =) Limitations \u00b6 Projections, selections, and order is not supported by the IccProvider . Therefore, we are unable to provide include , where , orderBy , limit , and offset functions in our SimContactsQuery API. Due to all of these limitations, all queries will return all contacts in the SIM card. Consumers of this library can perform their own sorting and pagination if they wish. Depending on memory size, SIM cards can hold 200 to 500+ contacts . The most common being around 250. Most, if not all, SIM cards have less than 1mb memory (averaging 32KB to 64KB). Therefore, memory and speed should not be affected much by not being able to sort/order and paginate at the query level. Debugging \u00b6 To look at all of the rows in the SIM Contacts table, use the Context.logSimContactsTable function in the debug module. For more info, read Debug the Sim Contacts table . Known issues \u00b6 Samsung phones (and perhaps other OEMs) support emails (in addition to name and number) data ahead of the Android 12 release. Updating and deleting SIM contacts that have email data using the APIs provided in this library may fail. This issue does not occur when moving the SIM card to a different phone that does not support emails. Developer notes (or for advanced users) \u00b6 In building the SimContacts APIs provided in this library, I used the following hardware to observe the behavior of reading/writing to the SIM card. Smart phones Non-smart phones SIM cards Nexus 6P (Android 8) BLU Z5 (unknown OS) Mint Mobile Samsung Galaxy A71 (Android 11) For software, I used the following apps. Apps Smart phones SIM Card Info v1.1.6 Nexus 6P Samsung Contacts v12.7.10.12 Samsung Galaxy A71 Note that the AOSP Contacts app and Google Contacts app can only import contacts from SIM card so they are not very helpful for us with this investigation. For Android code references, I used the internal IccProvider.java as reference to what the Android OS might be doing when 3rd party applications perform CRUD operations on SIM contacts. IccProvider @ Android 8 IccProvider @ Android 11 IccProvider @ Android 12 I'm using the content://icc/adn URI to read/write from/to SIM card. All of the investigation that I have done here may not apply for all SIM cards and phone OEMs! There is just way too many different SIM cards and phones out there for a single person (me) to test. However, I think that my findings should apply to most cases. Figuring out how to perform CRUD operations \u00b6 First, I added 20 contacts (name and number) to the SIM contacts using the BLU Z5 . The first contact is named \"a\" with number \"1\", the second is named \"ab\" with number \"12\", and so on. The last contact is named \"abcdefghijklmnopqrst\" with number \"12345678901234567890\". I did this because the BLU Z5 has determined that the maximum character limit for the name and number for my Mint Mobile SIM card is 20. Note that the character limits are most likely set by the SIM card and/or calculated by the OS managing it based on how much total memory is available. I also added a contact named \"bro\" with no number and a nameless contact with with number \"5555555555\". For a total of 22 contacts in the SIM card. I loaded the SIM card to my Nexus 6P . Then, I logged all of the rows in content://icc/adn using the Context.logSimContactsTable debug function I wrote up in the debug module. SIM Contact id: 0, name: A, number: 1, emails: null SIM Contact id: 1, name: Ab, number: 12, emails: null SIM Contact id: 2, name: Abc, number: 123, emails: null SIM Contact id: 3, name: Abcd, number: 1234, emails: null SIM Contact id: 4, name: Abcde, number: 12345, emails: null SIM Contact id: 5, name: Abcdef, number: 123456, emails: null SIM Contact id: 6, name: Abcdefg, number: 1234567, emails: null SIM Contact id: 7, name: Abcdefgh, number: 12345678, emails: null SIM Contact id: 8, name: Abcdefghi, number: 123456789, emails: null SIM Contact id: 9, name: Abcdefghij, number: 1234567890, emails: null SIM Contact id: 10, name: Abcdefghijk, number: 12345678901, emails: null SIM Contact id: 11, name: Abcdefghijkl, number: 123456789012, emails: null SIM Contact id: 12, name: Abcdefghijklm, number: 1234567890123, emails: null SIM Contact id: 13, name: Abcdefghijklmn, number: 12345678901234, emails: null SIM Contact id: 14, name: Abcdefghijklmno, number: 123456789012345, emails: null SIM Contact id: 15, name: Abcdefghijklmnop, number: 1234567890123456, emails: null SIM Contact id: 16, name: Abcdefghijklmnopq, number: 12345678901234567, emails: null SIM Contact id: 17, name: Abcdefghijklmnopqr, number: 123456789012345678, emails: null SIM Contact id: 18, name: Abcdefghijklmnopqrs, number: 1234567890123456789, emails: null SIM Contact id: 19, name: Abcdefghijklmnopqrst, number: 12345678901234567890, emails: null SIM Contact id: 20, name: Bro, number: , emails: null SIM Contact id: 21, name: , number: 5555555555, emails: null Our SimContactsQuery also retrieves the same exact results! I am able to see all of the contacts in the SIM Info app except for the nameless contact with number \"5555555555\". I attempted to add a nameless contact using the SIM Info app but it does not allow reading/writing nameless contacts. This is probably a bug in the SIM Info app or a limitation that is intentionally imposed for some reason. I wish I could see the source code of the app! Deleting the first contact with ID of 0 using the SIM Info app works just fine. Deleting the contact with ID of 2 using our SimContactsDelete works just fine too. At this point the first 5 rows in the table are; SIM Contact id: 1, name: Ab, number: 12, emails: null SIM Contact id: 3, name: Abcd, number: 1234, emails: null SIM Contact id: 4, name: Abcde, number: 12345, emails: null SIM Contact id: 5, name: Abcdef, number: 123456, emails: null SIM Contact id: 6, name: Abcdefg, number: 1234567, emails: null Inserting a contact using the SIM Info app and our SimContactsInsert (in that order) works just fine, resulting in two new rows being added. One very interesting to note is that the IDs of the previously deleted rows (0 and 2) have been assigned to the newly inserted contacts! SIM Contact id: 0, name: SIM Info Contact, number: 8, emails: null SIM Contact id: 1, name: Ab, number: 12, emails: null SIM Contact id: 2, name: SimContactsInsert, number: 9, emails: null SIM Contact id: 3, name: Abcd, number: 1234, emails: null SIM Contact id: 4, name: Abcde, number: 12345, emails: null SIM Contact id: 5, name: Abcdef, number: 123456, emails: null SIM Contact id: 6, name: Abcdefg, number: 1234567, emails: null This means that the IDs should not be used as a reference to a particular contact because it could \"change\" in the process of deleting and inserting. As for updates, let's start with this table... SIM Contact id: 3, name: Abcd, number: 1234, emails: null SIM Contact id: 4, name: Abcde, number: 12345, emails: null Notice that Contact ID 0, 1, and 2 are available. Using the SIM Info app to \"update\" the contact with ID 4, we get... SIM Contact id: 3, name: Abcd, number: 1234, emails: null SIM Contact id: 4, name: xxx, number: 12345, emails: null The ID remains 4. We get the same result using our SimContactsUpdate API =) Thus, we have implemented CRUD APIs!!! Figuring out character limits \u00b6 The BLU Z5 non-smartphone has determined that the maximum character limit for the name and number for my Mint Mobile SIM card is 20. I inserted a contact with a name with 26 characters and another contact with a number with 21 characters using the SIM Info app. The first insert (26 char name) succeeded but the second failed (21 char number). SIM Contact id: 0, name: abcdefghijklmnopqrstuvwxyz, number: 1, emails: null I did the same using our SimContactsInsert ... The same thing occurred. This means that the character limit is imposed on the number but perhaps not the name OR maybe the name has not reached the maximum. I tried inserting a name with over 100 characters and it failed. So there is a character limit for the name. I tried inserting names of shorter and shorter lengths until I find the max. It seems to be 30 characters. The character limits for the name is different for my Mint Mobile SIM card is different in the BLU Z5 vs Nexus 6P . BLU Z5 Nexus 6P name 20 30 number 20 20 I took out the SIM card from the Nexus 6P and plugging it back into the BLU Z5 to see if it will show the contacts that go over the 20 character limit. Both contacts with names longer than 20 characters are shown in the BLU Z5 BUT the name is truncated to 20. This could mean one of two things; The phones determine the character limits based on SIM card memory. The SIM card specifies the character limits but the BLU Z5 hard codes it to 20 regardless. Time to check with the Samsung Galaxy A71 ! The Samsung yielded the same results as the Nexus. So, perhaps it is just the self-imposed limitation of the BLU phone. One interesting difference between the Samsung and the Nexus is that our SimContactsInsert was indicating that the insert succeeded in the Samsung even though no new row was created in the SIM table (oh Samsung lol). The result Uri returned by the insert operation is null in the Nexus but not null in the Samsung. What this all means? Our SimContactsInsert and SimContactsUpdate APIs need to be able to detect the maximum character limits for the name and number before performing the actual insert or update operation. To figure out the max character limits, we can attempt to insert a string of length 35 (most names should fit there and most SIM cards have lower limits). Keep attempting to insert until insert succeeds, making the string shorter each time. Delete the successful insert and record the length of the string. Do this for both name and number and store the results in shared preferences mapped to a unique ID of the SIM card. We do not want to do this calculation everytime our APIs are used! Max character limits should be exposed to our API users also. Furthermore, we cannot rely on the result of the insert operation alone. If the result Uri is not null, we must perform a query to sanity check that the actual name and number was inserted! Emails \u00b6 There is an \"emails\" column in the SIM table. CRUD operations for it was not officially supported until recently in Android 12. IccProvider @ Android 11 IccProvider @ Android 12 Look for \"TODO\" comments in the `IccProvider``. You will see TODOs for emails in Android 11 but not Android 12. On my Samsung Galaxy A71 running Android 11... The column name is actually \"emails\" with an \"s\" (plural). What I observed, no email = \",\" at least one email = \"email,\" There seems to be a trailing \",\" regardless. It seems like the emails are in CSV format (comma separated values). I was not able to delete rows with emails in them. I even tried updating the where clause used in our SimContactsDelete to include the email but it does not work. The builtin Samsung Contacts app is able to insert, update, and delete rows with emails. This probably means that we don't have access to the internal APIs that the Samsung Contacts app has. Keep in mind that my Samsung is running Android 11 and support for email was not added until Android 12. TLDR; Classic Samsung to add features farther ahead of time than vanilla Android =) On my Nexus 6P running Android 8... The contacts with emails are shown without email data (emails are null in the SIM table). These rows are able to be updated and deleted. On my BLU Z5... SIM contacts with emails are shown without the email data. These rows are able to be updated and deleted. Other considerations \u00b6 It seems like there are new APIs around SIM Contacts that were introduced in API 31; https://developer.android.com/reference/android/provider/ContactsContract.SimContacts https://developer.android.com/reference/android/provider/SimPhonebookContract Those APIs are too new to be used by this library, which supports API levels down to 19. So, we'll stick with using the content://icc/adn uri to read/write to SIM card until it becomes deprecated, if ever.","title":"About SIM contacts"},{"location":"sim/about-sim-contacts/#sim-contacts","text":"This library gives you APIs that allow you to read and write Contacts stored in the SIM card. SimContactsQuery SimContactsInsert SimContactsUpdate SimContactsDelete","title":"SIM Contacts"},{"location":"sim/about-sim-contacts/#sim-contact-data","text":"SIM Contact data consists of the name and number . Support for email was recently added in Android 12. I don't think it is stable yet. Regardless, it is too new so this library will wait a bit before adding support for it.","title":"SIM Contact data"},{"location":"sim/about-sim-contacts/#character-limits","text":"The name and number are subject to the SIM card's maximum character limit, which is typically around 20-30 characters (in modern times). This may vary per SIM card. Inserts or updates will fail if the limit is breached.","title":"Character limits"},{"location":"sim/about-sim-contacts/#sim-contact-row-id","text":"The SIM contact that an ID is pointing to may change if the contact is deleted in the database and another contact is inserted. The inserted contact may be assigned the ID of the deleted contact. DO NOT RELY ON THIS TO MATCH VALUES IN THE DATABASE! The SIM table does not support selection by ID so you can't use this for anything anyways.","title":"SIM Contact row ID"},{"location":"sim/about-sim-contacts/#some-oems-automatically-sync-sim-card-data-with-contacts-provider-data","text":"Samsung phones import contacts from SIM into the Contacts Provider. When using the builtin Samsung Contacts app, modifications made to the SIM contacts from the Contacts Provider are propagated to the SIM card and vice versa. Samsung is most likely syncing the SIM contacts with the copy in the Contacts Provider via SyncAdapters. The RawContacts created in the Contacts Provider have a non-remote account name and type (pointing to the SIM card), accountName: primary.sim.account_name, accountType: vnd.sec.contact.sim Furthermore, SIM contacts imported into the Contacts Provider have the same restrictions as the SIM card in that only columns available in the SIM are editable (_id, name, number, emails). Editing SIM contacts using 3rd party apps such as the Google Contacts app are not supported. If you find any issues when using the SimContacts APIs, please raise an issue if you find any bugs or start a discussion and share your thoughts or knowledge =)","title":"Some OEMs automatically sync SIM card data with Contacts Provider data"},{"location":"sim/about-sim-contacts/#multi-sim-card-support","text":"Android 5.1 adds support for using more than one cellular carrier SIM card at a time . This feature lets users activate and use additional SIMs on devices that have two or more SIM card slots. The APIs in this library have not been tested against dual SIM card configurations. It should still work, at the very least the current active SIM card should be accessible. Please raise an issue if you find any bugs or start a discussion and share your thoughts or knowledge =)","title":"Multi SIM card support"},{"location":"sim/about-sim-contacts/#limitations","text":"Projections, selections, and order is not supported by the IccProvider . Therefore, we are unable to provide include , where , orderBy , limit , and offset functions in our SimContactsQuery API. Due to all of these limitations, all queries will return all contacts in the SIM card. Consumers of this library can perform their own sorting and pagination if they wish. Depending on memory size, SIM cards can hold 200 to 500+ contacts . The most common being around 250. Most, if not all, SIM cards have less than 1mb memory (averaging 32KB to 64KB). Therefore, memory and speed should not be affected much by not being able to sort/order and paginate at the query level.","title":"Limitations"},{"location":"sim/about-sim-contacts/#debugging","text":"To look at all of the rows in the SIM Contacts table, use the Context.logSimContactsTable function in the debug module. For more info, read Debug the Sim Contacts table .","title":"Debugging"},{"location":"sim/about-sim-contacts/#known-issues","text":"Samsung phones (and perhaps other OEMs) support emails (in addition to name and number) data ahead of the Android 12 release. Updating and deleting SIM contacts that have email data using the APIs provided in this library may fail. This issue does not occur when moving the SIM card to a different phone that does not support emails.","title":"Known issues"},{"location":"sim/about-sim-contacts/#developer-notes-or-for-advanced-users","text":"In building the SimContacts APIs provided in this library, I used the following hardware to observe the behavior of reading/writing to the SIM card. Smart phones Non-smart phones SIM cards Nexus 6P (Android 8) BLU Z5 (unknown OS) Mint Mobile Samsung Galaxy A71 (Android 11) For software, I used the following apps. Apps Smart phones SIM Card Info v1.1.6 Nexus 6P Samsung Contacts v12.7.10.12 Samsung Galaxy A71 Note that the AOSP Contacts app and Google Contacts app can only import contacts from SIM card so they are not very helpful for us with this investigation. For Android code references, I used the internal IccProvider.java as reference to what the Android OS might be doing when 3rd party applications perform CRUD operations on SIM contacts. IccProvider @ Android 8 IccProvider @ Android 11 IccProvider @ Android 12 I'm using the content://icc/adn URI to read/write from/to SIM card. All of the investigation that I have done here may not apply for all SIM cards and phone OEMs! There is just way too many different SIM cards and phones out there for a single person (me) to test. However, I think that my findings should apply to most cases.","title":"Developer notes (or for advanced users)"},{"location":"sim/about-sim-contacts/#figuring-out-how-to-perform-crud-operations","text":"First, I added 20 contacts (name and number) to the SIM contacts using the BLU Z5 . The first contact is named \"a\" with number \"1\", the second is named \"ab\" with number \"12\", and so on. The last contact is named \"abcdefghijklmnopqrst\" with number \"12345678901234567890\". I did this because the BLU Z5 has determined that the maximum character limit for the name and number for my Mint Mobile SIM card is 20. Note that the character limits are most likely set by the SIM card and/or calculated by the OS managing it based on how much total memory is available. I also added a contact named \"bro\" with no number and a nameless contact with with number \"5555555555\". For a total of 22 contacts in the SIM card. I loaded the SIM card to my Nexus 6P . Then, I logged all of the rows in content://icc/adn using the Context.logSimContactsTable debug function I wrote up in the debug module. SIM Contact id: 0, name: A, number: 1, emails: null SIM Contact id: 1, name: Ab, number: 12, emails: null SIM Contact id: 2, name: Abc, number: 123, emails: null SIM Contact id: 3, name: Abcd, number: 1234, emails: null SIM Contact id: 4, name: Abcde, number: 12345, emails: null SIM Contact id: 5, name: Abcdef, number: 123456, emails: null SIM Contact id: 6, name: Abcdefg, number: 1234567, emails: null SIM Contact id: 7, name: Abcdefgh, number: 12345678, emails: null SIM Contact id: 8, name: Abcdefghi, number: 123456789, emails: null SIM Contact id: 9, name: Abcdefghij, number: 1234567890, emails: null SIM Contact id: 10, name: Abcdefghijk, number: 12345678901, emails: null SIM Contact id: 11, name: Abcdefghijkl, number: 123456789012, emails: null SIM Contact id: 12, name: Abcdefghijklm, number: 1234567890123, emails: null SIM Contact id: 13, name: Abcdefghijklmn, number: 12345678901234, emails: null SIM Contact id: 14, name: Abcdefghijklmno, number: 123456789012345, emails: null SIM Contact id: 15, name: Abcdefghijklmnop, number: 1234567890123456, emails: null SIM Contact id: 16, name: Abcdefghijklmnopq, number: 12345678901234567, emails: null SIM Contact id: 17, name: Abcdefghijklmnopqr, number: 123456789012345678, emails: null SIM Contact id: 18, name: Abcdefghijklmnopqrs, number: 1234567890123456789, emails: null SIM Contact id: 19, name: Abcdefghijklmnopqrst, number: 12345678901234567890, emails: null SIM Contact id: 20, name: Bro, number: , emails: null SIM Contact id: 21, name: , number: 5555555555, emails: null Our SimContactsQuery also retrieves the same exact results! I am able to see all of the contacts in the SIM Info app except for the nameless contact with number \"5555555555\". I attempted to add a nameless contact using the SIM Info app but it does not allow reading/writing nameless contacts. This is probably a bug in the SIM Info app or a limitation that is intentionally imposed for some reason. I wish I could see the source code of the app! Deleting the first contact with ID of 0 using the SIM Info app works just fine. Deleting the contact with ID of 2 using our SimContactsDelete works just fine too. At this point the first 5 rows in the table are; SIM Contact id: 1, name: Ab, number: 12, emails: null SIM Contact id: 3, name: Abcd, number: 1234, emails: null SIM Contact id: 4, name: Abcde, number: 12345, emails: null SIM Contact id: 5, name: Abcdef, number: 123456, emails: null SIM Contact id: 6, name: Abcdefg, number: 1234567, emails: null Inserting a contact using the SIM Info app and our SimContactsInsert (in that order) works just fine, resulting in two new rows being added. One very interesting to note is that the IDs of the previously deleted rows (0 and 2) have been assigned to the newly inserted contacts! SIM Contact id: 0, name: SIM Info Contact, number: 8, emails: null SIM Contact id: 1, name: Ab, number: 12, emails: null SIM Contact id: 2, name: SimContactsInsert, number: 9, emails: null SIM Contact id: 3, name: Abcd, number: 1234, emails: null SIM Contact id: 4, name: Abcde, number: 12345, emails: null SIM Contact id: 5, name: Abcdef, number: 123456, emails: null SIM Contact id: 6, name: Abcdefg, number: 1234567, emails: null This means that the IDs should not be used as a reference to a particular contact because it could \"change\" in the process of deleting and inserting. As for updates, let's start with this table... SIM Contact id: 3, name: Abcd, number: 1234, emails: null SIM Contact id: 4, name: Abcde, number: 12345, emails: null Notice that Contact ID 0, 1, and 2 are available. Using the SIM Info app to \"update\" the contact with ID 4, we get... SIM Contact id: 3, name: Abcd, number: 1234, emails: null SIM Contact id: 4, name: xxx, number: 12345, emails: null The ID remains 4. We get the same result using our SimContactsUpdate API =) Thus, we have implemented CRUD APIs!!!","title":"Figuring out how to perform CRUD operations"},{"location":"sim/about-sim-contacts/#figuring-out-character-limits","text":"The BLU Z5 non-smartphone has determined that the maximum character limit for the name and number for my Mint Mobile SIM card is 20. I inserted a contact with a name with 26 characters and another contact with a number with 21 characters using the SIM Info app. The first insert (26 char name) succeeded but the second failed (21 char number). SIM Contact id: 0, name: abcdefghijklmnopqrstuvwxyz, number: 1, emails: null I did the same using our SimContactsInsert ... The same thing occurred. This means that the character limit is imposed on the number but perhaps not the name OR maybe the name has not reached the maximum. I tried inserting a name with over 100 characters and it failed. So there is a character limit for the name. I tried inserting names of shorter and shorter lengths until I find the max. It seems to be 30 characters. The character limits for the name is different for my Mint Mobile SIM card is different in the BLU Z5 vs Nexus 6P . BLU Z5 Nexus 6P name 20 30 number 20 20 I took out the SIM card from the Nexus 6P and plugging it back into the BLU Z5 to see if it will show the contacts that go over the 20 character limit. Both contacts with names longer than 20 characters are shown in the BLU Z5 BUT the name is truncated to 20. This could mean one of two things; The phones determine the character limits based on SIM card memory. The SIM card specifies the character limits but the BLU Z5 hard codes it to 20 regardless. Time to check with the Samsung Galaxy A71 ! The Samsung yielded the same results as the Nexus. So, perhaps it is just the self-imposed limitation of the BLU phone. One interesting difference between the Samsung and the Nexus is that our SimContactsInsert was indicating that the insert succeeded in the Samsung even though no new row was created in the SIM table (oh Samsung lol). The result Uri returned by the insert operation is null in the Nexus but not null in the Samsung. What this all means? Our SimContactsInsert and SimContactsUpdate APIs need to be able to detect the maximum character limits for the name and number before performing the actual insert or update operation. To figure out the max character limits, we can attempt to insert a string of length 35 (most names should fit there and most SIM cards have lower limits). Keep attempting to insert until insert succeeds, making the string shorter each time. Delete the successful insert and record the length of the string. Do this for both name and number and store the results in shared preferences mapped to a unique ID of the SIM card. We do not want to do this calculation everytime our APIs are used! Max character limits should be exposed to our API users also. Furthermore, we cannot rely on the result of the insert operation alone. If the result Uri is not null, we must perform a query to sanity check that the actual name and number was inserted!","title":"Figuring out character limits"},{"location":"sim/about-sim-contacts/#emails","text":"There is an \"emails\" column in the SIM table. CRUD operations for it was not officially supported until recently in Android 12. IccProvider @ Android 11 IccProvider @ Android 12 Look for \"TODO\" comments in the `IccProvider``. You will see TODOs for emails in Android 11 but not Android 12. On my Samsung Galaxy A71 running Android 11... The column name is actually \"emails\" with an \"s\" (plural). What I observed, no email = \",\" at least one email = \"email,\" There seems to be a trailing \",\" regardless. It seems like the emails are in CSV format (comma separated values). I was not able to delete rows with emails in them. I even tried updating the where clause used in our SimContactsDelete to include the email but it does not work. The builtin Samsung Contacts app is able to insert, update, and delete rows with emails. This probably means that we don't have access to the internal APIs that the Samsung Contacts app has. Keep in mind that my Samsung is running Android 11 and support for email was not added until Android 12. TLDR; Classic Samsung to add features farther ahead of time than vanilla Android =) On my Nexus 6P running Android 8... The contacts with emails are shown without email data (emails are null in the SIM table). These rows are able to be updated and deleted. On my BLU Z5... SIM contacts with emails are shown without the email data. These rows are able to be updated and deleted.","title":"Emails"},{"location":"sim/about-sim-contacts/#other-considerations","text":"It seems like there are new APIs around SIM Contacts that were introduced in API 31; https://developer.android.com/reference/android/provider/ContactsContract.SimContacts https://developer.android.com/reference/android/provider/SimPhonebookContract Those APIs are too new to be used by this library, which supports API levels down to 19. So, we'll stick with using the content://icc/adn uri to read/write to SIM card until it becomes deprecated, if ever.","title":"Other considerations"},{"location":"sim/delete-sim-contacts/","text":"Delete contacts from SIM card \u00b6 This library provides the SimContactsDelete API that allows you to delete existing contacts from the SIM card. An instance of the SimContactsDelete API is obtained by, val delete = Contacts ( context ). sim (). delete () A basic delete \u00b6 To delete a set of existing contacts from the SIM card, val deleteResult = Contacts ( context ) . sim () . delete () . simContacts ( existingSimContacts ) . commit () Executing the delete \u00b6 To execute the delete, . commit () Handling the delete result \u00b6 The commit function returns a Result , To check if all deletes succeeded, val allDeletesSuccessful = deleteResult . isSuccessful To check if a particular delete succeeded, val firstDeleteSuccessful = deleteResult . isSuccessful ( simContact ) Performing the delete and result processing asynchronously \u00b6 Deletes are executed when the commit or commitInOneTransaction function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the delete with permission \u00b6 Deletes require the android.permission.WRITE_CONTACTS permissions. If not granted, the delete will do nothing and return a failed result. To perform the delete with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Known issues \u00b6 Samsung phones (and perhaps other OEMs) support emails (in addition to name and number) data ahead of the Android 12 release. Updating and deleting SIM contacts that have email data using the APIs provided in this library may fail. This issue does not occur when moving the SIM card to a different phone that does not support emails.","title":"Delete contacts from SIM card"},{"location":"sim/delete-sim-contacts/#delete-contacts-from-sim-card","text":"This library provides the SimContactsDelete API that allows you to delete existing contacts from the SIM card. An instance of the SimContactsDelete API is obtained by, val delete = Contacts ( context ). sim (). delete ()","title":"Delete contacts from SIM card"},{"location":"sim/delete-sim-contacts/#a-basic-delete","text":"To delete a set of existing contacts from the SIM card, val deleteResult = Contacts ( context ) . sim () . delete () . simContacts ( existingSimContacts ) . commit ()","title":"A basic delete"},{"location":"sim/delete-sim-contacts/#executing-the-delete","text":"To execute the delete, . commit ()","title":"Executing the delete"},{"location":"sim/delete-sim-contacts/#handling-the-delete-result","text":"The commit function returns a Result , To check if all deletes succeeded, val allDeletesSuccessful = deleteResult . isSuccessful To check if a particular delete succeeded, val firstDeleteSuccessful = deleteResult . isSuccessful ( simContact )","title":"Handling the delete result"},{"location":"sim/delete-sim-contacts/#performing-the-delete-and-result-processing-asynchronously","text":"Deletes are executed when the commit or commitInOneTransaction function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the delete and result processing asynchronously"},{"location":"sim/delete-sim-contacts/#performing-the-delete-with-permission","text":"Deletes require the android.permission.WRITE_CONTACTS permissions. If not granted, the delete will do nothing and return a failed result. To perform the delete with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the delete with permission"},{"location":"sim/delete-sim-contacts/#known-issues","text":"Samsung phones (and perhaps other OEMs) support emails (in addition to name and number) data ahead of the Android 12 release. Updating and deleting SIM contacts that have email data using the APIs provided in this library may fail. This issue does not occur when moving the SIM card to a different phone that does not support emails.","title":"Known issues"},{"location":"sim/insert-sim-contacts/","text":"Insert contacts into SIM card \u00b6 This library provides the SimContactsInsert API that allows you to create/insert contacts into the SIM card. An instance of the SimContactsInsert API is obtained by, val insert = Contacts ( context ). sim (). insert () A basic insert \u00b6 To create/insert a new contact into the SIM card, val insertResult = Contacts ( context ) . sim () . insert () . simContact ( NewSimContact ( name = \"Dude\" , number = \"5555555555\" )) . commit () If you need to insert multiple contacts, val newContact1 = NewSimContact ( name = \"Dude1\" , number = \"1234567890\" ) val newContact2 = NewSimContact ( name = \"Dude2\" , number = \"0987654321\" ) val insertResult = Contacts ( context ) . sim () . insert () . simContacts ( newContact1 , newContact2 ) . commit () Blank contacts are ignored \u00b6 Blank contacts (name AND number are both null or blank) will NOT be inserted. The name OR number can be null or blank but not both. Character limits \u00b6 The name and number are subject to the SIM card's maximum character limit, which is typically around 20-30 characters (in modern times). This may vary per SIM card. Inserts or updates will fail if the limit is breached. Executing the insert \u00b6 To execute the insert, . commit () Handling the insert result \u00b6 The commit function returns a Result . To check if all inserts succeeded, val allInsertsSuccessful = insertResult . isSuccessful To check if a particular insert succeeded, val firstInsertSuccessful = insertResult . isSuccessful ( newContact1 ) The IccProvider does not yet return the row ID os newly inserted contacts. Look at the \"TODO\" at line 259 of Android's IccProvider . Therefore, this library's insert API is does not yet support getting the new rows from the result. Cancelling the insert \u00b6 To cancel an insert amid execution, . commit { returnTrueIfInsertShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel insert processing as soon as possible. The function is called numerous times during insert processing to check if processing should stop or continue. This gives you the option to cancel the insert. For example, to automatically cancel the insert inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val insertResult = insert . commit { ! isActive } } } Performing the insert and result processing asynchronously \u00b6 Inserts are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the insert with permission \u00b6 Inserts require the android.permission.WRITE_CONTACTS permission. If not granted, the insert will do nothing and return a failed result. To perform the insert with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Insert contacts into SIM card"},{"location":"sim/insert-sim-contacts/#insert-contacts-into-sim-card","text":"This library provides the SimContactsInsert API that allows you to create/insert contacts into the SIM card. An instance of the SimContactsInsert API is obtained by, val insert = Contacts ( context ). sim (). insert ()","title":"Insert contacts into SIM card"},{"location":"sim/insert-sim-contacts/#a-basic-insert","text":"To create/insert a new contact into the SIM card, val insertResult = Contacts ( context ) . sim () . insert () . simContact ( NewSimContact ( name = \"Dude\" , number = \"5555555555\" )) . commit () If you need to insert multiple contacts, val newContact1 = NewSimContact ( name = \"Dude1\" , number = \"1234567890\" ) val newContact2 = NewSimContact ( name = \"Dude2\" , number = \"0987654321\" ) val insertResult = Contacts ( context ) . sim () . insert () . simContacts ( newContact1 , newContact2 ) . commit ()","title":"A basic insert"},{"location":"sim/insert-sim-contacts/#blank-contacts-are-ignored","text":"Blank contacts (name AND number are both null or blank) will NOT be inserted. The name OR number can be null or blank but not both.","title":"Blank contacts are ignored"},{"location":"sim/insert-sim-contacts/#character-limits","text":"The name and number are subject to the SIM card's maximum character limit, which is typically around 20-30 characters (in modern times). This may vary per SIM card. Inserts or updates will fail if the limit is breached.","title":"Character limits"},{"location":"sim/insert-sim-contacts/#executing-the-insert","text":"To execute the insert, . commit ()","title":"Executing the insert"},{"location":"sim/insert-sim-contacts/#handling-the-insert-result","text":"The commit function returns a Result . To check if all inserts succeeded, val allInsertsSuccessful = insertResult . isSuccessful To check if a particular insert succeeded, val firstInsertSuccessful = insertResult . isSuccessful ( newContact1 ) The IccProvider does not yet return the row ID os newly inserted contacts. Look at the \"TODO\" at line 259 of Android's IccProvider . Therefore, this library's insert API is does not yet support getting the new rows from the result.","title":"Handling the insert result"},{"location":"sim/insert-sim-contacts/#cancelling-the-insert","text":"To cancel an insert amid execution, . commit { returnTrueIfInsertShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel insert processing as soon as possible. The function is called numerous times during insert processing to check if processing should stop or continue. This gives you the option to cancel the insert. For example, to automatically cancel the insert inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val insertResult = insert . commit { ! isActive } } }","title":"Cancelling the insert"},{"location":"sim/insert-sim-contacts/#performing-the-insert-and-result-processing-asynchronously","text":"Inserts are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the insert and result processing asynchronously"},{"location":"sim/insert-sim-contacts/#performing-the-insert-with-permission","text":"Inserts require the android.permission.WRITE_CONTACTS permission. If not granted, the insert will do nothing and return a failed result. To perform the insert with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the insert with permission"},{"location":"sim/query-sim-contacts/","text":"Query contacts in SIM card \u00b6 This library provides the SimContactsQuery API that allows you to get contacts stored in the SIM card. An instance of the SimContactsQuery API is obtained by, val query = Contacts ( context ). sim (). query () A basic query \u00b6 To get all of the contacts in the SIM card, val simContacts = Contacts ( context ). sim (). query (). find () Limitations \u00b6 Projections, selections, and order is not supported by the IccProvider . Therefore, we are unable to provide include , where , orderBy , limit , and offset functions in our SimContactsQuery API. Due to all of these limitations, all queries will return all contacts in the SIM card. You may perform your own sorting and pagination if you wish. Depending on memory size, SIM cards can hold 200 to 500+ contacts . The most common being around 250. Most, if not all, SIM cards have less than 1mb memory (averaging 32KB to 64KB). Therefore, memory and speed should not be affected much by not being able to sort/order and paginate at the query level. For more info, read about SIM Contacts Executing the query \u00b6 To execute the query, . find () Cancelling the query \u00b6 To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val simContacts = query . find { ! isActive } } } Performing the query asynchronously \u00b6 Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the query with permission \u00b6 Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Query contacts in SIM card"},{"location":"sim/query-sim-contacts/#query-contacts-in-sim-card","text":"This library provides the SimContactsQuery API that allows you to get contacts stored in the SIM card. An instance of the SimContactsQuery API is obtained by, val query = Contacts ( context ). sim (). query ()","title":"Query contacts in SIM card"},{"location":"sim/query-sim-contacts/#a-basic-query","text":"To get all of the contacts in the SIM card, val simContacts = Contacts ( context ). sim (). query (). find ()","title":"A basic query"},{"location":"sim/query-sim-contacts/#limitations","text":"Projections, selections, and order is not supported by the IccProvider . Therefore, we are unable to provide include , where , orderBy , limit , and offset functions in our SimContactsQuery API. Due to all of these limitations, all queries will return all contacts in the SIM card. You may perform your own sorting and pagination if you wish. Depending on memory size, SIM cards can hold 200 to 500+ contacts . The most common being around 250. Most, if not all, SIM cards have less than 1mb memory (averaging 32KB to 64KB). Therefore, memory and speed should not be affected much by not being able to sort/order and paginate at the query level. For more info, read about SIM Contacts","title":"Limitations"},{"location":"sim/query-sim-contacts/#executing-the-query","text":"To execute the query, . find ()","title":"Executing the query"},{"location":"sim/query-sim-contacts/#cancelling-the-query","text":"To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val simContacts = query . find { ! isActive } } }","title":"Cancelling the query"},{"location":"sim/query-sim-contacts/#performing-the-query-asynchronously","text":"Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the query asynchronously"},{"location":"sim/query-sim-contacts/#performing-the-query-with-permission","text":"Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the query with permission"},{"location":"sim/update-sim-contacts/","text":"Update contacts in SIM card \u00b6 This library provides the SimContactsUpdate API that allows you to update contacts in the SIM card. An instance of the SimContactsUpdate API is obtained by, val update = Contacts ( context ). sim (). update () A basic update \u00b6 To update an existing contact in the SIM card, var current : SimContact var modified : MutableSimContact = current . mutableCopy { // change the name and/or number } val updateResult = Contacts ( context ) . sim () . update () . simContact ( current , modified ) . commit () Making further updates \u00b6 The current entry in the SIM table is not updated based on the ID. Instead, the name AND number are used to lookup the entry to update. Continuing the example above, if you need to make another update, then you must use the modified copy as the current, current = modified modified = current . newCopy { // change the name and/or number } val result = update . simContact ( current , modified ) . commit () This limitation comes from Android, not this library. Updating multiple contacts \u00b6 If you need to update multiple contacts, val update1 = SimContactsUpdate . Entry ( contact1 , contact1 . mutableCopy { ... }) val update2 = SimContactsUpdate . Entry ( contact2 , contact2 . mutableCopy { ... }) val updateResult = Contacts ( context ) . sim () . update () . simContacts ( update1 , update2 ) . commit () Blank contacts are ignored \u00b6 Blank contacts (name AND number are both null or blank) will NOT be updated. The name OR number can be null or blank but not both. Character limits \u00b6 The name and number are subject to the SIM card's maximum character limit, which is typically around 20-30 characters (in modern times). This may vary per SIM card. Inserts or updates will fail if the limit is breached. Executing the update \u00b6 To execute the update, . commit () Handling the update result \u00b6 The commit function returns a Result . To check if all updates succeeded, val allUpdatesSuccessful = updateResult . isSuccessful To check if a particular update succeeded, val firstUpdateSuccessful = updateResult . isSuccessful ( simContact ) Cancelling the update \u00b6 To cancel an update amid execution, . commit { returnTrueIfUpdateShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel update processing as soon as possible. The function is called numerous times during update processing to check if processing should stop or continue. This gives you the option to cancel the update. For example, to automatically cancel the update inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val updateResult = update . commit { ! isActive } } } Performing the update and result processing asynchronously \u00b6 Updates are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the update with permission \u00b6 Updates require the android.permission.WRITE_CONTACTS permission. If not granted, the update will do nothing and return a failed result. To perform the update with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Known issues \u00b6 Samsung phones (and perhaps other OEMs) support emails (in addition to name and number) data ahead of the Android 12 release. Updating and deleting SIM contacts that have email data using the APIs provided in this library may fail. This issue does not occur when moving the SIM card to a different phone that does not support emails.","title":"Update contacts in SIM card"},{"location":"sim/update-sim-contacts/#update-contacts-in-sim-card","text":"This library provides the SimContactsUpdate API that allows you to update contacts in the SIM card. An instance of the SimContactsUpdate API is obtained by, val update = Contacts ( context ). sim (). update ()","title":"Update contacts in SIM card"},{"location":"sim/update-sim-contacts/#a-basic-update","text":"To update an existing contact in the SIM card, var current : SimContact var modified : MutableSimContact = current . mutableCopy { // change the name and/or number } val updateResult = Contacts ( context ) . sim () . update () . simContact ( current , modified ) . commit ()","title":"A basic update"},{"location":"sim/update-sim-contacts/#making-further-updates","text":"The current entry in the SIM table is not updated based on the ID. Instead, the name AND number are used to lookup the entry to update. Continuing the example above, if you need to make another update, then you must use the modified copy as the current, current = modified modified = current . newCopy { // change the name and/or number } val result = update . simContact ( current , modified ) . commit () This limitation comes from Android, not this library.","title":"Making further updates"},{"location":"sim/update-sim-contacts/#updating-multiple-contacts","text":"If you need to update multiple contacts, val update1 = SimContactsUpdate . Entry ( contact1 , contact1 . mutableCopy { ... }) val update2 = SimContactsUpdate . Entry ( contact2 , contact2 . mutableCopy { ... }) val updateResult = Contacts ( context ) . sim () . update () . simContacts ( update1 , update2 ) . commit ()","title":"Updating multiple contacts"},{"location":"sim/update-sim-contacts/#blank-contacts-are-ignored","text":"Blank contacts (name AND number are both null or blank) will NOT be updated. The name OR number can be null or blank but not both.","title":"Blank contacts are ignored"},{"location":"sim/update-sim-contacts/#character-limits","text":"The name and number are subject to the SIM card's maximum character limit, which is typically around 20-30 characters (in modern times). This may vary per SIM card. Inserts or updates will fail if the limit is breached.","title":"Character limits"},{"location":"sim/update-sim-contacts/#executing-the-update","text":"To execute the update, . commit ()","title":"Executing the update"},{"location":"sim/update-sim-contacts/#handling-the-update-result","text":"The commit function returns a Result . To check if all updates succeeded, val allUpdatesSuccessful = updateResult . isSuccessful To check if a particular update succeeded, val firstUpdateSuccessful = updateResult . isSuccessful ( simContact )","title":"Handling the update result"},{"location":"sim/update-sim-contacts/#cancelling-the-update","text":"To cancel an update amid execution, . commit { returnTrueIfUpdateShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel update processing as soon as possible. The function is called numerous times during update processing to check if processing should stop or continue. This gives you the option to cancel the update. For example, to automatically cancel the update inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val updateResult = update . commit { ! isActive } } }","title":"Cancelling the update"},{"location":"sim/update-sim-contacts/#performing-the-update-and-result-processing-asynchronously","text":"Updates are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the update and result processing asynchronously"},{"location":"sim/update-sim-contacts/#performing-the-update-with-permission","text":"Updates require the android.permission.WRITE_CONTACTS permission. If not granted, the update will do nothing and return a failed result. To perform the update with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the update with permission"},{"location":"sim/update-sim-contacts/#known-issues","text":"Samsung phones (and perhaps other OEMs) support emails (in addition to name and number) data ahead of the Android 12 release. Updating and deleting SIM contacts that have email data using the APIs provided in this library may fail. This issue does not occur when moving the SIM card to a different phone that does not support emails.","title":"Known issues"},{"location":"testing/test-contacts-api/","text":"Contacts API Testing \u00b6 TODO Complete this docs when implementation is complete TODO Add a reference to this docs in the README and the blog This library provides the TestContacts and MockContacts , which you can use as a substitute to your Contacts API instance in; black box tests ; UI instrumentation tests in androidTest/ white box tests ; unit & integration tests in test/ UI instrumentation tests \u00b6 TODO Show usage of TestContacts Unit & integration tests \u00b6 TODO Show usage of MockContacts Production test mode \u00b6 The TestContacts may also be used in your production apps, not just in tests. If you want your production app to interact (query, insert, update, delete) with only \"test contacts\", all you would need to do is substitute your Contacts API instance with an instance of TestContacts . @Singleton fun provideContactsApi ( context : Context ): Contacts = if ( test ) { TestContacts ( context ) } else { Contacts ( context ) } The above code block is just pseudo-code for a dependency injection setup. For example, if you are building a contacts app, you can add a \"test\" or \"debug\" mode such that only test contacts are; returned by query APIs updated by update APIs inserted by insert APIs deleted by delete APIs When turning off test/debug mode, you can easily delete all test contacts created during the session and return to normal mode.","title":"Contacts API Testing"},{"location":"testing/test-contacts-api/#contacts-api-testing","text":"TODO Complete this docs when implementation is complete TODO Add a reference to this docs in the README and the blog This library provides the TestContacts and MockContacts , which you can use as a substitute to your Contacts API instance in; black box tests ; UI instrumentation tests in androidTest/ white box tests ; unit & integration tests in test/","title":"Contacts API Testing"},{"location":"testing/test-contacts-api/#ui-instrumentation-tests","text":"TODO Show usage of TestContacts","title":"UI instrumentation tests"},{"location":"testing/test-contacts-api/#unit-integration-tests","text":"TODO Show usage of MockContacts","title":"Unit & integration tests"},{"location":"testing/test-contacts-api/#production-test-mode","text":"The TestContacts may also be used in your production apps, not just in tests. If you want your production app to interact (query, insert, update, delete) with only \"test contacts\", all you would need to do is substitute your Contacts API instance with an instance of TestContacts . @Singleton fun provideContactsApi ( context : Context ): Contacts = if ( test ) { TestContacts ( context ) } else { Contacts ( context ) } The above code block is just pseudo-code for a dependency injection setup. For example, if you are building a contacts app, you can add a \"test\" or \"debug\" mode such that only test contacts are; returned by query APIs updated by update APIs inserted by insert APIs deleted by delete APIs When turning off test/debug mode, you can easily delete all test contacts created during the session and return to normal mode.","title":"Production test mode"},{"location":"ui/integrate-rudimentary-contacts-integrated-ui-components/","text":"Integrate rudimentary contacts ui components \u00b6 TODO Coming soon","title":"Integrate rudimentary contacts ui components"},{"location":"ui/integrate-rudimentary-contacts-integrated-ui-components/#integrate-rudimentary-contacts-ui-components","text":"TODO Coming soon","title":"Integrate rudimentary contacts ui components"}]} \ No newline at end of file +{"config":{"indexing":"full","lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"","text":"Android Contacts, Reborn \u00b6 \u2139\ufe0f Written with \u2665\ufe0f and \ud83d\udd25 since December 2018. Open sourced since October 2021. This library provides a complete set of APIs to do everything you need with Contacts in Android. You no longer have to deal with the Contacts Provider , database operations, and cursors. Whether you just need to get all or some Contacts for a small part of your app (written in Kotlin or Java), or you are looking to create your own full-fledged Contacts app with the same capabilities as the native (AOSP) Android Contacts app and Google Contacts app, this library is for you! Please help support this project \ud83d\ude4f\u2764\ufe0f\u2b50\ufe0f Quick links \u00b6 \ud83d\udcdc Documentation \ud83d\ude89 Current release - 0.2.0 \ud83d\ude82 Upcoming release - v0.3.0 \ud83d\uddfa Project roadmap \ud83d\udc8c Why use this library? Features \u00b6 The core module provides, \u2705 All data kinds in the Contacts Provider; address, email, event, group membership, IM, name, nickname, note, organization, phone, photo, relation, SIP address, and website . \u2705 Custom data integration . \u2705 Broad queries and advanced queries of Contacts and RawContacts from zero or more Accounts and/or Groups. \u2705 Contact lookup keys \u2705 Include only desired fields in read/write operations to optimize CPU and memory . \u2705 Powerful, type-safe query DSL . \u2705 Pagination using order by, limit, and offset database functions. \u2705 Insert one or more RawContacts with an associated Account, causing automatic insertion of a new Contact subject to automatic aggregation by the Contacts Provider. \u2705 Update one or more Contacts, RawContacts, and Data. \u2705 Delete one or more Contacts, RawContacts, and Data. \u2705 Query , insert , update , and delete Profile (device owner) Contact, RawContact, and Data. \u2705 Query , insert , update , and delete Groups . \u2705 Query , insert update , and delete specific kinds of data . \u2705 Query , insert , update , and delete custom data . \u2705 Query , insert , and delete Blocked Numbers . \u2705 Query , insert , update , and delete SIM card contacts . \u2705 Query for Accounts in the system or RawContacts table. \u2705 Query for just RawContacts. \u2705 Associate local RawContacts (no Account) to an Account . \u2705 Link/unlink two or more Contacts. \u2705 Get/set contact options ; starred (favorite), custom ringtone, send to voicemail . \u2705 Get/set Contacts/RawContact photo and thumbnail . \u2705 Get/set default (primary) Contact Data (e.g. default/primary phone number, email, etc). \u2705 Convenience functions . \u2705 Contact data is synced automatically across devices . \u2705 Support for logging API input and output \u2705 Redactable entities and API input and output for production-safe logging that upholds user data privacy laws to meet GDPR guidelines (this is not legal advice) . \u2705 Full in-depth documentation/guides . \u2705 Full Java interoptibilty . \u2705 Core APIs have zero dependency . \u2705 Clean separation between Contacts vs RawContacts . \u2705 Clear distinction between truly deeply immutable, mutable, new, and existing entities allowing for thread safety and JetPack compose optimizations . There are also extensions that add functionality to every core function, \ud83e\uddf0 Asynchronous work using Kotlin Coroutines . \ud83e\uddf0 Permissions request/handling using Kotlin Coroutines . \ud83d\udd1c Kotlin Flow extensions \ud83d\udd1c RxJava extensions Also included are some pre-baked goodies to be used as is or just for reference, \ud83c\udf6c Gender custom data . \ud83c\udf6c Google Contacts custom data . \ud83c\udf6c Handle name custom data . \ud83c\udf6c Pokemon custom data \ud83c\udf6c Role Playing Game (RPG) custom data . \ud83c\udf6c Rudimentary contacts-integrated UI components . \ud83c\udf6c Debug functions to aid in development There are also more features that are on the way! \u2622\ufe0f Work profile contacts \u2622\ufe0f Dynamically integrate custom data from other apps \u2622\ufe0f Read/write from/to .VCF file . Installation \u00b6 \u2139\ufe0f This library is a multi-module project published with JitPack First, include JitPack in the repositories list, repositories { maven { url \"https://jitpack.io\" } } To install individual modules, dependencies { implementation 'com.github.vestrel00.contacts-android:core:0.2.0' implementation 'com.github.vestrel00.contacts-android:async:0.2.0' implementation 'com.github.vestrel00.contacts-android:customdata-gender:0.2.0' implementation 'com.github.vestrel00.contacts-android:customdata-googlecontacts:0.2.0' implementation 'com.github.vestrel00.contacts-android:customdata-handlename:0.2.0' implementation 'com.github.vestrel00.contacts-android:customdata-pokemon:0.2.0' implementation 'com.github.vestrel00.contacts-android:customdata-rpg:0.2.0' implementation 'com.github.vestrel00.contacts-android:debug:0.2.0' implementation 'com.github.vestrel00.contacts-android:permissions:0.2.0' implementation 'com.github.vestrel00.contacts-android:test:0.2.0' implementation 'com.github.vestrel00.contacts-android:ui:0.2.0' // Notice that when importing specific modules/subprojects, the first \":\" comes after \"contacts-android\". } The core module is really all you need. All other modules are optional. To install all modules in a single line, dependencies { implementation 'com.github.vestrel00:contacts-android:0.2.0' // Notice that when importing all modules, the first \":\" comes after \"vestrel00\". } \u26a0\ufe0f Starting with version 0.2.0, installing all modules in a single line is only supported when using the dependencyResolutionManagement in settings.gradle . You are still able to install all modules by specifying them individually. For more info about the different modules and dependency resolution management, read the Installation guide . Setup \u00b6 There is no setup required. It's up to you how you want to create and retain instances of the contacts.core.Contacts(context) API. For more info, read Contacts API Setup . It is also useful to read about API Entities . Quick Start \u00b6 To retrieve all contacts containing all available contact data, val contacts = Contacts ( context ). query (). find () To simply search for Contacts, yielding the exact same results as the native Contacts app, val contacts = Contacts ( context ) . broadQuery () . whereAnyContactDataPartiallyMatches ( searchText ) . find () \u2139\ufe0f For more info, read Query contacts . Something a bit more advanced... To retrieve the first 5 contacts (including only the contact id, display name, and phone numbers in the results) ordered by display name in descending order, matching ALL of these rules; - a first name starting with \"leo\" - has emails from gmail or hotmail - lives in the US - has been born prior to making this query - is favorited (starred) - has a nickname of \"DarEdEvil\" (case sensitive) - works for Facebook - has a note - belongs to the account of \"john.doe@gmail.com\" or \"john.doe@myspace.com\" val contacts = Contacts ( context ) . query () . where { ( Name . GivenName startsWith \"leo\" ) and ( Email . Address { endsWith ( \"gmail.com\" ) or endsWith ( \"hotmail.com\" ) }) and ( Address . Country equalToIgnoreCase \"us\" ) and ( Event { ( Date lessThan Date (). toWhereString ()) and ( Type equalTo EventEntity . Type . BIRTHDAY ) }) and ( Contact . Options . Starred equalTo true ) and ( Nickname . Name equalTo \"DarEdEvil\" ) and ( Organization . Company `in` listOf ( \"facebook\" , \"FB\" )) and ( Note . Note . isNotNullOrEmpty ()) } . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" ), Account ( \"john.doe@myspace.com\" , \"com.myspace\" ), ) . include { setOf ( Contact . Id , Contact . DisplayNamePrimary , Phone . Number ) } . orderBy ( ContactsFields . DisplayNamePrimary . desc ()) . offset ( 0 ) . limit ( 5 ) . find () \u2139\ufe0f For more info, read Query contacts (advanced) . Once you have the contacts, you now have access to all of their data! val contact : Contact Log . d ( \"Contact\" , \"\"\" ID: ${ contact . id } Lookup Key: ${ contact . lookupKey } Display name: ${ contact . displayNamePrimary } Display name alt: ${ contact . displayNameAlt } Photo Uri: ${ contact . photoUri } Thumbnail Uri: ${ contact . photoThumbnailUri } Last updated: ${ contact . lastUpdatedTimestamp } Starred?: ${ contact . options ?. starred } Send to voicemail?: ${ contact . options ?. sendToVoicemail } Ringtone: ${ contact . options ?. customRingtone } Aggregate data from all RawContacts of the contact ----------------------------------- Addresses: ${ contact . addressList () } Emails: ${ contact . emailList () } Events: ${ contact . eventList () } Group memberships: ${ contact . groupMembershipList () } IMs: ${ contact . imList () } Names: ${ contact . nameList () } Nicknames: ${ contact . nicknameList () } Notes: ${ contact . noteList () } Organizations: ${ contact . organizationList () } Phones: ${ contact . phoneList () } Relations: ${ contact . relationList () } SipAddresses: ${ contact . sipAddressList () } Websites: ${ contact . websiteList () } ----------------------------------- \"\"\" . trimIndent () // There are also aggregate data functions that return a sequence instead of a list. ) \u2139\ufe0f For more info, read about API Entities . More than enough APIs that will allow you to build your own contacts app! \u00b6 This library is capable of doing more than just querying contacts. Actually, you can build your own full-fledged contacts app with it! Let's take a look at a few other APIs this library provides... To get the first 20 gmail emails ordered by email address in descending order, val emails = Contacts ( context ) . data () . query () . emails () . where { Email . Address endsWith \"gmail.com\" } . orderBy ( Fields . Email . Address . desc ( ignoreCase = true )) . offset ( 0 ) . limit ( 20 ) . find () It's not just for emails. It's for all data kinds (including custom data). \u2139\ufe0f For more info, read Query specific data kinds . To CREATE/INSERT a contact with a name of \"John Doe\" who works at Amazon with a work email of \"john.doe@amazon.com\" (in Kotlin), val insertResult = Contacts ( context ) . insert () . rawContacts ( NewRawContact (). apply { name = NewName (). apply { givenName = \"John\" familyName = \"Doe\" } organization = NewOrganization (). apply { company = \"Amazon\" title = \"Superstar\" } emails . add ( NewEmail (). apply { address = \"john.doe@amazon.com\" type = EmailEntity . Type . WORK }) }) . commit () Or alternatively, in a more Kotlinized style using named arguments, val insertResult = Contacts ( context ) . insert () . rawContacts ( NewRawContact ( name = NewName ( givenName = \"John\" , familyName = \"Doe\" ), organization = NewOrganization ( company = \"Amazon\" , title = \"Superstar\" ), emails = mutableListOf ( NewEmail ( address = \"john.doe@amazon.com\" , type = EmailEntity . Type . WORK )) )) . commit () Or alternatively, using extension functions, val insertResult = Contacts ( context ) . insert () . rawContact { setName { givenName = \"John\" familyName = \"Doe\" } setOrganization { company = \"Amazon\" title = \"Superstar\" } addEmail { address = \"john.doe@amazon.com\" type = EmailEntity . Type . WORK } } . commit () \u2139\ufe0f For more info, read Insert contacts . If John Doe switches jobs and heads over to Microsoft, we can UPDATE his data, Contacts ( context ) . update () . contacts ( johnDoe . mutableCopy { setOrganization { company = \"Microsoft\" title = \"Newb\" } emails (). first (). apply { address = \"john.doe@microsoft.com\" } }) . commit () \u2139\ufe0f For more info, read Update contacts . If we no longer like John Doe, we can DELETE him from our life, Contacts ( context ) . delete () . contacts ( johnDoe ) . commit () \u2139\ufe0f For more info, read Delete Contacts . Threading and permissions \u00b6 This library provides Kotlin coroutine extensions in the permissions module for all API functions to handle permissions and async module for executing work in background threads. launch { val contacts = Contacts ( context ) . queryWithPermission () ... . findWithContext () val deferredResult = Contacts ( context ) . insertWithPermission () ... . commitAsync () val result = deferredResult . await () } \u2139\ufe0f For more info, read Permissions handling using coroutines and Execute work outside of the UI thread using coroutines . So, if we call the above function and we don't yet have permission. The user will be prompted to give the appropriate permissions before the query proceeds. Then, the work is done in the coroutine context of choice (default is Dispatchers.IO). If the user does not give permission, the query will return no results. \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes. Full documentation, guides, and samples \u00b6 The above examples barely scratches the surface of what this library provides. For more in-depth documentation, visit the GitHub Pages . For a sample app reference, take a look at and run the sample module. All APIs in the library are optimized! \u00b6 Some other APIs or util functions out there typically perform one internal database query per contact returned. They do this to fetch the data per contact. This means that if there are 1,000 matching contacts, then an extra 1,000 internal database queries are performed! This is not cool! To address this issue, the query APIs provided in the Contacts, Reborn library, perform only at least two and at most six or seven internal database queries no matter how many contacts are matched! Even if there are 100,000 contacts matched, the library will only perform two to seven internal database queries (depending on your query parameters). Of course, if you don't want to fetch all hundreds of thousands of contacts, the query APIs support pagination with limit and offset functions :sunglasses: Cancellations are also supported! To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val contacts = query . find { ! isActive } } // Or, using the coroutine extensions in the async module... val contacts = query . findWithContext () } All core APIs are framework-agnostic and works well with Java and Kotlin \u00b6 The API does not and will not force you to use any frameworks (e.g. RxJava or Coroutines/Flow)! All core functions of the API live in the core module, which you can import to your project all by itself. Don't believe me? Take a look at the dependencies in the core/build.gradle :D So, feel free to use the core API however you want with whatever libraries or frameworks you want, such as Reactive, Coroutines/Flow, AsyncTask (hope not), WorkManager, and whatever permissions handling APIs you want to use. All other modules in this library are optional and are just there for your convenience or for reference. I also made sure that all core functions and entities are interoperable with Java. So, if you were wondering why I\u2019m using a semi-builder pattern instead of using named arguments with default values, that is why. I\u2019ve also made some other intentional decisions about API design to ensure the best possible experience for both Kotlin and Java consumers without sacrificing Kotlin language standards. It is Kotlin-first, Java-second (with love and care). \u26a0\ufe0f Modules other than the core module are not guaranteed to be compatible with Java. Requirements \u00b6 Min SDK 19+ Proguard \u00b6 If you use Proguard and the async and/or permissions , you may need to add rules for Coroutines . License \u00b6 Copyright 2022 Contacts Contributors Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.","title":"Overview"},{"location":"#android-contacts-reborn","text":"\u2139\ufe0f Written with \u2665\ufe0f and \ud83d\udd25 since December 2018. Open sourced since October 2021. This library provides a complete set of APIs to do everything you need with Contacts in Android. You no longer have to deal with the Contacts Provider , database operations, and cursors. Whether you just need to get all or some Contacts for a small part of your app (written in Kotlin or Java), or you are looking to create your own full-fledged Contacts app with the same capabilities as the native (AOSP) Android Contacts app and Google Contacts app, this library is for you! Please help support this project \ud83d\ude4f\u2764\ufe0f\u2b50\ufe0f","title":"Android Contacts, Reborn"},{"location":"#quick-links","text":"\ud83d\udcdc Documentation \ud83d\ude89 Current release - 0.2.0 \ud83d\ude82 Upcoming release - v0.3.0 \ud83d\uddfa Project roadmap \ud83d\udc8c Why use this library?","title":"Quick links"},{"location":"#features","text":"The core module provides, \u2705 All data kinds in the Contacts Provider; address, email, event, group membership, IM, name, nickname, note, organization, phone, photo, relation, SIP address, and website . \u2705 Custom data integration . \u2705 Broad queries and advanced queries of Contacts and RawContacts from zero or more Accounts and/or Groups. \u2705 Contact lookup keys \u2705 Include only desired fields in read/write operations to optimize CPU and memory . \u2705 Powerful, type-safe query DSL . \u2705 Pagination using order by, limit, and offset database functions. \u2705 Insert one or more RawContacts with an associated Account, causing automatic insertion of a new Contact subject to automatic aggregation by the Contacts Provider. \u2705 Update one or more Contacts, RawContacts, and Data. \u2705 Delete one or more Contacts, RawContacts, and Data. \u2705 Query , insert , update , and delete Profile (device owner) Contact, RawContact, and Data. \u2705 Query , insert , update , and delete Groups . \u2705 Query , insert update , and delete specific kinds of data . \u2705 Query , insert , update , and delete custom data . \u2705 Query , insert , and delete Blocked Numbers . \u2705 Query , insert , update , and delete SIM card contacts . \u2705 Query for Accounts in the system or RawContacts table. \u2705 Query for just RawContacts. \u2705 Associate local RawContacts (no Account) to an Account . \u2705 Link/unlink two or more Contacts. \u2705 Get/set contact options ; starred (favorite), custom ringtone, send to voicemail . \u2705 Get/set Contacts/RawContact photo and thumbnail . \u2705 Get/set default (primary) Contact Data (e.g. default/primary phone number, email, etc). \u2705 Convenience functions . \u2705 Contact data is synced automatically across devices . \u2705 Support for logging API input and output \u2705 Redactable entities and API input and output for production-safe logging that upholds user data privacy laws to meet GDPR guidelines (this is not legal advice) . \u2705 Full in-depth documentation/guides . \u2705 Full Java interoptibilty . \u2705 Core APIs have zero dependency . \u2705 Clean separation between Contacts vs RawContacts . \u2705 Clear distinction between truly deeply immutable, mutable, new, and existing entities allowing for thread safety and JetPack compose optimizations . There are also extensions that add functionality to every core function, \ud83e\uddf0 Asynchronous work using Kotlin Coroutines . \ud83e\uddf0 Permissions request/handling using Kotlin Coroutines . \ud83d\udd1c Kotlin Flow extensions \ud83d\udd1c RxJava extensions Also included are some pre-baked goodies to be used as is or just for reference, \ud83c\udf6c Gender custom data . \ud83c\udf6c Google Contacts custom data . \ud83c\udf6c Handle name custom data . \ud83c\udf6c Pokemon custom data \ud83c\udf6c Role Playing Game (RPG) custom data . \ud83c\udf6c Rudimentary contacts-integrated UI components . \ud83c\udf6c Debug functions to aid in development There are also more features that are on the way! \u2622\ufe0f Work profile contacts \u2622\ufe0f Dynamically integrate custom data from other apps \u2622\ufe0f Read/write from/to .VCF file .","title":"Features"},{"location":"#installation","text":"\u2139\ufe0f This library is a multi-module project published with JitPack First, include JitPack in the repositories list, repositories { maven { url \"https://jitpack.io\" } } To install individual modules, dependencies { implementation 'com.github.vestrel00.contacts-android:core:0.2.0' implementation 'com.github.vestrel00.contacts-android:async:0.2.0' implementation 'com.github.vestrel00.contacts-android:customdata-gender:0.2.0' implementation 'com.github.vestrel00.contacts-android:customdata-googlecontacts:0.2.0' implementation 'com.github.vestrel00.contacts-android:customdata-handlename:0.2.0' implementation 'com.github.vestrel00.contacts-android:customdata-pokemon:0.2.0' implementation 'com.github.vestrel00.contacts-android:customdata-rpg:0.2.0' implementation 'com.github.vestrel00.contacts-android:debug:0.2.0' implementation 'com.github.vestrel00.contacts-android:permissions:0.2.0' implementation 'com.github.vestrel00.contacts-android:test:0.2.0' implementation 'com.github.vestrel00.contacts-android:ui:0.2.0' // Notice that when importing specific modules/subprojects, the first \":\" comes after \"contacts-android\". } The core module is really all you need. All other modules are optional. To install all modules in a single line, dependencies { implementation 'com.github.vestrel00:contacts-android:0.2.0' // Notice that when importing all modules, the first \":\" comes after \"vestrel00\". } \u26a0\ufe0f Starting with version 0.2.0, installing all modules in a single line is only supported when using the dependencyResolutionManagement in settings.gradle . You are still able to install all modules by specifying them individually. For more info about the different modules and dependency resolution management, read the Installation guide .","title":"Installation"},{"location":"#setup","text":"There is no setup required. It's up to you how you want to create and retain instances of the contacts.core.Contacts(context) API. For more info, read Contacts API Setup . It is also useful to read about API Entities .","title":"Setup"},{"location":"#quick-start","text":"To retrieve all contacts containing all available contact data, val contacts = Contacts ( context ). query (). find () To simply search for Contacts, yielding the exact same results as the native Contacts app, val contacts = Contacts ( context ) . broadQuery () . whereAnyContactDataPartiallyMatches ( searchText ) . find () \u2139\ufe0f For more info, read Query contacts . Something a bit more advanced... To retrieve the first 5 contacts (including only the contact id, display name, and phone numbers in the results) ordered by display name in descending order, matching ALL of these rules; - a first name starting with \"leo\" - has emails from gmail or hotmail - lives in the US - has been born prior to making this query - is favorited (starred) - has a nickname of \"DarEdEvil\" (case sensitive) - works for Facebook - has a note - belongs to the account of \"john.doe@gmail.com\" or \"john.doe@myspace.com\" val contacts = Contacts ( context ) . query () . where { ( Name . GivenName startsWith \"leo\" ) and ( Email . Address { endsWith ( \"gmail.com\" ) or endsWith ( \"hotmail.com\" ) }) and ( Address . Country equalToIgnoreCase \"us\" ) and ( Event { ( Date lessThan Date (). toWhereString ()) and ( Type equalTo EventEntity . Type . BIRTHDAY ) }) and ( Contact . Options . Starred equalTo true ) and ( Nickname . Name equalTo \"DarEdEvil\" ) and ( Organization . Company `in` listOf ( \"facebook\" , \"FB\" )) and ( Note . Note . isNotNullOrEmpty ()) } . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" ), Account ( \"john.doe@myspace.com\" , \"com.myspace\" ), ) . include { setOf ( Contact . Id , Contact . DisplayNamePrimary , Phone . Number ) } . orderBy ( ContactsFields . DisplayNamePrimary . desc ()) . offset ( 0 ) . limit ( 5 ) . find () \u2139\ufe0f For more info, read Query contacts (advanced) . Once you have the contacts, you now have access to all of their data! val contact : Contact Log . d ( \"Contact\" , \"\"\" ID: ${ contact . id } Lookup Key: ${ contact . lookupKey } Display name: ${ contact . displayNamePrimary } Display name alt: ${ contact . displayNameAlt } Photo Uri: ${ contact . photoUri } Thumbnail Uri: ${ contact . photoThumbnailUri } Last updated: ${ contact . lastUpdatedTimestamp } Starred?: ${ contact . options ?. starred } Send to voicemail?: ${ contact . options ?. sendToVoicemail } Ringtone: ${ contact . options ?. customRingtone } Aggregate data from all RawContacts of the contact ----------------------------------- Addresses: ${ contact . addressList () } Emails: ${ contact . emailList () } Events: ${ contact . eventList () } Group memberships: ${ contact . groupMembershipList () } IMs: ${ contact . imList () } Names: ${ contact . nameList () } Nicknames: ${ contact . nicknameList () } Notes: ${ contact . noteList () } Organizations: ${ contact . organizationList () } Phones: ${ contact . phoneList () } Relations: ${ contact . relationList () } SipAddresses: ${ contact . sipAddressList () } Websites: ${ contact . websiteList () } ----------------------------------- \"\"\" . trimIndent () // There are also aggregate data functions that return a sequence instead of a list. ) \u2139\ufe0f For more info, read about API Entities .","title":"Quick Start"},{"location":"#more-than-enough-apis-that-will-allow-you-to-build-your-own-contacts-app","text":"This library is capable of doing more than just querying contacts. Actually, you can build your own full-fledged contacts app with it! Let's take a look at a few other APIs this library provides... To get the first 20 gmail emails ordered by email address in descending order, val emails = Contacts ( context ) . data () . query () . emails () . where { Email . Address endsWith \"gmail.com\" } . orderBy ( Fields . Email . Address . desc ( ignoreCase = true )) . offset ( 0 ) . limit ( 20 ) . find () It's not just for emails. It's for all data kinds (including custom data). \u2139\ufe0f For more info, read Query specific data kinds . To CREATE/INSERT a contact with a name of \"John Doe\" who works at Amazon with a work email of \"john.doe@amazon.com\" (in Kotlin), val insertResult = Contacts ( context ) . insert () . rawContacts ( NewRawContact (). apply { name = NewName (). apply { givenName = \"John\" familyName = \"Doe\" } organization = NewOrganization (). apply { company = \"Amazon\" title = \"Superstar\" } emails . add ( NewEmail (). apply { address = \"john.doe@amazon.com\" type = EmailEntity . Type . WORK }) }) . commit () Or alternatively, in a more Kotlinized style using named arguments, val insertResult = Contacts ( context ) . insert () . rawContacts ( NewRawContact ( name = NewName ( givenName = \"John\" , familyName = \"Doe\" ), organization = NewOrganization ( company = \"Amazon\" , title = \"Superstar\" ), emails = mutableListOf ( NewEmail ( address = \"john.doe@amazon.com\" , type = EmailEntity . Type . WORK )) )) . commit () Or alternatively, using extension functions, val insertResult = Contacts ( context ) . insert () . rawContact { setName { givenName = \"John\" familyName = \"Doe\" } setOrganization { company = \"Amazon\" title = \"Superstar\" } addEmail { address = \"john.doe@amazon.com\" type = EmailEntity . Type . WORK } } . commit () \u2139\ufe0f For more info, read Insert contacts . If John Doe switches jobs and heads over to Microsoft, we can UPDATE his data, Contacts ( context ) . update () . contacts ( johnDoe . mutableCopy { setOrganization { company = \"Microsoft\" title = \"Newb\" } emails (). first (). apply { address = \"john.doe@microsoft.com\" } }) . commit () \u2139\ufe0f For more info, read Update contacts . If we no longer like John Doe, we can DELETE him from our life, Contacts ( context ) . delete () . contacts ( johnDoe ) . commit () \u2139\ufe0f For more info, read Delete Contacts .","title":"More than enough APIs that will allow you to build your own contacts app!"},{"location":"#threading-and-permissions","text":"This library provides Kotlin coroutine extensions in the permissions module for all API functions to handle permissions and async module for executing work in background threads. launch { val contacts = Contacts ( context ) . queryWithPermission () ... . findWithContext () val deferredResult = Contacts ( context ) . insertWithPermission () ... . commitAsync () val result = deferredResult . await () } \u2139\ufe0f For more info, read Permissions handling using coroutines and Execute work outside of the UI thread using coroutines . So, if we call the above function and we don't yet have permission. The user will be prompted to give the appropriate permissions before the query proceeds. Then, the work is done in the coroutine context of choice (default is Dispatchers.IO). If the user does not give permission, the query will return no results. \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes.","title":"Threading and permissions"},{"location":"#full-documentation-guides-and-samples","text":"The above examples barely scratches the surface of what this library provides. For more in-depth documentation, visit the GitHub Pages . For a sample app reference, take a look at and run the sample module.","title":"Full documentation, guides, and samples"},{"location":"#all-apis-in-the-library-are-optimized","text":"Some other APIs or util functions out there typically perform one internal database query per contact returned. They do this to fetch the data per contact. This means that if there are 1,000 matching contacts, then an extra 1,000 internal database queries are performed! This is not cool! To address this issue, the query APIs provided in the Contacts, Reborn library, perform only at least two and at most six or seven internal database queries no matter how many contacts are matched! Even if there are 100,000 contacts matched, the library will only perform two to seven internal database queries (depending on your query parameters). Of course, if you don't want to fetch all hundreds of thousands of contacts, the query APIs support pagination with limit and offset functions :sunglasses: Cancellations are also supported! To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val contacts = query . find { ! isActive } } // Or, using the coroutine extensions in the async module... val contacts = query . findWithContext () }","title":"All APIs in the library are optimized!"},{"location":"#all-core-apis-are-framework-agnostic-and-works-well-with-java-and-kotlin","text":"The API does not and will not force you to use any frameworks (e.g. RxJava or Coroutines/Flow)! All core functions of the API live in the core module, which you can import to your project all by itself. Don't believe me? Take a look at the dependencies in the core/build.gradle :D So, feel free to use the core API however you want with whatever libraries or frameworks you want, such as Reactive, Coroutines/Flow, AsyncTask (hope not), WorkManager, and whatever permissions handling APIs you want to use. All other modules in this library are optional and are just there for your convenience or for reference. I also made sure that all core functions and entities are interoperable with Java. So, if you were wondering why I\u2019m using a semi-builder pattern instead of using named arguments with default values, that is why. I\u2019ve also made some other intentional decisions about API design to ensure the best possible experience for both Kotlin and Java consumers without sacrificing Kotlin language standards. It is Kotlin-first, Java-second (with love and care). \u26a0\ufe0f Modules other than the core module are not guaranteed to be compatible with Java.","title":"All core APIs are framework-agnostic and works well with Java and Kotlin"},{"location":"#requirements","text":"Min SDK 19+","title":"Requirements"},{"location":"#proguard","text":"If you use Proguard and the async and/or permissions , you may need to add rules for Coroutines .","title":"Proguard"},{"location":"#license","text":"Copyright 2022 Contacts Contributors Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.","title":"License"},{"location":"contributing/","text":"Contributing \u00b6 There are only a few loose guidelines to follow. Read the Setup and Guidelines sections. Setup \u00b6 To open, build, and run this project, you will need to use Android Studio Bumblebee 2021.1.1 and later versions. If you run into any build issues, open up Android Studio preferences and make sure the following is set correctly... Build, Execution, Deployment -> Build Tools -> Gradle Use Gradle from: 'gradlew-wrapper.properties' file Gradle JDK: Embedded JDK version 11.x.x Language & Frameworks -> Kotlin Current Kotlin plugin version: 211-1.6.10-release-923-AS7442.40 Restart Android Studio, clean, build, and invalidate caches & restart. Guidelines \u00b6 Simple is better. Over-engineering is not welcome here. Don't over complicate function implementations unnecessarily, especially the public API. Less is more. If you have not noticed yet, the dependency list of this project is almost non-existent. The core module only depends on Kotlin's standard library. Not even the support annotations are included (though this is questionable and may change quickly). All modules only have dependencies on essentials. Nice-to-haves are excluded. Contacts have been here since API 1. In that spirit, we should not need to import tons of unnecessary dependencies to deliver the most basic Android API. Java compatibility is a must. Java is not dead even in Android, though it may seem like it. There are still probably a lot of people that have not migrated over to Kotlin. This is especially true for larger organizations with large code bases and unable to afford migrating to Kotlin. The API must be usable in Java, with exceptions to Kotlin-specific modules (e.g. async, permissions). Be patient. Early on, I (Vandolf) will be the only one to approve incoming code. It may take a few days for me to review code and decline/approve. I have a full time job after all =) As time passes, I'm hoping to give the power of approvals to others in the community. Uphold the spirit of Contacts, Reborn! Don't deviate from the existing API design. New code should follow existing API design to promote uniformity. It'll be easier to maintain and cross-pollinate.","title":"Contributing"},{"location":"contributing/#contributing","text":"There are only a few loose guidelines to follow. Read the Setup and Guidelines sections.","title":"Contributing"},{"location":"contributing/#setup","text":"To open, build, and run this project, you will need to use Android Studio Bumblebee 2021.1.1 and later versions. If you run into any build issues, open up Android Studio preferences and make sure the following is set correctly... Build, Execution, Deployment -> Build Tools -> Gradle Use Gradle from: 'gradlew-wrapper.properties' file Gradle JDK: Embedded JDK version 11.x.x Language & Frameworks -> Kotlin Current Kotlin plugin version: 211-1.6.10-release-923-AS7442.40 Restart Android Studio, clean, build, and invalidate caches & restart.","title":"Setup"},{"location":"contributing/#guidelines","text":"Simple is better. Over-engineering is not welcome here. Don't over complicate function implementations unnecessarily, especially the public API. Less is more. If you have not noticed yet, the dependency list of this project is almost non-existent. The core module only depends on Kotlin's standard library. Not even the support annotations are included (though this is questionable and may change quickly). All modules only have dependencies on essentials. Nice-to-haves are excluded. Contacts have been here since API 1. In that spirit, we should not need to import tons of unnecessary dependencies to deliver the most basic Android API. Java compatibility is a must. Java is not dead even in Android, though it may seem like it. There are still probably a lot of people that have not migrated over to Kotlin. This is especially true for larger organizations with large code bases and unable to afford migrating to Kotlin. The API must be usable in Java, with exceptions to Kotlin-specific modules (e.g. async, permissions). Be patient. Early on, I (Vandolf) will be the only one to approve incoming code. It may take a few days for me to review code and decline/approve. I have a full time job after all =) As time passes, I'm hoping to give the power of approvals to others in the community. Uphold the spirit of Contacts, Reborn! Don't deviate from the existing API design. New code should follow existing API design to promote uniformity. It'll be easier to maintain and cross-pollinate.","title":"Guidelines"},{"location":"dev-notes/","text":"Developer Notes \u00b6 This document contains useful developer notes that should be kept in mind during development. It serves as a memory of all the quirks and gotcha's of things like Android's ContactsContract . This is only meant to be read by contributors of this library, not consumers! Contacts Provider / ContactsContract \u00b6 It is important to know about the ins and outs of Android's Contacts Provider. After all, this API is just a wrapper around it. It is important to get familiar with the official documentation of the Contact's Provider . Here is a summary; There are 3 main database tables used in dealing with contacts; Contacts RawContacts Data \u2139\ufe0f There are more but that is covered later. All of these tables and their fields are enumerated and documented in android.provider.ContactsContract . Each table serves a different purpose; Contacts Rows representing different people. RawContacts Rows that link Contacts rows to specific Accounts. Data Rows containing data (e.g. name, email) for a RawContacts row. These tables contain the following (notable) information (columns); Contacts _ID DISPLAY_NAME_PRIMARY RawContacts _ID : the Contacts._ID ACCOUNT_NAME : the Account.name ACCOUNT_TYPE the Account.type Data RAW_CONTACT_ID : the RawContacts._ID CONTACT_ID : the Contacts._ID DATA_1 to DATA_15 : contains a piece of contact data (e.g. first and last name, email address and type) determined by the MIMETYPE MIMETYPE : the type of data that this row's DATA_X columns contain (e.g. name and email data) The tables are connected the following way; RawContacts contains a reference to the Contacts row Id. Data contains a reference to the RawContacts row Id and Contacts row Id. Contacts; Display Name \u00b6 The Contacts.DISPLAY_NAME name may be different than the Data StructuredName display name! If a structured name in the Data table is not provided, then other kinds of data will be used as the Contacts row display name. For example, if an email is provided but no structured name then the display name will be the email. When a structured name is inserted, the Contacts Provider automatically updates the Contacts row display name. \u2139\ufe0f In the case of StructuredName , the Contacts.DISPLAY_NAME is made up of the prefix, given, middle, family name, and suffix and not the unstructured display name. If no data rows suitable to be a display name are available, then the Contacts row display name will be null. Data suitable to be a Contacts row display name are enumerated in DisplayNameSources ; email nickname organization phone number structured name Data not suitable to be display names are; address event group im note relation sip website The kind of data used as the display for the Contact is set in ContactNameColumns.DISPLAY_NAME_SOURCE . A note about StructuredName There may be a scenario where the unstructured StructuredName.DISPLAY_NAME does not match the structured components. Such scenarios are possible but is considered incorrect. For example, it is possible to programmatically set the display name to \"Ice Cold\" but set the given and family name to \"Hot Fire\". The Contacts.DISPLAY_NAME is made up of the prefix, given, middle, family name, and suffix (\"Hot Fire\") and not the unstructured display name. The Contacts Provider's general matching algorithm does not include the Contacts.DISPLAY_NAME . However, the StructuredName.DISPLAY_NAME is included in the matching process but not the rest of the structured components (e.g. given and family name). The native Contacts app displays the Contacts.DISPLAY_NAME . So, here comes the unusual scenario that looks like a bug. The general matching algorithm will match the text \"Ice\" or \"Cold\" but not \"Hot\" or \"Fire\". The end result is that searching for the Contact \"Ice Cold\" will show a Contact called \"Hot Fire\"! Contact Display Name and Default Name Rows \u00b6 If available, the \"default\" (isPrimary and isSuperPrimary set to 1) name row for a Contact is automatically set as the Contact display name by the Contacts Provider. Otherwise, the Contacts Provider chooses from any of the other suitable data from the aggregate Contact. \u2139\ufe0f The ContactsColumns.NAME_RAW_CONTACT_ID is automatically updated by the Contacts Provider along with the display name. The default status of other sources (e.g. email) does not affect the Contact display name. The native Contacts app also sets the most recently updated name as the default at every update. This results in the Contact display name changing to the most recently updated name from one of the associated RawContacts. The \"most recently updated name\" is the name field that was last updated by the user when editing in the Contacts app, which is irrelevant to its value. It does not matter if the user deleted the last character of the name, added the same character back, and then saved. It still counts as the most recently updated. All of the above only applies to API 21 and above. Display name resolution is different for APIs below 21 (pre-Lollipop)! The ContactsColumns.NAME_RAW_CONTACT_ID was added in API 21. It changed the way display names are resolved for Contacts with more than one constituent RawContacts, which is what has been described so far. Before this change (APIs 20 and below), the native Contacts app is still able to set the Contact display name somehow. I'm not sure how. If someone figures it out, please let me know. I tried updating the Contact DISPLAY_NAME directly but it does not work. Setting a name row as default also does not affect the Contact DISPLAY_NAME . Contacts; ID vs LOOKUP_KEY \u00b6 The Contacts._ID is the unique identifier for the row in the Contacts table. The Contacts.LOOKUP_KEY is the unique identifier for an aggregate Contact (a person). The _ID may change due to aggregation and sync. The same goes for the LOOKUP_KEY but unlike the ID it may still be used to find the aggregate contact. Unlike the Contact ID, the lookup key is the same across devices (for contacts that are associated with an Account and are synced). \u2139\ufe0f I did the following investigation with a much larger data set. I simplified it here for brevity. Let's take a look at the following Contacts and RawContacts table rows, #### Contacts table Contact id: 55, lookupKey: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50, displayNamePrimary: Contact With Local RawContact Contact id: 56, lookupKey: 2059i6f5de8460f7f227e, displayNamePrimary: Contact With Synced RawContact #### RawContacts table RawContact id: 55, contactId: 55, displayNamePrimary: Contact With Local RawContact RawContact id: 56, contactId: 56, displayNamePrimary: Contact With Synced RawContact There are two Contacts each having one RawContact. Notice that the lookup keys are a bit different. Contact With Local RawContact: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 Contact With Synced RawContact: 2059i6f5de8460f7f227e The Contact with unsynced, device-only, local RawContact has a much longer (or shorter e.g. 0r62-2A2C2E) lookup key and starts with \"0r -\" and all characters after it are in uppercase. The other thing to notice is that the \"55\" in \"0r55-\" seems to be the same as the RawContact ID (I did a bit more experiments than what is written in these notes to confirm that it is indeed the RawContact ID and not the Contact ID). We probably don't need to worry about these details though the Contacts Provider probably uses these things internally. We also should not rely on it. However, it may be safe to assume that the Contact lookup key is a reference to a RawContact (or reference to more than one constituent RawContact when multiple RawContacts are linked). Again, an internal Contacts Provider detail we should not rely on BUT is probably relevant when implementing sync adapters. When we link the two, we get... Contact id: 55, lookupKey: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50.2059i6f5de8460f7f227e, displayNamePrimary: Contact With Synced RawContact #### RawContacts table RawContact id: 55, contactId: 55, displayNamePrimary: Contact With Local RawContact RawContact id: 56, contactId: 55, displayNamePrimary: Contact With Synced RawContact Notice, Contact with ID 56 has been deleted. Contact with ID 55 still exist with the lookup keys of both Contact 55 and 56 combined separated by a \".\" . This holds true in cases where two or more local-only or non-local-only RawContacts are linked. RawContacts remain unchanged except reference to Contact 56 has been replaced with 55. The primary display name of Contact 55 came from RawContact 55 prior to the link and now comes from RawContact 56 after the link. This primary name resolution is probably irrelevant so pay no attention to it. The most important part to notice is that the lookup keys get combined. The lookup uri is required to build a Contacts.CONTENT_LOOKUP_URI ... /** * A content:// style URI for this table that should be used to create * shortcuts or otherwise create long-term links to contacts. This URI * should always be followed by a \"/\" and the contact's {@link #LOOKUP_KEY}. * It can optionally also have a \"/\" and last known contact ID appended after * that. This \"complete\" format is an important optimization and is highly recommended. *

        * As long as the contact's row ID remains the same, this URI is * equivalent to {@link #CONTENT_URI}. If the contact's row ID changes * as a result of a sync or aggregation, this URI will look up the * contact using indirect information (sync IDs or constituent raw * contacts). *

        * Lookup key should be appended unencoded - it is stored in the encoded * form, ready for use in a URI. */ public static final Uri CONTENT_LOOKUP_URI = Uri . withAppendedPath ( CONTENT_URI , \"lookup\" ); /** * Build a {@link #CONTENT_LOOKUP_URI} lookup {@link Uri} using the * given {@link ContactsContract.Contacts#_ID} and {@link #LOOKUP_KEY}. *

        * Returns null if unable to construct a valid lookup URI from the * provided parameters. */ public static Uri getLookupUri ( long contactId , String lookupKey ) { if ( TextUtils . isEmpty ( lookupKey )) { return null ; } return ContentUris . withAppendedId ( Uri . withAppendedPath ( Contacts . CONTENT_LOOKUP_URI , lookupKey ), contactId ); } From the lookup uri, we can lookup the Contact row... public static Uri lookupContact ( ContentResolver resolver , Uri lookupUri ) { ... } Or simply get the Contact ID... // code inside `public static Uri lookupContact` resolver . query ( lookupUri , new String [] { Contacts . _ID }, null , null , null ) However, given that the lookup key of the deleted Contact 56 still lives on, it is possible to get the linked Contact 55 using the lookup key of Contact 56 using our standard query APIs! . where { Contact . LookupKey contains lookupKey } The above is correct as long as these assumptions hold true; the lookup key is unique there is no lookup key that can contain a shorter lookup key the Contact ID fails this test because a smaller number is contained in a larger number synced contacts have shorter lookup keys than local contacts. However, local contacts' lookup keys are capitalized whereas synced contact are not. Also, there seems to be other differences in pattern between long and short lookup keys. It should be safe to make this assumption. Until the community finds that this assumption is flawed, we'll assume that it is true! For now, we can avoid having to create another API or extensions just for using lookup keys . When we unlink , we get... #### Contacts table Contact id: 55, lookupKey: 2059i6f5de8460f7f227e, displayNamePrimary: Contact With Synced RawContact Contact id: 58, lookupKey: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50, displayNamePrimary: Contact With Local RawContact #### RawContacts table RawContact id: 55, contactId: 58, displayNamePrimary: Contact With Local RawContact RawContact id: 56, contactId: 55, displayNamePrimary: Contact With Synced RawContact Notice, A new Contact row with ID of 58 is created. The lookup keys are separated and distributed between Contact 55 and 58. RawContact 55 Contact reference has been set to Contact 58. Let's compare the Contact-RawContact relationship before and after linking and then unlinking. Contact ID Lookup Key RawContact.Contact ID Before 55, 56 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50, 2059i6f5de8460f7f227e 55, 56 After 55, 58 2059i6f5de8460f7f227e, 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 58, 55 Notice, Contact ID 55 swapped lookup keys with the former Contact 56 (now 58). RawContact ID 55 swapped Contact reference with RawContact 56. The Contact IDs and lookup keys got shuffled BUT the Contact-RawContact relationship remains the same if using the lookup keys as point of reference! Here is another way to look at the table, using the lookup key as the constant... Lookup Key Before After 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 Contact 55, RawContact 55 Contact 58, RawContact 55 2059i6f5de8460f7f227e Contact 56, RawContact 56 Contact 55, RawContact 56 Notice that the indirect relationship between the lookup key and RawContacts remains the same before and after the link-unlink even though the Contact IDs changed. \u2139\ufe0f As mentioned earlier in this section, the \"55\" in \"0r55-\" seems to be referencing the RawContact ID. In other words, since local RawContacts are not synced or tracked in a remote database where Contacts -> RawContacts mappings exist, the Contacts Provider most likely uses this \"0r -\" pattern to make the connection. This is not really relevant for us as we are not relying on this mechanism. I'm just pointing out my observations, which could be incorrect. This means that... If users of this library saved a reference Contact ID 55, then a link-unlink (or sync adapter functions) occur. Getting Contact by ID 55 will result in the RawContact-Data of the former Contact 56 to be returned. This is a bug! Same goes if users saved a reference to Contact ID 56. If users of this library saved a reference to the lookup keys, then a link-unlink (or sync adapter functions) occur. Getting Contact by lookup key will result in the correct RawContact-Data to be returned. So when to use Contact ID vs lookup key? Lookup key: for a reference to a Contact that needs to be loaded after some period of time. Saving/restoring activity/fragment instance state. Saving to an external database, preferences, or files. Creating shortcuts. ID: for everything else. Performing read/write operations in the same function call or session in your app. Performing read/write operations that require ID (e.g. Contact photo and options). Another thing to check is what happens when associating a local RawContact to an Account (move from device to Account) and vice versa. Is the lookup key of the Contact affected? After associating the local RawContact to an Account... #### Contacts table Contact id: 58, lookupKey: 2059i4abd4a8f8ff89642 #### RawContacts table RawContact id: 55, contactId: 58 The lookup key changed but the Contact ID remained the same! In this case, loading a reference to the previously local Contact will fail! I verified that this is indeed the behavior of the native (AOSP) Contacts app. Moving the RawContact from device to Google using Google Contacts app while having Contact details activity opened in the AOSP Contacts app will result in \"error Contact does not exist\" message in the AOSP Contacts app! \u2139\ufe0f The RawContact and its Data also remained the same in this case. Removing the account from it results in... #### Contacts table Contact id: 59, lookupKey: 0r58-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 #### RawContacts table RawContact id: 58, contactId: 59 The Contact and RawContacts row have been deleted and new rows have been created to replace them! I also verified that the Data rows have also been deleted and new rows have been created to replace them! This stuff is not really relevant for lookup key but still good to know for implementing moving between accounts in the future. RawContacts; Accounts + Contacts \u00b6 The RawContacts table associates a person to an android.accounts.Account that it belongs to. Each new RawContacts row created results in; a new row in the Contacts table (unless the RawContact is associated to another existing Contact) a new row in the RawContacts with account name and type set to null 0 or more rows in the Data table with a reference to the new Contacts and RawContacts Ids \u2139\ufe0f It is possible to create RawContacts without any rows in the Data table. See the Data required section for more details. For example, creating 4 new contacts using the native Android Contacts app results in; Contact id: 4, displayName: First Local Contact Contact id: 5, displayName: Second Local Contact Contact id: 6, displayName: Third Local Contact Contact id: 7, displayName: Third Local Contact RawContact id: 4, accountName: null, accountType: null RawContact id: 5, accountName: null, accountType: null RawContact id: 6, accountName: null, accountType: null RawContact id: 7, accountName: null, accountType: null Data id: 15, rawContactId: 4, contactId: 4, data: First Local Contact Data id: 16, rawContactId: 5, contactId: 5, data: Second Local Contact Data id: 17, rawContactId: 6, contactId: 6, data: Third Local Contact Data id: 18, rawContactId: 7, contactId: 7, data: Third Local Contact Local Contacts / RawContacts RawContacts inserted without an associated account are considered local or device-only raw contacts, which are not synced. The native Contacts app hides the following UI fields when inserting or updating local raw contacts; - Event - Relation - Group memberships To enforce this behavior, this library ignores all of the above during inserts and updates for local raw contacts. Lollipop (API 22) and below When an Account is added, from a state where no accounts have yet been added to the system, the Contacts Provider automatically sets all of the null accountName and accountType in the RawContacts table to that Account's name and type; RawContact id: 4, accountName: vestrel00@gmail.com, accountType: com.google RawContact id: 5, accountName: vestrel00@gmail.com, accountType: com.google RawContact id: 6, accountName: vestrel00@gmail.com, accountType: com.google RawContact id: 7, accountName: vestrel00@gmail.com, accountType: com.google RawContacts inserted without an associated account will automatically get assigned to an account if there are any available. This may take a few seconds, whenever the Contacts Provider decides to do it. Dissociating RawContacts from Accounts will result in the Contacts Provider associating those back to an Account. Marshmallow (API 23) and above The Contacts Provider no longer associates local contacts to an account when an account is or becomes available. Local contacts remain local. Account removal Removing the Account will remove all of the associated rows in the local RawContact, Data, and Groups tables. This includes user Profile data in those tables. SyncColumns modifications This library supports modifying the SyncColumns.ACCOUNT_NAME and SyncColumns.ACCOUNT_TYPE of the RawContacts table in some cases only. In some cases does not work as intended and produces unwanted side-effects. It probably has something to do with syncing with remote servers and local Account / sync data not matching up similar to errors on network requests if the system time does not match network time. The motivation behind changing the Account columns of the RawContacts table rows is that it would allow users to; Associate local RawContacts (those that are not associated with an Account) to an Account, allowing syncing between devices. Dissociate RawContacts from their Account such that they remain local to the device and not synced between devices. Transfer RawContacts from one Account to another. When modifying the SyncColumns directly, the first works as intended. The second works with some unwanted side-effects. The third does not work at all and produces unwanted side-effects. These are the behaviors that I have found; Associating local RawContact A to Account X. Works as intended. RawContact A is now associated with Account X and is synced across devices. Dissociating RawContact A (setting the SyncColumns' Account name and type to null) from Account X. Partially works with some unwanted-side effects. Dissociates RawContact A from the device but not other devices. RawContact A is no longer visible in the native Contacts app UNLESS it retains the group membership to at least the default group from an Account. At this point, RawContact A is a local contact. Changes to this local RawContact A will not be synced across devices. If RawContact A is updated in another device and synced up to the server, then a syncing side-effect occurs because the RawContact A in the device is different from the RawContact A in the server. This causes the Contacts Provider to create another RawContact, resulting in a \"duplicate\". The two RawContact As may get aggregated to the same Contact depending on how similar they are. If local RawContact A is re-associated back to Account X, it will still no longer be synced. Associating RawContact A from original Account X to Account Y. Does not work and have bad side-effects. No change in other devices. For Lollipop (API 22) and below, RawContact A is no longer visible in the native Contacts app and syncing Account Y in system settings fails. For Marshmallow (API 23) and above, RawContact A is no longer visible in the native Contacts app. RawContact A is automatically deleted locally at some point by the Contacts Provider. Syncing Account Y in system settings succeeds. Given that associating originally local RawContacts to an Account is the only thing that actually works, it is the only function that will be exposed to consumers. If consumers want to transfer RawContacts from one Account to another, they can create a copy of a RawContact associated with the desired Account and then delete the original RawContact. Same idea can be used to transform an Account-associated RawContact to a local RawContact. Perhaps we can implement some functions in this library that does these things? We won't for now because the native Contacts app does not support these functions anyways. It can always be implemented later if the community really wants. Here are some other things to note. The Contacts Provider automatically creates a group membership to the default group of the target Account when the account changes. This occurs even if the group membership already exists resulting in duplicates. The Contacts Provider DOES NOT delete existing group memberships when the account changes. This has to be done manually to prevent duplicates. RawContacts; Deletion \u00b6 Deleting a contact's Contacts row, RawContacts row(s), and associated Data row(s) are best explained in the documentation in ContactsContract.RawContacts ; When a raw contact is deleted, all of its Data rows as well as StatusUpdates, AggregationExceptions, PhoneLookup rows are deleted automatically. When all raw contacts associated with a Contacts row are deleted, the Contacts row itself is also deleted automatically. The invocation of resolver.delete(...), does not immediately delete a raw contacts row. Instead, it sets the ContactsContract.RawContactsColumns.DELETED flag on the raw contact and removes the raw contact from its aggregate contact. The sync adapter then deletes the raw contact from the server and finalizes phone-side deletion by calling resolver.delete(...) again and passing the ContactsContract#CALLER_IS_SYNCADAPTER query parameter. Some sync adapters are read-only, meaning that they only sync server-side changes to the phone, but not the reverse. If one of those raw contacts is marked for deletion, it will remain on the phone. However it will be effectively invisible, because it will not be part of any aggregate contact. TLDR To delete a contacts and all associated rows, simply delete all RawContact rows with the desired Contacts id. Deletion of the Contacts row and associated Data row(s) will be done automatically by the Contacts Provider. Note that deleting a RawContacts row may not immediately delete the RawContacts row. In this case, it is marked as deleted and its reference to a contact id is nulled. The Contact may still exist if it still has at least one constituent RawContact that is not marked for deletion. \u2139\ufe0f A RawContact is marked for deletion as specified by RawContactsColumns.DELETED . Typically, deleting RawContacts immediately removes the row from the RawContacts table. However, RawContacts row remains and is simply marked for deletion UNTIL the sync adapters syncs the changes. One of the reasons syncs do not occur is when the system sync settings are turned off for the Account or there is no network connection. Such RawContacts should not be included in query results for Contacts. The AOSP and Google Contacts app also does not show them. Note that local RawContacts rows (not associated with an Account) are deleted immediately as no sync needs to occur. Multiple RawContacts Per Contact \u00b6 Each row in the Contacts table may be associated with more than one row in the RawContacts table. The Contacts Provider may consolidate multiple contacts belonging to different accounts and combine them into a single entry in the Contacts table whilst maintaining the separate entries in the RawContacts table. A more likely scenario that causes multiple RawContacts per Contact is when two or more Contacts are \"linked\" (or \"merged\" for API 23 and below, or \"joined\" for API 22 and below). Behavior of linking/merging/joining contacts (AggregationExceptions) \u00b6 The native Contacts app terminology has changed over time; API 22 and below; join / separate API 23; merge / unmerge API 24 and above; link / unlink However, the internals have not changed; KEEP_TOGETHER / KEEP_SEPARATE . These operations are supported by the ContactsContract.AggregationExceptions . For example, given the following tables, ### Contacts table Contact id: 32, displayName: X, starred: 0, timesContacted: 1, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 Contact id: 33, displayName: Y, starred: 1, timesContacted: 2, lastTimeContacted: 1573071750624, customRingtone: content://media/internal/audio/media/115, sendToVoicemail: 1 ### RawContacts table RawContact id: 30, contactId: 32, displayName: X, accountName: x@x.com, accountType: com.google, starred: 0, timesContacted: 1, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 RawContact id: 31, contactId: 33, displayName: Y, accountName: y@y.com, accountType: com.google, starred: 1, timesContacted: 2, lastTimeContacted: 1573071750624, customRingtone: content://media/internal/audio/media/115, sendToVoicemail: 1 ### Data table Data id: 57, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/group_membership, data1: 18 Data id: 58, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/name, data1: X, isPrimary: 1, isSuperPrimary: 1 Data id: 59, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: x@x.com Data id: 60, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: xx@x.com, isPrimary: 1, isSuperPrimary: 1 Data id: 63, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/group_membership, data1: 6 Data id: 64, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/name, data1: Y, isPrimary: 1, isSuperPrimary: 1 Data id: 65, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/email_v2, data1: y@y.com Data id: 66, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/email_v2, data1: yy@y.com, isPrimary: 1, isSuperPrimary: 1 When Contact X links/merges/joins Contact Y , the tables becomes; ### Contacts table Contact id: 32, displayName: X, starred: 1, timesContacted: 2, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 ### RawContacts table RawContact id: 30, contactId: 32, displayName: X, accountName: x@x.com, accountType: com.google, starred: 0, timesContacted: 1, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 RawContact id: 31, contactId: 32, displayName: Y, accountName: y@y.com, accountType: com.google, starred: 1, timesContacted: 2, lastTimeContacted: 1573071750624, customRingtone: content://media/internal/audio/media/115, sendToVoicemail: 1 ### Data table Data id: 57, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/group_membership, data1: 18 Data id: 58, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/name, data1: X, isPrimary: 1, isSuperPrimary: 1 Data id: 59, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: x@x.com Data id: 60, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: xx@x.com, isPrimary: 1, isSuperPrimary: 0 Data id: 63, rawContactId: 31, contactId: 32, mimeType: vnd.android.cursor.item/group_membership, data1: 6 Data id: 64, rawContactId: 31, contactId: 32, mimeType: vnd.android.cursor.item/name, data1: Y, isPrimary: 1, isSuperPrimary: 0 Data id: 65, rawContactId: 31, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: y@y.com Data id: 66, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/email_v2, data1: yy@y.com, isPrimary: 1, isSuperPrimary: 0 What changed? Contact Y's row has been deleted and its column values have been merged into Contact X row. If the reverse occurred (Contact Y merged with Contact X), Contact Y's row would still be deleted. The difference is that Contact X's display name will be set to Contact Y's display name, which is done by the native Contacts app manually by setting Contact Y's Data name row to be the \"default\" (isPrimary and isSuperPrimary both set to 1). \u2139\ufe0f The AggregationExceptions table records the linked RawContacts' IDs in ascending order regardless of the order used in RAW_CONTACT_ID1 and RAW_CONTACT_ID2 at the time of merging. The RawContacts and Data table remains the same except the joined contactId column values have now been changed to the id of Contact X. All Data rows' isSuperPrimary value has been set to 0 though the isPrimary columns remain the same. In other words, this clears any \"default\" set before the link. These are done automatically by the Contacts Provider during the link operation. What is not done automatically by the Contacts Provider is that the name row of former Contact X is set as the default. The native Contacts app does this manually. The Contacts Providers automatically sets the Contact display name to whatever the default name row is for the Contact, if available. For more info on Contact display name resolution, read the Contact Display Name and Default Name Rows section. \u2139\ufe0f Display name resolution is different for APIs below 21 (pre-lollipop). The display name of the RawContacts remain the same. The Groups table remains unmodified. Options updates Changes to the options (starred, timesContacted, lastTimeContacted, customRingtone, and sendToVoicemail) of a RawContact may affect the options of the parent Contact. On the other hand, changes to the options of the parent Contact will be propagated to all child RawContact options. Photo updates A RawContact may have a full-sized photo saved as a file and a thumbnail version of that saved in the Data table in a photo mimetype row. A Contact's full-sized photo and thumbnail are simply references to the \"chosen\" RawContact's full-sized photo and thumbnail (though the URIs may differ). \u2139\ufe0f When removing the photo in the native contacts app, the photo data row is not immediately deleted, though the PHOTO_FILE_ID is immediately set to null. This may result in the PHOTO_URI and PHOTO_THUMBNAIL_URI to still have a valid image uri even though the photo has been \"removed\". This library immediately deletes the photo data row, which seems to work perfectly. Data inserts In the native Contacts app, Data inserted in combined (raw) contacts mode will be associated to the first RawContact in the list sorted by the RawContact ID. \u2139\ufe0f This may not be the same as the RawContact referenced by ContactsColumns.NAME_RAW_CONTACT_ID . UI changes? The native Contacts App does not display the groups field when displaying / editing Contacts that have multiple RawContacts (linked/merged/joined) in combined mode. However, it does allow editing individual RawContact Data rows in which case the groups field is displayed and editable. In the native Contacts app, the name attribute used comes from the name row with IS_SUPER_PRIMARY set to true. This and all other \"unique\" mimetypes (organization) and non-unique mimetypes (email) per RawContact are shown only if they are not blank. Showing multiple RawContact's data in the same edit screen (combined mode) In older version of the native, Android Open Source Project (AOSP) Contacts app, data from multiple RawContacts was being shown in the same edit screen. This caused a lot of confusion about which data belonged to which RawContact. Newer versions of AOSP Contacts only allow editing one RawContact at a time to avoid confusion. Though, several RawContacts' data are still shown (not-editable) in the same screen. AggregationExceptions table \u00b6 Given the following Contacts and their RawContacts; Contact A RawContact 1 Contact B RawContact 2 Contact C RawContact 3 Contact D RawContact 4 Linking one by one in this order; Contact B link Contact A Contact C link Contact D Contact C link Contact B Results in the following AggregationExceptions rows respectively; Aggregation exception id: 430, type: 1, rawContactId1: 1, rawContactId2: 2 Aggregation exception id: 430, type: 1, rawContactId1: 1, rawContactId2: 2 Aggregation exception id: 432, type: 1, rawContactId1: 3, rawContactId2: 4 Aggregation exception id: 436, type: 1, rawContactId1: 1, rawContactId2: 2 Aggregation exception id: 439, type: 1, rawContactId1: 1, rawContactId2: 3 Aggregation exception id: 442, type: 1, rawContactId1: 1, rawContactId2: 4 Aggregation exception id: 440, type: 1, rawContactId1: 2, rawContactId2: 3 Aggregation exception id: 443, type: 1, rawContactId1: 2, rawContactId2: 4 Aggregation exception id: 444, type: 1, rawContactId1: 3, rawContactId2: 4 There is a pattern here. RawContact ids are sorted in ascending order and linked from least to greatest exhaustively but no double links (1-2 is the same as 2-1). RawContact 1 has a row with RawContact 2, 3, and 4. RawContact 2 has a row with RawContact 3 and 4. RawContact 3 has a row with RawContact 4. Linking all in one go; Contact C link Contact A, B, D Results in the same AggregationExceptions rows. Unlinking results in the same AggregationExceptions rows except the type is 2 (TYPE_KEEP_SEPARATE). Data Table \u00b6 The Data table uses generic column names (e.g. \"data1\", \"data2\", ...) using the column \"mimetype\" to distinguish the type of data in that generic column. For example, the column name of StructuredName.DISPLAY_NAME is the same as Email.ADDRESS , which is \"data1\". Each row in the Data table consists of a piece of RawContact data (e.g. a phone number), its \"mimetype\", and the associated RawContact and Contact id. A row does not contain all of the data for a contact. RawContacts may only have one row of certain mimetypes and may have multiple rows of other mimetypes. Here is the list. Unique mimetype per RawContact Name (StructuredName) Nickname Note Organization Photo SipAddress Non-unique mimetype per Raw Contact Address (StructuredPostal) Email Event GroupMembership Im Phone Relation Website Although some mimetypes are unique per RawContact, none of those mimetypes are unique per Contact because a Contact is an aggregate of one or more RawContacts! Data Primary and Super Primary Rows \u00b6 As per documentation, for a set of data rows with the same mimetype (e.g. a set of emails), there should only be one primary data row (e.g. email) per RawContact and one super primary data row per Contact. Furthermore, a data row that is super primary must also be primary. Unfortunately, the Contacts Provider does not do any data set validation for the Data columns IS_PRIMARY and IS_SUPER_PRIMARY . This means that it is possible to set more than one data row of the same mimetype as primary for the same RawContact and super primary for the same aggregate Contact. It is also possible to set a data row as super primary but not primary. Upholding the the contract is left to us... For example, given this relationship; Contact RawContact X Email A Email B RawContact Y Email C Email D When Emails A, B, C, and D are inserted with the RawContacts or after the RawContacts have been created, we get the following state; Email Primary Super Primary A 0 0 B 0 0 C 0 0 D 0 0 The state does not change when RawContact X is linked with RawContact Y. After setting Email A as the \"default\" email, it becomes primary and super primary; Email Primary Super Primary A 1 1 B 0 0 C 0 0 D 0 0 Then setting Email B as the default email, it becomes primary and super primary. Email A is no longer primary or super primary. Email Primary Super Primary A 0 0 B 1 1 C 0 0 D 0 0 Then setting Email C as the default email, it becomes primary and super primary. Email B is still primary because it belongs to a different RawContact than Email C. However, Email B is no longer the super primary as there can only be one per aggregate Contact. Email Primary Super Primary A 0 0 B 1 0 C 1 1 D 0 0 Then setting Email D as the default email, it becomes primary and super primary. Email C is no longer primary or super primary. Email Primary Super Primary A 0 0 B 1 0 C 0 0 D 1 1 Then clearing the default email D, removes its primary and super primary status. However, email B remains a primary but not a super primary. Email Primary Super Primary A 0 0 B 1 0 C 0 0 D 0 0 The above behavior is observed from the native Contacts app. The \"super primary\" data of an aggregate Contact is referred to as the \"default\". \u2139\ufe0f At this point, the native Contacts app still shows email B as the first email in the list even though it isn't the \"default\" (super primary) because it is still a primary. This adds a bit of confusion in my opinion, especially when more than 2, 3, or 4 RawContacts are linked. A \"fix\" would be to only order the list of emails using \"super primary\" instead of \"super primary\" and \"primary\". OR to remove the primary status of the data set of all linked RawContacts. One benefit of the native Contacts implementation of this is that it retains the primary status when unlinking RawContacts. This library should follow what the native Contacts app is doing in spirit of recreating the native experience as closely as possible, even if it seems like a lesser experience. Data Table Joins \u00b6 All columns accessible via cursors returned from Data table queries are specified in DataColumnsWithJoins , which includes the DataColumns , ContactsColumns , and ContactOptionsColumns . In code, mentions of the \"Data table\" typically refers to the joined table. The DataColumns gives us access to all of the columns in the Data table. All other joined columns, including the ContactsColumns are appended to each row in the query. This means that the ContactsColumns ; DISPLAY_NAME , PHOTO_URI , and PHOTO_THUMBNAIL_URI are repeated for all Data rows belonging to the same Contact. The ContactOptionsColumns values joined with the Data table are the values of the Contact, not the RawContact that the Data row belongs to! The same applies to the \"display_name\". Data Updates \u00b6 A new row in the Data table is created for each new piece of data (e.g. email address) entered for the contact. Removing a piece of existing data results in the deletion of the row in the Data table if that row no longer contains any meaningful data (no meaningful non-null \"datax\" columns left). This is the behavior of the native Android Contacts app. Therefore, querying for null fields is not possible. For example, there may be no Data rows that exist where the email address is null. Thus, a query to search for all contacts with null email address may return 0 contacts even if there are some contacts without email addresses. Data Required \u00b6 Creating blank RawContacts without email address (or other fields), results in no rows in the Data table for the email address, and all other fields. There are a few exceptions. The following Data rows are automatically created for all contacts, if not provided; Group membership, underlying value defaults to the account's default system group Name, underlying value defaults to null Nickname, underlying value defaults to null Note, underlying value defaults to null \u2139\ufe0f All of the above rows are only automatically created for RawContacts that are associated with an Account. If a valid account is provided, the default (auto add) system group membership row is automatically created immediately by the Contacts Provider at the time of contact insertion. The name, nickname, and note are automatically created at a later time. If a valid account is not provided, none of the above data rows are automatically created. Blank RawContacts The Contacts Providers allows for RawContacts that have no rows in the Data table (let's call them \"blanks\") to exist. The native Contacts app does not allow insertion of new RawContacts without at least one data row. It also deletes blanks on update. Despite seemingly not allowing blanks, the native Contacts app shows them. There are two scenarios where blanks may exist. Contact with RawContact(s) with no Data row(s). In this case, the Contact is blank as well as its RawContact(s). Contact that has RawContact with Data row(s) and a RawContact with no Data row. In this case, the Contact and the RawContact with Data row(s) are not blank but the RawContact with no Data row is blank. Data StructuredName \u00b6 The DISPLAY_NAME is the unstructured representation of the name. It is made up of structured components; PREFIX , GIVEN_NAME , MIDDLE_NAME , FAMILY_NAME , and SUFFIX . When updating or inserting a row; If the display name is null and there are non-null structured components provided (e.g. given and family name), the Contacts Provider will automatically set the display name by combining the structured components. If the display name is not null and all structured components are null, the Contacts Provider automatically (to the best of its ability) derive the values for all the structured components. If the display name and structured components are not null, the Contacts Provider does nothing automatically. Data StructuredPostal \u00b6 The FORMATTED_ADDRESS is the unstructured representation of the postal address. It is made up of structured components; STREET , POBOX , NEIGHBORHOOD , CITY , REGION , POSTCODE , and COUNTRY . When updating or inserting a row; If the formatted address is null and there are non-null structured components provided (e.g. street and city), the Contacts Provider will automatically set the formatted address by combining the structured components. If the formatted address is not null and all structured components are null, the Contacts Provider automatically sets the street value to the formatted address. If the formatted address and structured components are not null, the Contacts Provider does nothing automatically. Groups Table & Accounts \u00b6 Contacts are assigned to one or more groups via the GroupMembership . It typically looks like this; Group id: 1, systemId: Contacts, readOnly: 1, title: My Contacts, favorites: 0, autoAdd: 1, accountName: vestrel00@gmail.com, accountType: com.google Group id: 2, systemId: null, readOnly: 1, title: Starred in Android, favorites: 1, autoAdd: 0, accountName: vestrel00@gmail.com, accountType: com.google Group id: 3, systemId: Friends, readOnly: 1, title: Friends, favorites: 0, autoAdd: 0, accountName: vestrel00@gmail.com, accountType: com.google Group id: 4, systemId: Family, readOnly: 1, title: Family, favorites: 0, autoAdd: 0, accountName: vestrel00@gmail.com, accountType: com.google Group id: 5, systemId: Coworkers, readOnly: 1, title: Coworkers, favorites: 0, autoAdd: 0, accountName: vestrel00@gmail.com, accountType: com.google Group id: 6, systemId: null, readOnly: 0, title: Custom Group, favorites: 0, autoAdd: 0, accountName: vestrel00@gmail.com, accountType: com.google The actual groups are in a separate table; Groups. Each group is associated with an Account. No group can exist without an account. It is account-exclusive. Each account will have its own set of the above system groups. This means that there may be multiple groups with the same title belonging to different accounts. System ids are typically Contacts, Friends, Family, and Coworkers. These ids are typically the same across all copies of Android. Notes; - The Contacts system group is the default group in which all raw contacts of an account belongs to. Therefore, it is typically hidden when showing the list of groups in the UI. - The starred (favorites) group is not a system group as it has null system id. However, it behaves like one in that it is read only and it comes with most (if not all) copies of the native app. Removing the Account will delete all of the associated rows in the Groups table. Groups, duplicate titles The Contacts Provider allows multiple groups with the same title (case-sensitive comparison) belonging to the same account to exist. In older versions of Android, the native Contacts app allows the creation of new groups with existing titles. In newer versions, duplicate titles are not allowed. Therefore, this library does not allow for duplicate titles. \u2139\ufe0f In newer versions, the group with the duplicate title gets deleted either automatically by the Contacts Provider or when viewing groups in the native Contacts app. It's not an immediate failure on insert or update. This could lead to bugs! Groups Table & GroupMemberships (Data Table) \u00b6 There may be multiple groups with the same title from different accounts. Therefore, the group membership should point to the group belonging to the same account as the raw contact. The native Contacts app displays only the groups belonging to the selected account. Updating group memberships of existing raw contacts seem to be almost instant. All raw contacts must be a part of at least the default group (system id is \"Contacts\"). Raw contacts with no group membership will be asynchronously added to the Account's default group by the Contacts Provider. Membership to the default group should never be deleted! Starred in Android (Favorites) \u00b6 When the ContactOptionsColumns.STARRED column of a Contact in the Contacts table is set to true, the Contacts Provider automatically adds a group membership to the favorites group for all RawContacts linked to the Contact. Setting STARRED to false removes all group memberships to the favorites group. The STARRED is interdependent with group memberships to the favorites group. Adding a group membership to the favorites group results in STARRED being set to true. Removing the membership sets it to false. Raw contacts that are not associated with an account do not have any group memberships. Even though these raw contacts may not have a membership to the favorites group, they may still be \"starred\" (favorited) via the ContactOptionsColumns.STARRED column in the Contacts table, which is not dependent on the existence of a favorites group membership. Refresh RawContact instances after changing the starred value. Otherwise, performing an update on the RawContact with a stale set of group memberships may revert the star/unstar operation. For example, query returns a starred RawContact -> set starred to false -> update RawContact (still containing a group membership to the favorites group) -> starred will be set back to true. Group memberships & Local RawContacts \u00b6 Local RawContacts may have a group membership to the default system group of an Account without being associated with the Account... The native Contacts app may not have an edit-RawContact option for newly inserted RawContacts that have no group membership to the default group when an Account is available. Though, edits can still be made in other ways. Instead, an option to \"Add to contacts\" is shown that adds a membership to the default group but does not associate the raw contact to the Account that owns the group. The edit UI does not show the group membership field. Weirdly, this only occurs when there is exactly only one Account. If there are no Accounts or there are two or more Accounts, then this does not occur. Also, this does not occur for a Contact with a RawContact that has a group membership AND a RawContact that has no group membership. Groups; Deletion \u00b6 Similar to deleting RawContacts, deleting a Groups row may not immediately delete the Groups row. In this case, it is marked as deleted. \u2139\ufe0f A Group is marked for deletion as specified by GroupsColumns.DELETED . Typically, deleting Groups immediately removes the row from the Groups table. However, Groups row remains and is simply marked for deletion UNTIL the sync adapters syncs the changes. One of the reasons syncs do not occur is when the system sync settings are turned off for the Account or there is no network connection. Such Groups should not be included in query results for Contacts. The AOSP and Google Contacts app also does not show them. Note that local Groups rows (not associated with an Account) are deleted immediately as no sync needs to occur. Groups; UI \u00b6 In newer Android versions of the native Contacts app, \"groups\" are now being referred to as \"labels\". However, the underlying code still uses groups. Google is probably just trying to make it more user friendly by calling it label instead of group. User Profile \u00b6 There exist one (profile) Contacts row that identifies the user; ContactsColumns.IS_USER_PROFILE . There is at least one RawContacts row that is associated with the user profile; RawContactsColumns.RAW_CONTACT_IS_USER_PROFILE . Associated RawContacts may or may not be associated with an Account. The RawContacts row(s) may have rows in the Data table as usual. These profile table rows have special IDs that differ from regular rows. See ContactsContract.isProfileId . \u2139\ufe0f The Contacts Provider will throw an IllegalArgument exception when attempting to include ContactsColumns.IS_USER_PROFILE and RawContactsColumns.RAW_CONTACT_IS_USER_PROFILE columns in Data table queries. I have not yet tried including these columns in the Contacts or RawContacts table queries. The profile Contact row may not be merged / linked with other contacts and do not belong to any group (favorites / starred). Profile rows in the Contacts, RawContacts, and Data table are not visible via queries in the respective tables. They will not be in the resulting cursor. To get the profile Contacts table rows, query the Profile.CONTENT_URI . To get profile RawContacts table rows, query the Profile.CONTENT_RAW_CONTACTS_URI . To get the profile Data table rows, query the Profile.CONTENT_RAW_CONTACTS_URI appended with the RawContact id and RawContacts.Data.CONTENT_DIRECTORY . To insert a new profile RawContact, use Profile.CONTENT_RAW_CONTACTS_URI . It will automatically be associated with the profile Contact. If the profile Contact does not yet exist, it will be created automatically. To insert a new profile Data row, either; insert to the Profile.CONTENT_RAW_CONTACTS_URI appended with the RawContact id and RawContacts.Data.CONTENT_DIRECTORY insert to the Data table directly, referencing the RawContact id Same rules apply to all table rows. If all profile RawContacts table rows have been deleted, then associated Contacts and Data table rows will automatically be deleted. Profile aggregation The RawContacts of a (Contact) Profile are linked via the indexed rows; Profile.CONTENT_RAW_CONTACTS_URI . Therefore, the AggregationsExceptions table is not used here. Profile and users Note that as of Android 5 Lollipop, there may exist multiple users in a device. Each user has a separate list of accounts and contact data. This also means that each user has a separate (local) profile contact. Profile and Accounts According to the Profile documentation; \"... each account (including data set, if applicable) on the device may contribute a single raw contact representing the user's personal profile data from that source.\" In other words, one account can have one profile RawContact. Whether or not profile RawContacts associated to an Account can be carried over and synced across devices and users is up to the Contacts Provider / Sync provider for that Account. \u2139\ufe0f From my experience, profile RawContacts associated to an Account is not carried over / synced across devices or users. Despite the documentation of \"one profile RawContact per one Account\", the Contacts Provider allows for multiple RawContacts per Account, including multiple local RawContacts (no Account). Thus, we should let consumers exploit this but set defaults to be one-for-one. Creating / setting up the profile in the native Contacts app results in the creation of a local RawContact (not associated with an Account) even if there are available Accounts. The Contacts Provider does not associate local contacts to an account when an account is or becomes available (regardless of API level). Removing the Account will delete all of the associated rows in the Contact, RawContact, Data, and Groups tables. This includes user Profile data in those tables. Profile permissions Profile permissions (READ_PROFILE and WRITE_PROFILE) have been removed since API 23. However, they are still required for API 22 and below. Reading and writing the profile is included in the Contacts permissions. There is no need to ask for profile permissions at runtime because prior to API 23, permissions in the AndroidManifest have to be accepted prior to installation. Syncing Data / Sync Adapters \u00b6 First, it\u2019s good to know the official documentation of sync adapters; https://developer.android.com/guide/topics/providers/contacts-provider#SyncAdapters Now, let\u2019s ingest the official docs\u2026 Data belonging to a RawContact that is associated with a Google account will be available anywhere the Google account is used; in any Android or iOS device, a web browser, etc\u2026 Data is synced by Google\u2019s sync adapters to and from their remote servers. Syncing depends on the account sync settings, which can be configured in the native system settings app and possibly through some remote configuration. This library does not provide any sync adapters. Instead, it relies on existing sync adapters to do the syncing. Sync adapters and syncing are really out of scope of this library. Syncing is its own thing that typically happens outside of an application UI. This library is focused on reading and writing native and custom data to and from the local database. Syncing the local database to and from a remote service is a different story altogether =) Custom Data / MimeTypes \u00b6 First, it\u2019s good to know the official documentation of custom data rows; https://developer.android.com/guide/topics/providers/contacts-provider#CustomData Now, let\u2019s ingest the official docs\u2026 Custom mimetypes do not belong to the native Contacts Provider mimetype set (e.g. address, email, phone, etc). The Contacts Provider allows for the creation of new / custom mimetypes. This is especially useful for other apps (Google Contacts, Facebook, Twitter, WhatsApp, etc) that want to attach extra pieces of data to a particular RawContact. Custom data are NOT synced, including those that belong to RawContacts that are associated with an Account. Custom sync adapters are required to sync custom data. This library currently does NOT provide custom sync adapters to sync custom data! Custom data from other apps such as Facebook, Twitter, WhatsApp, etc may or may not be synced. It all depends on those applications and their custom sync adapters (if they have any) and sync settings. For insight on how aforementioned social media services may be syncing their data, read through the official documentation; https://developer.android.com/guide/topics/providers/contacts-provider#SocialStream Unused ContactsContract Stuff \u00b6 We are currently not utilizing these things because I haven't found usages of them while using the native Contacts app. They are probably working behind the scenes but until we find uses for these, let's leave it out because YAGNI . Settings . Contacts-specific settings for various Accounts (settings for an Account). Might be useful to add this for SHOULD_SYNC and UNGROUPED_VISIBLE . ContactsColumns.IN_VISIBLE_GROUP + Groups.GROUP_VISIBLE . Flag indicating if the contacts belonging to this group should be visible in any user interface. Java Support \u00b6 This library is intended to be Java-friendly. The policy is that we should attempt to write Java-friendly code that does not increase lines of code by much or add external dependencies to cater exclusively to Java users. Creating Entities & data class \u00b6 First, consumers are not allowed to create immutable entities. Those must come from the API itself to ensure data integrity. Whether or not we will change this in the future is debatable =) Consumers are able to set read-only and private or internal variables though because all Entity implementations are data classes. Data classes provide a copy function that allows for setting any property no matter their visibility and even if the constructor is private. As a matter of fact, setting the constructor of a data class as private gives this warning by Android Studio: \"Private data class constructor is exposed via the 'copy' method. There is currently no way to disable the copy function of data classes (that I know of). The only thing we can do is to provide documentation to consumers, insisting against the use of the copy method as it may lead to unwanted side effects when updating and deleting contacts. \u2139\ufe0f We could just use regular classes instead of data classes but entities should be data classes because it is what they are (know what I mean?!). Also, I'd hate to have to generate equals and hashcode functions for them, which will make the code harder to maintain. Though, we might do this anyways at some point if we want to make it possible for a mutable entity to equal an immutable entity. Time will tell =) FIXME? Hide / disable data class copy function if kotlin ever allows it. https://discuss.kotlinlang.org/t/data-class-copy-visibility-modifier/19746 Immutable vs Mutable Entities \u00b6 This library provides true immutability for immutable entities. Take a look at the current (simplified) hierarchy; 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 > ) : RawContactEntity data class Address ( val formattedAddress : String? ) data class MutableAddress ( var formattedAddress : String? ) \u2139\ufe0f The use of sealed class is to prevent consumers from defining their own entities. This restriction may or may not change in the future. Notice that there is nothing mutable in the immutable Contact . Everything are val s and the data structures used (i.e. RawContact , Address , and List ) are all immutable. This provides consumers 100% confidence that immutable entities are not mutable. They will not change or mutate in any way. Once they are constructed, they will always remain the same. Why immutability is so important will not be covered in this dev notes because it would be too big (that's what she said) and there are blogs and books written about this. One of the most important advantages of immutability is that it is thread-safe. Immutable instances can be used in several different threads without the need for synchronization and worries about deadlocks. In other words, they are thread-safe and faster than the mutable version. The current structure also allows consumers to be able to distinguish between immutable and mutable entities exhaustively. E.G. fun doSomethingAndReturn ( contact : ContactEntity ) = when ( contact ) { is Contact -> {} is MutableContact -> {} } \u2139\ufe0f The mutable entities provided in this library are NOT thread-safe . Consumers will have to perform their own synchronizations if they want to use and mutate mutable entities in multi-threaded scenarios. The cost of the current immutability implementation \u00b6 The cost of implementing true immutability is more lines of code. Notice that the MutableContact does not inherit from Contact . The same goes for the other entities. This leads to having to write seemingly duplicate code when writing functions and extensions. // FIXME? Furthermore, equality between immutable and mutable entities are not yet implemented. This means that Contact(\"john\") == MutableContact(\"john\") will return false even though their underlying contents are the same. This can be fixed by overriding the equals and hashcode functions of all entities. However, that is a lot more code that I would like to avoid, which is why I'm using data class for all entities in the first place! This may change in the future if the community really wants to change it =) On a side note, the same cost is incurred by Kotlin's standard libs. For example, notice that AbstractMutableList does not inherit from and is completely separate from AbstractList . I'm sure stdlib devs also had to write seemingly duplicate code in implementations of the List interface. Avoiding the cost... Shortcuts and pitfalls. \u00b6 One thing that may come to mind in attempts to reduce lines of seemingly duplicate code is to have just a mutable implementation of an immutable declaration. For example, we can restructure the hierarchy to; sealed interface Contact { val rawContacts : List < RawContact > } data class MutableContact ( override val rawContacts : List < MutableRawContact > ) : Contact sealed interface RawContact { val addresses : List < Address > } data class MutableRawContact ( override var addresses : MutableList < MutableAddress > ) : RawContact sealed interface Address { val formattedAddress : String? } data class MutableAddress ( override var formattedAddress : String? ) : Address Notice that there is a non-concrete declaration (i.e. Contact , RawContact , and Address ) and just one concrete implementation (i.e. MutableContact , MutableRawContact , and MutableAddress ). \u2139\ufe0f A val declaration can be overridden by a var . Keep in mind that val only requires getters whereas var requires both getters and setters. Therefore, a var cannot be overridden by a val . Or maybe there is a different reason Kotlin imposes this restriction. On a similar note, the List interface can be overridden to a MutableList . We, as API contributors, can avoid having to write seemingly duplicate functions and extensions! However! Can you see what's wrong with this setup? If we do this, we would either be deceiving consumers to think that the instances of \"immutable\" class signatures (i.e. Contact , RawContact , and Address ) are actually immutable OR we would have to let consumers know that the API does not really provide true immutability. Neither option is ideal (nor is it acceptable IMO). Consumers would have a reference to a Contact , which they may assume is immutable because of the usage of val instead of var , but in actuality the underlying implementation is mutable... This could be a cause of really hard to find bugs in multi-threaded usage. Consumers may use Contact with the assumption that it is immutable only to find that it can actually be mutated! We could fix this by just making the mutable implementation thread-safe but since that is the only implementation, consumers will be forced to use thread-safe code when they don't have to thereby negatively affecting performance. Keep in mind that thread safety is only one of several reasons for immutability. Those other reasons will be violated too. Consumers will be shocked if they ever do the following or something similar. fun x ( contact : Contact ) = when ( contact ) { is MutableContact -> {} // this is always true is Contact -> {} // this is always true } In any case, I have to admit, it is a nice trick that would save API contributors time. But that's just it! It's just a trick. A shortcut. A nice little time save at the cost of integrity. It is not worth it (IMO). Why Not Add Android X / Support Library Dependencies? \u00b6 I want to keep the dependency list of this library to a minimum. The Contacts Provider is native to Android since the beginning. I want to honor that fact by avoiding adding dependencies here. I made a bit of an exception by adding the Dexter library for permissions handling for the permissions modules (not in the core modules). I'm tempted to remove the Dexter dependency and implement permissions handling myself because Dexter brings in a lot of other dependencies with it. However, it is not part of the core module so I'm able to live with this. TODO Remove/replace Dexter. It is no longer being maintained. Keeping dependencies to a minimum is just a small challenge I made up. We will see how long it can last! I left comments all over the code on when an androidx dependency may be useful. The most glaring example of this is @WorkerThread. Even with that, I'll hold off on adding the androidx annotation lib. I think we can all be consenting adults =) If the community strongly desires the addition of these support libs, then the community will win =)","title":"Developer notes"},{"location":"dev-notes/#developer-notes","text":"This document contains useful developer notes that should be kept in mind during development. It serves as a memory of all the quirks and gotcha's of things like Android's ContactsContract . This is only meant to be read by contributors of this library, not consumers!","title":"Developer Notes"},{"location":"dev-notes/#contacts-provider-contactscontract","text":"It is important to know about the ins and outs of Android's Contacts Provider. After all, this API is just a wrapper around it. It is important to get familiar with the official documentation of the Contact's Provider . Here is a summary; There are 3 main database tables used in dealing with contacts; Contacts RawContacts Data \u2139\ufe0f There are more but that is covered later. All of these tables and their fields are enumerated and documented in android.provider.ContactsContract . Each table serves a different purpose; Contacts Rows representing different people. RawContacts Rows that link Contacts rows to specific Accounts. Data Rows containing data (e.g. name, email) for a RawContacts row. These tables contain the following (notable) information (columns); Contacts _ID DISPLAY_NAME_PRIMARY RawContacts _ID : the Contacts._ID ACCOUNT_NAME : the Account.name ACCOUNT_TYPE the Account.type Data RAW_CONTACT_ID : the RawContacts._ID CONTACT_ID : the Contacts._ID DATA_1 to DATA_15 : contains a piece of contact data (e.g. first and last name, email address and type) determined by the MIMETYPE MIMETYPE : the type of data that this row's DATA_X columns contain (e.g. name and email data) The tables are connected the following way; RawContacts contains a reference to the Contacts row Id. Data contains a reference to the RawContacts row Id and Contacts row Id.","title":"Contacts Provider / ContactsContract"},{"location":"dev-notes/#contacts-display-name","text":"The Contacts.DISPLAY_NAME name may be different than the Data StructuredName display name! If a structured name in the Data table is not provided, then other kinds of data will be used as the Contacts row display name. For example, if an email is provided but no structured name then the display name will be the email. When a structured name is inserted, the Contacts Provider automatically updates the Contacts row display name. \u2139\ufe0f In the case of StructuredName , the Contacts.DISPLAY_NAME is made up of the prefix, given, middle, family name, and suffix and not the unstructured display name. If no data rows suitable to be a display name are available, then the Contacts row display name will be null. Data suitable to be a Contacts row display name are enumerated in DisplayNameSources ; email nickname organization phone number structured name Data not suitable to be display names are; address event group im note relation sip website The kind of data used as the display for the Contact is set in ContactNameColumns.DISPLAY_NAME_SOURCE . A note about StructuredName There may be a scenario where the unstructured StructuredName.DISPLAY_NAME does not match the structured components. Such scenarios are possible but is considered incorrect. For example, it is possible to programmatically set the display name to \"Ice Cold\" but set the given and family name to \"Hot Fire\". The Contacts.DISPLAY_NAME is made up of the prefix, given, middle, family name, and suffix (\"Hot Fire\") and not the unstructured display name. The Contacts Provider's general matching algorithm does not include the Contacts.DISPLAY_NAME . However, the StructuredName.DISPLAY_NAME is included in the matching process but not the rest of the structured components (e.g. given and family name). The native Contacts app displays the Contacts.DISPLAY_NAME . So, here comes the unusual scenario that looks like a bug. The general matching algorithm will match the text \"Ice\" or \"Cold\" but not \"Hot\" or \"Fire\". The end result is that searching for the Contact \"Ice Cold\" will show a Contact called \"Hot Fire\"!","title":"Contacts; Display Name"},{"location":"dev-notes/#contact-display-name-and-default-name-rows","text":"If available, the \"default\" (isPrimary and isSuperPrimary set to 1) name row for a Contact is automatically set as the Contact display name by the Contacts Provider. Otherwise, the Contacts Provider chooses from any of the other suitable data from the aggregate Contact. \u2139\ufe0f The ContactsColumns.NAME_RAW_CONTACT_ID is automatically updated by the Contacts Provider along with the display name. The default status of other sources (e.g. email) does not affect the Contact display name. The native Contacts app also sets the most recently updated name as the default at every update. This results in the Contact display name changing to the most recently updated name from one of the associated RawContacts. The \"most recently updated name\" is the name field that was last updated by the user when editing in the Contacts app, which is irrelevant to its value. It does not matter if the user deleted the last character of the name, added the same character back, and then saved. It still counts as the most recently updated. All of the above only applies to API 21 and above. Display name resolution is different for APIs below 21 (pre-Lollipop)! The ContactsColumns.NAME_RAW_CONTACT_ID was added in API 21. It changed the way display names are resolved for Contacts with more than one constituent RawContacts, which is what has been described so far. Before this change (APIs 20 and below), the native Contacts app is still able to set the Contact display name somehow. I'm not sure how. If someone figures it out, please let me know. I tried updating the Contact DISPLAY_NAME directly but it does not work. Setting a name row as default also does not affect the Contact DISPLAY_NAME .","title":"Contact Display Name and Default Name Rows"},{"location":"dev-notes/#contacts-id-vs-lookup_key","text":"The Contacts._ID is the unique identifier for the row in the Contacts table. The Contacts.LOOKUP_KEY is the unique identifier for an aggregate Contact (a person). The _ID may change due to aggregation and sync. The same goes for the LOOKUP_KEY but unlike the ID it may still be used to find the aggregate contact. Unlike the Contact ID, the lookup key is the same across devices (for contacts that are associated with an Account and are synced). \u2139\ufe0f I did the following investigation with a much larger data set. I simplified it here for brevity. Let's take a look at the following Contacts and RawContacts table rows, #### Contacts table Contact id: 55, lookupKey: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50, displayNamePrimary: Contact With Local RawContact Contact id: 56, lookupKey: 2059i6f5de8460f7f227e, displayNamePrimary: Contact With Synced RawContact #### RawContacts table RawContact id: 55, contactId: 55, displayNamePrimary: Contact With Local RawContact RawContact id: 56, contactId: 56, displayNamePrimary: Contact With Synced RawContact There are two Contacts each having one RawContact. Notice that the lookup keys are a bit different. Contact With Local RawContact: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 Contact With Synced RawContact: 2059i6f5de8460f7f227e The Contact with unsynced, device-only, local RawContact has a much longer (or shorter e.g. 0r62-2A2C2E) lookup key and starts with \"0r -\" and all characters after it are in uppercase. The other thing to notice is that the \"55\" in \"0r55-\" seems to be the same as the RawContact ID (I did a bit more experiments than what is written in these notes to confirm that it is indeed the RawContact ID and not the Contact ID). We probably don't need to worry about these details though the Contacts Provider probably uses these things internally. We also should not rely on it. However, it may be safe to assume that the Contact lookup key is a reference to a RawContact (or reference to more than one constituent RawContact when multiple RawContacts are linked). Again, an internal Contacts Provider detail we should not rely on BUT is probably relevant when implementing sync adapters. When we link the two, we get... Contact id: 55, lookupKey: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50.2059i6f5de8460f7f227e, displayNamePrimary: Contact With Synced RawContact #### RawContacts table RawContact id: 55, contactId: 55, displayNamePrimary: Contact With Local RawContact RawContact id: 56, contactId: 55, displayNamePrimary: Contact With Synced RawContact Notice, Contact with ID 56 has been deleted. Contact with ID 55 still exist with the lookup keys of both Contact 55 and 56 combined separated by a \".\" . This holds true in cases where two or more local-only or non-local-only RawContacts are linked. RawContacts remain unchanged except reference to Contact 56 has been replaced with 55. The primary display name of Contact 55 came from RawContact 55 prior to the link and now comes from RawContact 56 after the link. This primary name resolution is probably irrelevant so pay no attention to it. The most important part to notice is that the lookup keys get combined. The lookup uri is required to build a Contacts.CONTENT_LOOKUP_URI ... /** * A content:// style URI for this table that should be used to create * shortcuts or otherwise create long-term links to contacts. This URI * should always be followed by a \"/\" and the contact's {@link #LOOKUP_KEY}. * It can optionally also have a \"/\" and last known contact ID appended after * that. This \"complete\" format is an important optimization and is highly recommended. *

        * As long as the contact's row ID remains the same, this URI is * equivalent to {@link #CONTENT_URI}. If the contact's row ID changes * as a result of a sync or aggregation, this URI will look up the * contact using indirect information (sync IDs or constituent raw * contacts). *

        * Lookup key should be appended unencoded - it is stored in the encoded * form, ready for use in a URI. */ public static final Uri CONTENT_LOOKUP_URI = Uri . withAppendedPath ( CONTENT_URI , \"lookup\" ); /** * Build a {@link #CONTENT_LOOKUP_URI} lookup {@link Uri} using the * given {@link ContactsContract.Contacts#_ID} and {@link #LOOKUP_KEY}. *

        * Returns null if unable to construct a valid lookup URI from the * provided parameters. */ public static Uri getLookupUri ( long contactId , String lookupKey ) { if ( TextUtils . isEmpty ( lookupKey )) { return null ; } return ContentUris . withAppendedId ( Uri . withAppendedPath ( Contacts . CONTENT_LOOKUP_URI , lookupKey ), contactId ); } From the lookup uri, we can lookup the Contact row... public static Uri lookupContact ( ContentResolver resolver , Uri lookupUri ) { ... } Or simply get the Contact ID... // code inside `public static Uri lookupContact` resolver . query ( lookupUri , new String [] { Contacts . _ID }, null , null , null ) However, given that the lookup key of the deleted Contact 56 still lives on, it is possible to get the linked Contact 55 using the lookup key of Contact 56 using our standard query APIs! . where { Contact . LookupKey contains lookupKey } The above is correct as long as these assumptions hold true; the lookup key is unique there is no lookup key that can contain a shorter lookup key the Contact ID fails this test because a smaller number is contained in a larger number synced contacts have shorter lookup keys than local contacts. However, local contacts' lookup keys are capitalized whereas synced contact are not. Also, there seems to be other differences in pattern between long and short lookup keys. It should be safe to make this assumption. Until the community finds that this assumption is flawed, we'll assume that it is true! For now, we can avoid having to create another API or extensions just for using lookup keys . When we unlink , we get... #### Contacts table Contact id: 55, lookupKey: 2059i6f5de8460f7f227e, displayNamePrimary: Contact With Synced RawContact Contact id: 58, lookupKey: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50, displayNamePrimary: Contact With Local RawContact #### RawContacts table RawContact id: 55, contactId: 58, displayNamePrimary: Contact With Local RawContact RawContact id: 56, contactId: 55, displayNamePrimary: Contact With Synced RawContact Notice, A new Contact row with ID of 58 is created. The lookup keys are separated and distributed between Contact 55 and 58. RawContact 55 Contact reference has been set to Contact 58. Let's compare the Contact-RawContact relationship before and after linking and then unlinking. Contact ID Lookup Key RawContact.Contact ID Before 55, 56 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50, 2059i6f5de8460f7f227e 55, 56 After 55, 58 2059i6f5de8460f7f227e, 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 58, 55 Notice, Contact ID 55 swapped lookup keys with the former Contact 56 (now 58). RawContact ID 55 swapped Contact reference with RawContact 56. The Contact IDs and lookup keys got shuffled BUT the Contact-RawContact relationship remains the same if using the lookup keys as point of reference! Here is another way to look at the table, using the lookup key as the constant... Lookup Key Before After 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 Contact 55, RawContact 55 Contact 58, RawContact 55 2059i6f5de8460f7f227e Contact 56, RawContact 56 Contact 55, RawContact 56 Notice that the indirect relationship between the lookup key and RawContacts remains the same before and after the link-unlink even though the Contact IDs changed. \u2139\ufe0f As mentioned earlier in this section, the \"55\" in \"0r55-\" seems to be referencing the RawContact ID. In other words, since local RawContacts are not synced or tracked in a remote database where Contacts -> RawContacts mappings exist, the Contacts Provider most likely uses this \"0r -\" pattern to make the connection. This is not really relevant for us as we are not relying on this mechanism. I'm just pointing out my observations, which could be incorrect. This means that... If users of this library saved a reference Contact ID 55, then a link-unlink (or sync adapter functions) occur. Getting Contact by ID 55 will result in the RawContact-Data of the former Contact 56 to be returned. This is a bug! Same goes if users saved a reference to Contact ID 56. If users of this library saved a reference to the lookup keys, then a link-unlink (or sync adapter functions) occur. Getting Contact by lookup key will result in the correct RawContact-Data to be returned. So when to use Contact ID vs lookup key? Lookup key: for a reference to a Contact that needs to be loaded after some period of time. Saving/restoring activity/fragment instance state. Saving to an external database, preferences, or files. Creating shortcuts. ID: for everything else. Performing read/write operations in the same function call or session in your app. Performing read/write operations that require ID (e.g. Contact photo and options). Another thing to check is what happens when associating a local RawContact to an Account (move from device to Account) and vice versa. Is the lookup key of the Contact affected? After associating the local RawContact to an Account... #### Contacts table Contact id: 58, lookupKey: 2059i4abd4a8f8ff89642 #### RawContacts table RawContact id: 55, contactId: 58 The lookup key changed but the Contact ID remained the same! In this case, loading a reference to the previously local Contact will fail! I verified that this is indeed the behavior of the native (AOSP) Contacts app. Moving the RawContact from device to Google using Google Contacts app while having Contact details activity opened in the AOSP Contacts app will result in \"error Contact does not exist\" message in the AOSP Contacts app! \u2139\ufe0f The RawContact and its Data also remained the same in this case. Removing the account from it results in... #### Contacts table Contact id: 59, lookupKey: 0r58-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 #### RawContacts table RawContact id: 58, contactId: 59 The Contact and RawContacts row have been deleted and new rows have been created to replace them! I also verified that the Data rows have also been deleted and new rows have been created to replace them! This stuff is not really relevant for lookup key but still good to know for implementing moving between accounts in the future.","title":"Contacts; ID vs LOOKUP_KEY"},{"location":"dev-notes/#rawcontacts-accounts-contacts","text":"The RawContacts table associates a person to an android.accounts.Account that it belongs to. Each new RawContacts row created results in; a new row in the Contacts table (unless the RawContact is associated to another existing Contact) a new row in the RawContacts with account name and type set to null 0 or more rows in the Data table with a reference to the new Contacts and RawContacts Ids \u2139\ufe0f It is possible to create RawContacts without any rows in the Data table. See the Data required section for more details. For example, creating 4 new contacts using the native Android Contacts app results in; Contact id: 4, displayName: First Local Contact Contact id: 5, displayName: Second Local Contact Contact id: 6, displayName: Third Local Contact Contact id: 7, displayName: Third Local Contact RawContact id: 4, accountName: null, accountType: null RawContact id: 5, accountName: null, accountType: null RawContact id: 6, accountName: null, accountType: null RawContact id: 7, accountName: null, accountType: null Data id: 15, rawContactId: 4, contactId: 4, data: First Local Contact Data id: 16, rawContactId: 5, contactId: 5, data: Second Local Contact Data id: 17, rawContactId: 6, contactId: 6, data: Third Local Contact Data id: 18, rawContactId: 7, contactId: 7, data: Third Local Contact Local Contacts / RawContacts RawContacts inserted without an associated account are considered local or device-only raw contacts, which are not synced. The native Contacts app hides the following UI fields when inserting or updating local raw contacts; - Event - Relation - Group memberships To enforce this behavior, this library ignores all of the above during inserts and updates for local raw contacts. Lollipop (API 22) and below When an Account is added, from a state where no accounts have yet been added to the system, the Contacts Provider automatically sets all of the null accountName and accountType in the RawContacts table to that Account's name and type; RawContact id: 4, accountName: vestrel00@gmail.com, accountType: com.google RawContact id: 5, accountName: vestrel00@gmail.com, accountType: com.google RawContact id: 6, accountName: vestrel00@gmail.com, accountType: com.google RawContact id: 7, accountName: vestrel00@gmail.com, accountType: com.google RawContacts inserted without an associated account will automatically get assigned to an account if there are any available. This may take a few seconds, whenever the Contacts Provider decides to do it. Dissociating RawContacts from Accounts will result in the Contacts Provider associating those back to an Account. Marshmallow (API 23) and above The Contacts Provider no longer associates local contacts to an account when an account is or becomes available. Local contacts remain local. Account removal Removing the Account will remove all of the associated rows in the local RawContact, Data, and Groups tables. This includes user Profile data in those tables. SyncColumns modifications This library supports modifying the SyncColumns.ACCOUNT_NAME and SyncColumns.ACCOUNT_TYPE of the RawContacts table in some cases only. In some cases does not work as intended and produces unwanted side-effects. It probably has something to do with syncing with remote servers and local Account / sync data not matching up similar to errors on network requests if the system time does not match network time. The motivation behind changing the Account columns of the RawContacts table rows is that it would allow users to; Associate local RawContacts (those that are not associated with an Account) to an Account, allowing syncing between devices. Dissociate RawContacts from their Account such that they remain local to the device and not synced between devices. Transfer RawContacts from one Account to another. When modifying the SyncColumns directly, the first works as intended. The second works with some unwanted side-effects. The third does not work at all and produces unwanted side-effects. These are the behaviors that I have found; Associating local RawContact A to Account X. Works as intended. RawContact A is now associated with Account X and is synced across devices. Dissociating RawContact A (setting the SyncColumns' Account name and type to null) from Account X. Partially works with some unwanted-side effects. Dissociates RawContact A from the device but not other devices. RawContact A is no longer visible in the native Contacts app UNLESS it retains the group membership to at least the default group from an Account. At this point, RawContact A is a local contact. Changes to this local RawContact A will not be synced across devices. If RawContact A is updated in another device and synced up to the server, then a syncing side-effect occurs because the RawContact A in the device is different from the RawContact A in the server. This causes the Contacts Provider to create another RawContact, resulting in a \"duplicate\". The two RawContact As may get aggregated to the same Contact depending on how similar they are. If local RawContact A is re-associated back to Account X, it will still no longer be synced. Associating RawContact A from original Account X to Account Y. Does not work and have bad side-effects. No change in other devices. For Lollipop (API 22) and below, RawContact A is no longer visible in the native Contacts app and syncing Account Y in system settings fails. For Marshmallow (API 23) and above, RawContact A is no longer visible in the native Contacts app. RawContact A is automatically deleted locally at some point by the Contacts Provider. Syncing Account Y in system settings succeeds. Given that associating originally local RawContacts to an Account is the only thing that actually works, it is the only function that will be exposed to consumers. If consumers want to transfer RawContacts from one Account to another, they can create a copy of a RawContact associated with the desired Account and then delete the original RawContact. Same idea can be used to transform an Account-associated RawContact to a local RawContact. Perhaps we can implement some functions in this library that does these things? We won't for now because the native Contacts app does not support these functions anyways. It can always be implemented later if the community really wants. Here are some other things to note. The Contacts Provider automatically creates a group membership to the default group of the target Account when the account changes. This occurs even if the group membership already exists resulting in duplicates. The Contacts Provider DOES NOT delete existing group memberships when the account changes. This has to be done manually to prevent duplicates.","title":"RawContacts; Accounts + Contacts"},{"location":"dev-notes/#rawcontacts-deletion","text":"Deleting a contact's Contacts row, RawContacts row(s), and associated Data row(s) are best explained in the documentation in ContactsContract.RawContacts ; When a raw contact is deleted, all of its Data rows as well as StatusUpdates, AggregationExceptions, PhoneLookup rows are deleted automatically. When all raw contacts associated with a Contacts row are deleted, the Contacts row itself is also deleted automatically. The invocation of resolver.delete(...), does not immediately delete a raw contacts row. Instead, it sets the ContactsContract.RawContactsColumns.DELETED flag on the raw contact and removes the raw contact from its aggregate contact. The sync adapter then deletes the raw contact from the server and finalizes phone-side deletion by calling resolver.delete(...) again and passing the ContactsContract#CALLER_IS_SYNCADAPTER query parameter. Some sync adapters are read-only, meaning that they only sync server-side changes to the phone, but not the reverse. If one of those raw contacts is marked for deletion, it will remain on the phone. However it will be effectively invisible, because it will not be part of any aggregate contact. TLDR To delete a contacts and all associated rows, simply delete all RawContact rows with the desired Contacts id. Deletion of the Contacts row and associated Data row(s) will be done automatically by the Contacts Provider. Note that deleting a RawContacts row may not immediately delete the RawContacts row. In this case, it is marked as deleted and its reference to a contact id is nulled. The Contact may still exist if it still has at least one constituent RawContact that is not marked for deletion. \u2139\ufe0f A RawContact is marked for deletion as specified by RawContactsColumns.DELETED . Typically, deleting RawContacts immediately removes the row from the RawContacts table. However, RawContacts row remains and is simply marked for deletion UNTIL the sync adapters syncs the changes. One of the reasons syncs do not occur is when the system sync settings are turned off for the Account or there is no network connection. Such RawContacts should not be included in query results for Contacts. The AOSP and Google Contacts app also does not show them. Note that local RawContacts rows (not associated with an Account) are deleted immediately as no sync needs to occur.","title":"RawContacts; Deletion"},{"location":"dev-notes/#multiple-rawcontacts-per-contact","text":"Each row in the Contacts table may be associated with more than one row in the RawContacts table. The Contacts Provider may consolidate multiple contacts belonging to different accounts and combine them into a single entry in the Contacts table whilst maintaining the separate entries in the RawContacts table. A more likely scenario that causes multiple RawContacts per Contact is when two or more Contacts are \"linked\" (or \"merged\" for API 23 and below, or \"joined\" for API 22 and below).","title":"Multiple RawContacts Per Contact"},{"location":"dev-notes/#behavior-of-linkingmergingjoining-contacts-aggregationexceptions","text":"The native Contacts app terminology has changed over time; API 22 and below; join / separate API 23; merge / unmerge API 24 and above; link / unlink However, the internals have not changed; KEEP_TOGETHER / KEEP_SEPARATE . These operations are supported by the ContactsContract.AggregationExceptions . For example, given the following tables, ### Contacts table Contact id: 32, displayName: X, starred: 0, timesContacted: 1, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 Contact id: 33, displayName: Y, starred: 1, timesContacted: 2, lastTimeContacted: 1573071750624, customRingtone: content://media/internal/audio/media/115, sendToVoicemail: 1 ### RawContacts table RawContact id: 30, contactId: 32, displayName: X, accountName: x@x.com, accountType: com.google, starred: 0, timesContacted: 1, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 RawContact id: 31, contactId: 33, displayName: Y, accountName: y@y.com, accountType: com.google, starred: 1, timesContacted: 2, lastTimeContacted: 1573071750624, customRingtone: content://media/internal/audio/media/115, sendToVoicemail: 1 ### Data table Data id: 57, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/group_membership, data1: 18 Data id: 58, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/name, data1: X, isPrimary: 1, isSuperPrimary: 1 Data id: 59, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: x@x.com Data id: 60, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: xx@x.com, isPrimary: 1, isSuperPrimary: 1 Data id: 63, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/group_membership, data1: 6 Data id: 64, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/name, data1: Y, isPrimary: 1, isSuperPrimary: 1 Data id: 65, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/email_v2, data1: y@y.com Data id: 66, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/email_v2, data1: yy@y.com, isPrimary: 1, isSuperPrimary: 1 When Contact X links/merges/joins Contact Y , the tables becomes; ### Contacts table Contact id: 32, displayName: X, starred: 1, timesContacted: 2, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 ### RawContacts table RawContact id: 30, contactId: 32, displayName: X, accountName: x@x.com, accountType: com.google, starred: 0, timesContacted: 1, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 RawContact id: 31, contactId: 32, displayName: Y, accountName: y@y.com, accountType: com.google, starred: 1, timesContacted: 2, lastTimeContacted: 1573071750624, customRingtone: content://media/internal/audio/media/115, sendToVoicemail: 1 ### Data table Data id: 57, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/group_membership, data1: 18 Data id: 58, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/name, data1: X, isPrimary: 1, isSuperPrimary: 1 Data id: 59, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: x@x.com Data id: 60, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: xx@x.com, isPrimary: 1, isSuperPrimary: 0 Data id: 63, rawContactId: 31, contactId: 32, mimeType: vnd.android.cursor.item/group_membership, data1: 6 Data id: 64, rawContactId: 31, contactId: 32, mimeType: vnd.android.cursor.item/name, data1: Y, isPrimary: 1, isSuperPrimary: 0 Data id: 65, rawContactId: 31, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: y@y.com Data id: 66, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/email_v2, data1: yy@y.com, isPrimary: 1, isSuperPrimary: 0 What changed? Contact Y's row has been deleted and its column values have been merged into Contact X row. If the reverse occurred (Contact Y merged with Contact X), Contact Y's row would still be deleted. The difference is that Contact X's display name will be set to Contact Y's display name, which is done by the native Contacts app manually by setting Contact Y's Data name row to be the \"default\" (isPrimary and isSuperPrimary both set to 1). \u2139\ufe0f The AggregationExceptions table records the linked RawContacts' IDs in ascending order regardless of the order used in RAW_CONTACT_ID1 and RAW_CONTACT_ID2 at the time of merging. The RawContacts and Data table remains the same except the joined contactId column values have now been changed to the id of Contact X. All Data rows' isSuperPrimary value has been set to 0 though the isPrimary columns remain the same. In other words, this clears any \"default\" set before the link. These are done automatically by the Contacts Provider during the link operation. What is not done automatically by the Contacts Provider is that the name row of former Contact X is set as the default. The native Contacts app does this manually. The Contacts Providers automatically sets the Contact display name to whatever the default name row is for the Contact, if available. For more info on Contact display name resolution, read the Contact Display Name and Default Name Rows section. \u2139\ufe0f Display name resolution is different for APIs below 21 (pre-lollipop). The display name of the RawContacts remain the same. The Groups table remains unmodified. Options updates Changes to the options (starred, timesContacted, lastTimeContacted, customRingtone, and sendToVoicemail) of a RawContact may affect the options of the parent Contact. On the other hand, changes to the options of the parent Contact will be propagated to all child RawContact options. Photo updates A RawContact may have a full-sized photo saved as a file and a thumbnail version of that saved in the Data table in a photo mimetype row. A Contact's full-sized photo and thumbnail are simply references to the \"chosen\" RawContact's full-sized photo and thumbnail (though the URIs may differ). \u2139\ufe0f When removing the photo in the native contacts app, the photo data row is not immediately deleted, though the PHOTO_FILE_ID is immediately set to null. This may result in the PHOTO_URI and PHOTO_THUMBNAIL_URI to still have a valid image uri even though the photo has been \"removed\". This library immediately deletes the photo data row, which seems to work perfectly. Data inserts In the native Contacts app, Data inserted in combined (raw) contacts mode will be associated to the first RawContact in the list sorted by the RawContact ID. \u2139\ufe0f This may not be the same as the RawContact referenced by ContactsColumns.NAME_RAW_CONTACT_ID . UI changes? The native Contacts App does not display the groups field when displaying / editing Contacts that have multiple RawContacts (linked/merged/joined) in combined mode. However, it does allow editing individual RawContact Data rows in which case the groups field is displayed and editable. In the native Contacts app, the name attribute used comes from the name row with IS_SUPER_PRIMARY set to true. This and all other \"unique\" mimetypes (organization) and non-unique mimetypes (email) per RawContact are shown only if they are not blank. Showing multiple RawContact's data in the same edit screen (combined mode) In older version of the native, Android Open Source Project (AOSP) Contacts app, data from multiple RawContacts was being shown in the same edit screen. This caused a lot of confusion about which data belonged to which RawContact. Newer versions of AOSP Contacts only allow editing one RawContact at a time to avoid confusion. Though, several RawContacts' data are still shown (not-editable) in the same screen.","title":"Behavior of linking/merging/joining contacts (AggregationExceptions)"},{"location":"dev-notes/#aggregationexceptions-table","text":"Given the following Contacts and their RawContacts; Contact A RawContact 1 Contact B RawContact 2 Contact C RawContact 3 Contact D RawContact 4 Linking one by one in this order; Contact B link Contact A Contact C link Contact D Contact C link Contact B Results in the following AggregationExceptions rows respectively; Aggregation exception id: 430, type: 1, rawContactId1: 1, rawContactId2: 2 Aggregation exception id: 430, type: 1, rawContactId1: 1, rawContactId2: 2 Aggregation exception id: 432, type: 1, rawContactId1: 3, rawContactId2: 4 Aggregation exception id: 436, type: 1, rawContactId1: 1, rawContactId2: 2 Aggregation exception id: 439, type: 1, rawContactId1: 1, rawContactId2: 3 Aggregation exception id: 442, type: 1, rawContactId1: 1, rawContactId2: 4 Aggregation exception id: 440, type: 1, rawContactId1: 2, rawContactId2: 3 Aggregation exception id: 443, type: 1, rawContactId1: 2, rawContactId2: 4 Aggregation exception id: 444, type: 1, rawContactId1: 3, rawContactId2: 4 There is a pattern here. RawContact ids are sorted in ascending order and linked from least to greatest exhaustively but no double links (1-2 is the same as 2-1). RawContact 1 has a row with RawContact 2, 3, and 4. RawContact 2 has a row with RawContact 3 and 4. RawContact 3 has a row with RawContact 4. Linking all in one go; Contact C link Contact A, B, D Results in the same AggregationExceptions rows. Unlinking results in the same AggregationExceptions rows except the type is 2 (TYPE_KEEP_SEPARATE).","title":"AggregationExceptions table"},{"location":"dev-notes/#data-table","text":"The Data table uses generic column names (e.g. \"data1\", \"data2\", ...) using the column \"mimetype\" to distinguish the type of data in that generic column. For example, the column name of StructuredName.DISPLAY_NAME is the same as Email.ADDRESS , which is \"data1\". Each row in the Data table consists of a piece of RawContact data (e.g. a phone number), its \"mimetype\", and the associated RawContact and Contact id. A row does not contain all of the data for a contact. RawContacts may only have one row of certain mimetypes and may have multiple rows of other mimetypes. Here is the list. Unique mimetype per RawContact Name (StructuredName) Nickname Note Organization Photo SipAddress Non-unique mimetype per Raw Contact Address (StructuredPostal) Email Event GroupMembership Im Phone Relation Website Although some mimetypes are unique per RawContact, none of those mimetypes are unique per Contact because a Contact is an aggregate of one or more RawContacts!","title":"Data Table"},{"location":"dev-notes/#data-primary-and-super-primary-rows","text":"As per documentation, for a set of data rows with the same mimetype (e.g. a set of emails), there should only be one primary data row (e.g. email) per RawContact and one super primary data row per Contact. Furthermore, a data row that is super primary must also be primary. Unfortunately, the Contacts Provider does not do any data set validation for the Data columns IS_PRIMARY and IS_SUPER_PRIMARY . This means that it is possible to set more than one data row of the same mimetype as primary for the same RawContact and super primary for the same aggregate Contact. It is also possible to set a data row as super primary but not primary. Upholding the the contract is left to us... For example, given this relationship; Contact RawContact X Email A Email B RawContact Y Email C Email D When Emails A, B, C, and D are inserted with the RawContacts or after the RawContacts have been created, we get the following state; Email Primary Super Primary A 0 0 B 0 0 C 0 0 D 0 0 The state does not change when RawContact X is linked with RawContact Y. After setting Email A as the \"default\" email, it becomes primary and super primary; Email Primary Super Primary A 1 1 B 0 0 C 0 0 D 0 0 Then setting Email B as the default email, it becomes primary and super primary. Email A is no longer primary or super primary. Email Primary Super Primary A 0 0 B 1 1 C 0 0 D 0 0 Then setting Email C as the default email, it becomes primary and super primary. Email B is still primary because it belongs to a different RawContact than Email C. However, Email B is no longer the super primary as there can only be one per aggregate Contact. Email Primary Super Primary A 0 0 B 1 0 C 1 1 D 0 0 Then setting Email D as the default email, it becomes primary and super primary. Email C is no longer primary or super primary. Email Primary Super Primary A 0 0 B 1 0 C 0 0 D 1 1 Then clearing the default email D, removes its primary and super primary status. However, email B remains a primary but not a super primary. Email Primary Super Primary A 0 0 B 1 0 C 0 0 D 0 0 The above behavior is observed from the native Contacts app. The \"super primary\" data of an aggregate Contact is referred to as the \"default\". \u2139\ufe0f At this point, the native Contacts app still shows email B as the first email in the list even though it isn't the \"default\" (super primary) because it is still a primary. This adds a bit of confusion in my opinion, especially when more than 2, 3, or 4 RawContacts are linked. A \"fix\" would be to only order the list of emails using \"super primary\" instead of \"super primary\" and \"primary\". OR to remove the primary status of the data set of all linked RawContacts. One benefit of the native Contacts implementation of this is that it retains the primary status when unlinking RawContacts. This library should follow what the native Contacts app is doing in spirit of recreating the native experience as closely as possible, even if it seems like a lesser experience.","title":"Data Primary and Super Primary Rows"},{"location":"dev-notes/#data-table-joins","text":"All columns accessible via cursors returned from Data table queries are specified in DataColumnsWithJoins , which includes the DataColumns , ContactsColumns , and ContactOptionsColumns . In code, mentions of the \"Data table\" typically refers to the joined table. The DataColumns gives us access to all of the columns in the Data table. All other joined columns, including the ContactsColumns are appended to each row in the query. This means that the ContactsColumns ; DISPLAY_NAME , PHOTO_URI , and PHOTO_THUMBNAIL_URI are repeated for all Data rows belonging to the same Contact. The ContactOptionsColumns values joined with the Data table are the values of the Contact, not the RawContact that the Data row belongs to! The same applies to the \"display_name\".","title":"Data Table Joins"},{"location":"dev-notes/#data-updates","text":"A new row in the Data table is created for each new piece of data (e.g. email address) entered for the contact. Removing a piece of existing data results in the deletion of the row in the Data table if that row no longer contains any meaningful data (no meaningful non-null \"datax\" columns left). This is the behavior of the native Android Contacts app. Therefore, querying for null fields is not possible. For example, there may be no Data rows that exist where the email address is null. Thus, a query to search for all contacts with null email address may return 0 contacts even if there are some contacts without email addresses.","title":"Data Updates"},{"location":"dev-notes/#data-required","text":"Creating blank RawContacts without email address (or other fields), results in no rows in the Data table for the email address, and all other fields. There are a few exceptions. The following Data rows are automatically created for all contacts, if not provided; Group membership, underlying value defaults to the account's default system group Name, underlying value defaults to null Nickname, underlying value defaults to null Note, underlying value defaults to null \u2139\ufe0f All of the above rows are only automatically created for RawContacts that are associated with an Account. If a valid account is provided, the default (auto add) system group membership row is automatically created immediately by the Contacts Provider at the time of contact insertion. The name, nickname, and note are automatically created at a later time. If a valid account is not provided, none of the above data rows are automatically created. Blank RawContacts The Contacts Providers allows for RawContacts that have no rows in the Data table (let's call them \"blanks\") to exist. The native Contacts app does not allow insertion of new RawContacts without at least one data row. It also deletes blanks on update. Despite seemingly not allowing blanks, the native Contacts app shows them. There are two scenarios where blanks may exist. Contact with RawContact(s) with no Data row(s). In this case, the Contact is blank as well as its RawContact(s). Contact that has RawContact with Data row(s) and a RawContact with no Data row. In this case, the Contact and the RawContact with Data row(s) are not blank but the RawContact with no Data row is blank.","title":"Data Required"},{"location":"dev-notes/#data-structuredname","text":"The DISPLAY_NAME is the unstructured representation of the name. It is made up of structured components; PREFIX , GIVEN_NAME , MIDDLE_NAME , FAMILY_NAME , and SUFFIX . When updating or inserting a row; If the display name is null and there are non-null structured components provided (e.g. given and family name), the Contacts Provider will automatically set the display name by combining the structured components. If the display name is not null and all structured components are null, the Contacts Provider automatically (to the best of its ability) derive the values for all the structured components. If the display name and structured components are not null, the Contacts Provider does nothing automatically.","title":"Data StructuredName"},{"location":"dev-notes/#data-structuredpostal","text":"The FORMATTED_ADDRESS is the unstructured representation of the postal address. It is made up of structured components; STREET , POBOX , NEIGHBORHOOD , CITY , REGION , POSTCODE , and COUNTRY . When updating or inserting a row; If the formatted address is null and there are non-null structured components provided (e.g. street and city), the Contacts Provider will automatically set the formatted address by combining the structured components. If the formatted address is not null and all structured components are null, the Contacts Provider automatically sets the street value to the formatted address. If the formatted address and structured components are not null, the Contacts Provider does nothing automatically.","title":"Data StructuredPostal"},{"location":"dev-notes/#groups-table-accounts","text":"Contacts are assigned to one or more groups via the GroupMembership . It typically looks like this; Group id: 1, systemId: Contacts, readOnly: 1, title: My Contacts, favorites: 0, autoAdd: 1, accountName: vestrel00@gmail.com, accountType: com.google Group id: 2, systemId: null, readOnly: 1, title: Starred in Android, favorites: 1, autoAdd: 0, accountName: vestrel00@gmail.com, accountType: com.google Group id: 3, systemId: Friends, readOnly: 1, title: Friends, favorites: 0, autoAdd: 0, accountName: vestrel00@gmail.com, accountType: com.google Group id: 4, systemId: Family, readOnly: 1, title: Family, favorites: 0, autoAdd: 0, accountName: vestrel00@gmail.com, accountType: com.google Group id: 5, systemId: Coworkers, readOnly: 1, title: Coworkers, favorites: 0, autoAdd: 0, accountName: vestrel00@gmail.com, accountType: com.google Group id: 6, systemId: null, readOnly: 0, title: Custom Group, favorites: 0, autoAdd: 0, accountName: vestrel00@gmail.com, accountType: com.google The actual groups are in a separate table; Groups. Each group is associated with an Account. No group can exist without an account. It is account-exclusive. Each account will have its own set of the above system groups. This means that there may be multiple groups with the same title belonging to different accounts. System ids are typically Contacts, Friends, Family, and Coworkers. These ids are typically the same across all copies of Android. Notes; - The Contacts system group is the default group in which all raw contacts of an account belongs to. Therefore, it is typically hidden when showing the list of groups in the UI. - The starred (favorites) group is not a system group as it has null system id. However, it behaves like one in that it is read only and it comes with most (if not all) copies of the native app. Removing the Account will delete all of the associated rows in the Groups table. Groups, duplicate titles The Contacts Provider allows multiple groups with the same title (case-sensitive comparison) belonging to the same account to exist. In older versions of Android, the native Contacts app allows the creation of new groups with existing titles. In newer versions, duplicate titles are not allowed. Therefore, this library does not allow for duplicate titles. \u2139\ufe0f In newer versions, the group with the duplicate title gets deleted either automatically by the Contacts Provider or when viewing groups in the native Contacts app. It's not an immediate failure on insert or update. This could lead to bugs!","title":"Groups Table & Accounts"},{"location":"dev-notes/#groups-table-groupmemberships-data-table","text":"There may be multiple groups with the same title from different accounts. Therefore, the group membership should point to the group belonging to the same account as the raw contact. The native Contacts app displays only the groups belonging to the selected account. Updating group memberships of existing raw contacts seem to be almost instant. All raw contacts must be a part of at least the default group (system id is \"Contacts\"). Raw contacts with no group membership will be asynchronously added to the Account's default group by the Contacts Provider. Membership to the default group should never be deleted!","title":"Groups Table & GroupMemberships (Data Table)"},{"location":"dev-notes/#starred-in-android-favorites","text":"When the ContactOptionsColumns.STARRED column of a Contact in the Contacts table is set to true, the Contacts Provider automatically adds a group membership to the favorites group for all RawContacts linked to the Contact. Setting STARRED to false removes all group memberships to the favorites group. The STARRED is interdependent with group memberships to the favorites group. Adding a group membership to the favorites group results in STARRED being set to true. Removing the membership sets it to false. Raw contacts that are not associated with an account do not have any group memberships. Even though these raw contacts may not have a membership to the favorites group, they may still be \"starred\" (favorited) via the ContactOptionsColumns.STARRED column in the Contacts table, which is not dependent on the existence of a favorites group membership. Refresh RawContact instances after changing the starred value. Otherwise, performing an update on the RawContact with a stale set of group memberships may revert the star/unstar operation. For example, query returns a starred RawContact -> set starred to false -> update RawContact (still containing a group membership to the favorites group) -> starred will be set back to true.","title":"Starred in Android (Favorites)"},{"location":"dev-notes/#group-memberships-local-rawcontacts","text":"Local RawContacts may have a group membership to the default system group of an Account without being associated with the Account... The native Contacts app may not have an edit-RawContact option for newly inserted RawContacts that have no group membership to the default group when an Account is available. Though, edits can still be made in other ways. Instead, an option to \"Add to contacts\" is shown that adds a membership to the default group but does not associate the raw contact to the Account that owns the group. The edit UI does not show the group membership field. Weirdly, this only occurs when there is exactly only one Account. If there are no Accounts or there are two or more Accounts, then this does not occur. Also, this does not occur for a Contact with a RawContact that has a group membership AND a RawContact that has no group membership.","title":"Group memberships & Local RawContacts"},{"location":"dev-notes/#groups-deletion","text":"Similar to deleting RawContacts, deleting a Groups row may not immediately delete the Groups row. In this case, it is marked as deleted. \u2139\ufe0f A Group is marked for deletion as specified by GroupsColumns.DELETED . Typically, deleting Groups immediately removes the row from the Groups table. However, Groups row remains and is simply marked for deletion UNTIL the sync adapters syncs the changes. One of the reasons syncs do not occur is when the system sync settings are turned off for the Account or there is no network connection. Such Groups should not be included in query results for Contacts. The AOSP and Google Contacts app also does not show them. Note that local Groups rows (not associated with an Account) are deleted immediately as no sync needs to occur.","title":"Groups; Deletion"},{"location":"dev-notes/#groups-ui","text":"In newer Android versions of the native Contacts app, \"groups\" are now being referred to as \"labels\". However, the underlying code still uses groups. Google is probably just trying to make it more user friendly by calling it label instead of group.","title":"Groups; UI"},{"location":"dev-notes/#user-profile","text":"There exist one (profile) Contacts row that identifies the user; ContactsColumns.IS_USER_PROFILE . There is at least one RawContacts row that is associated with the user profile; RawContactsColumns.RAW_CONTACT_IS_USER_PROFILE . Associated RawContacts may or may not be associated with an Account. The RawContacts row(s) may have rows in the Data table as usual. These profile table rows have special IDs that differ from regular rows. See ContactsContract.isProfileId . \u2139\ufe0f The Contacts Provider will throw an IllegalArgument exception when attempting to include ContactsColumns.IS_USER_PROFILE and RawContactsColumns.RAW_CONTACT_IS_USER_PROFILE columns in Data table queries. I have not yet tried including these columns in the Contacts or RawContacts table queries. The profile Contact row may not be merged / linked with other contacts and do not belong to any group (favorites / starred). Profile rows in the Contacts, RawContacts, and Data table are not visible via queries in the respective tables. They will not be in the resulting cursor. To get the profile Contacts table rows, query the Profile.CONTENT_URI . To get profile RawContacts table rows, query the Profile.CONTENT_RAW_CONTACTS_URI . To get the profile Data table rows, query the Profile.CONTENT_RAW_CONTACTS_URI appended with the RawContact id and RawContacts.Data.CONTENT_DIRECTORY . To insert a new profile RawContact, use Profile.CONTENT_RAW_CONTACTS_URI . It will automatically be associated with the profile Contact. If the profile Contact does not yet exist, it will be created automatically. To insert a new profile Data row, either; insert to the Profile.CONTENT_RAW_CONTACTS_URI appended with the RawContact id and RawContacts.Data.CONTENT_DIRECTORY insert to the Data table directly, referencing the RawContact id Same rules apply to all table rows. If all profile RawContacts table rows have been deleted, then associated Contacts and Data table rows will automatically be deleted. Profile aggregation The RawContacts of a (Contact) Profile are linked via the indexed rows; Profile.CONTENT_RAW_CONTACTS_URI . Therefore, the AggregationsExceptions table is not used here. Profile and users Note that as of Android 5 Lollipop, there may exist multiple users in a device. Each user has a separate list of accounts and contact data. This also means that each user has a separate (local) profile contact. Profile and Accounts According to the Profile documentation; \"... each account (including data set, if applicable) on the device may contribute a single raw contact representing the user's personal profile data from that source.\" In other words, one account can have one profile RawContact. Whether or not profile RawContacts associated to an Account can be carried over and synced across devices and users is up to the Contacts Provider / Sync provider for that Account. \u2139\ufe0f From my experience, profile RawContacts associated to an Account is not carried over / synced across devices or users. Despite the documentation of \"one profile RawContact per one Account\", the Contacts Provider allows for multiple RawContacts per Account, including multiple local RawContacts (no Account). Thus, we should let consumers exploit this but set defaults to be one-for-one. Creating / setting up the profile in the native Contacts app results in the creation of a local RawContact (not associated with an Account) even if there are available Accounts. The Contacts Provider does not associate local contacts to an account when an account is or becomes available (regardless of API level). Removing the Account will delete all of the associated rows in the Contact, RawContact, Data, and Groups tables. This includes user Profile data in those tables. Profile permissions Profile permissions (READ_PROFILE and WRITE_PROFILE) have been removed since API 23. However, they are still required for API 22 and below. Reading and writing the profile is included in the Contacts permissions. There is no need to ask for profile permissions at runtime because prior to API 23, permissions in the AndroidManifest have to be accepted prior to installation.","title":"User Profile"},{"location":"dev-notes/#syncing-data-sync-adapters","text":"First, it\u2019s good to know the official documentation of sync adapters; https://developer.android.com/guide/topics/providers/contacts-provider#SyncAdapters Now, let\u2019s ingest the official docs\u2026 Data belonging to a RawContact that is associated with a Google account will be available anywhere the Google account is used; in any Android or iOS device, a web browser, etc\u2026 Data is synced by Google\u2019s sync adapters to and from their remote servers. Syncing depends on the account sync settings, which can be configured in the native system settings app and possibly through some remote configuration. This library does not provide any sync adapters. Instead, it relies on existing sync adapters to do the syncing. Sync adapters and syncing are really out of scope of this library. Syncing is its own thing that typically happens outside of an application UI. This library is focused on reading and writing native and custom data to and from the local database. Syncing the local database to and from a remote service is a different story altogether =)","title":"Syncing Data / Sync Adapters"},{"location":"dev-notes/#custom-data-mimetypes","text":"First, it\u2019s good to know the official documentation of custom data rows; https://developer.android.com/guide/topics/providers/contacts-provider#CustomData Now, let\u2019s ingest the official docs\u2026 Custom mimetypes do not belong to the native Contacts Provider mimetype set (e.g. address, email, phone, etc). The Contacts Provider allows for the creation of new / custom mimetypes. This is especially useful for other apps (Google Contacts, Facebook, Twitter, WhatsApp, etc) that want to attach extra pieces of data to a particular RawContact. Custom data are NOT synced, including those that belong to RawContacts that are associated with an Account. Custom sync adapters are required to sync custom data. This library currently does NOT provide custom sync adapters to sync custom data! Custom data from other apps such as Facebook, Twitter, WhatsApp, etc may or may not be synced. It all depends on those applications and their custom sync adapters (if they have any) and sync settings. For insight on how aforementioned social media services may be syncing their data, read through the official documentation; https://developer.android.com/guide/topics/providers/contacts-provider#SocialStream","title":"Custom Data / MimeTypes"},{"location":"dev-notes/#unused-contactscontract-stuff","text":"We are currently not utilizing these things because I haven't found usages of them while using the native Contacts app. They are probably working behind the scenes but until we find uses for these, let's leave it out because YAGNI . Settings . Contacts-specific settings for various Accounts (settings for an Account). Might be useful to add this for SHOULD_SYNC and UNGROUPED_VISIBLE . ContactsColumns.IN_VISIBLE_GROUP + Groups.GROUP_VISIBLE . Flag indicating if the contacts belonging to this group should be visible in any user interface.","title":"Unused ContactsContract Stuff"},{"location":"dev-notes/#java-support","text":"This library is intended to be Java-friendly. The policy is that we should attempt to write Java-friendly code that does not increase lines of code by much or add external dependencies to cater exclusively to Java users.","title":"Java Support"},{"location":"dev-notes/#creating-entities-data-class","text":"First, consumers are not allowed to create immutable entities. Those must come from the API itself to ensure data integrity. Whether or not we will change this in the future is debatable =) Consumers are able to set read-only and private or internal variables though because all Entity implementations are data classes. Data classes provide a copy function that allows for setting any property no matter their visibility and even if the constructor is private. As a matter of fact, setting the constructor of a data class as private gives this warning by Android Studio: \"Private data class constructor is exposed via the 'copy' method. There is currently no way to disable the copy function of data classes (that I know of). The only thing we can do is to provide documentation to consumers, insisting against the use of the copy method as it may lead to unwanted side effects when updating and deleting contacts. \u2139\ufe0f We could just use regular classes instead of data classes but entities should be data classes because it is what they are (know what I mean?!). Also, I'd hate to have to generate equals and hashcode functions for them, which will make the code harder to maintain. Though, we might do this anyways at some point if we want to make it possible for a mutable entity to equal an immutable entity. Time will tell =) FIXME? Hide / disable data class copy function if kotlin ever allows it. https://discuss.kotlinlang.org/t/data-class-copy-visibility-modifier/19746","title":"Creating Entities & data class"},{"location":"dev-notes/#immutable-vs-mutable-entities","text":"This library provides true immutability for immutable entities. Take a look at the current (simplified) hierarchy; 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 > ) : RawContactEntity data class Address ( val formattedAddress : String? ) data class MutableAddress ( var formattedAddress : String? ) \u2139\ufe0f The use of sealed class is to prevent consumers from defining their own entities. This restriction may or may not change in the future. Notice that there is nothing mutable in the immutable Contact . Everything are val s and the data structures used (i.e. RawContact , Address , and List ) are all immutable. This provides consumers 100% confidence that immutable entities are not mutable. They will not change or mutate in any way. Once they are constructed, they will always remain the same. Why immutability is so important will not be covered in this dev notes because it would be too big (that's what she said) and there are blogs and books written about this. One of the most important advantages of immutability is that it is thread-safe. Immutable instances can be used in several different threads without the need for synchronization and worries about deadlocks. In other words, they are thread-safe and faster than the mutable version. The current structure also allows consumers to be able to distinguish between immutable and mutable entities exhaustively. E.G. fun doSomethingAndReturn ( contact : ContactEntity ) = when ( contact ) { is Contact -> {} is MutableContact -> {} } \u2139\ufe0f The mutable entities provided in this library are NOT thread-safe . Consumers will have to perform their own synchronizations if they want to use and mutate mutable entities in multi-threaded scenarios.","title":"Immutable vs Mutable Entities"},{"location":"dev-notes/#the-cost-of-the-current-immutability-implementation","text":"The cost of implementing true immutability is more lines of code. Notice that the MutableContact does not inherit from Contact . The same goes for the other entities. This leads to having to write seemingly duplicate code when writing functions and extensions. // FIXME? Furthermore, equality between immutable and mutable entities are not yet implemented. This means that Contact(\"john\") == MutableContact(\"john\") will return false even though their underlying contents are the same. This can be fixed by overriding the equals and hashcode functions of all entities. However, that is a lot more code that I would like to avoid, which is why I'm using data class for all entities in the first place! This may change in the future if the community really wants to change it =) On a side note, the same cost is incurred by Kotlin's standard libs. For example, notice that AbstractMutableList does not inherit from and is completely separate from AbstractList . I'm sure stdlib devs also had to write seemingly duplicate code in implementations of the List interface.","title":"The cost of the current immutability implementation"},{"location":"dev-notes/#avoiding-the-cost-shortcuts-and-pitfalls","text":"One thing that may come to mind in attempts to reduce lines of seemingly duplicate code is to have just a mutable implementation of an immutable declaration. For example, we can restructure the hierarchy to; sealed interface Contact { val rawContacts : List < RawContact > } data class MutableContact ( override val rawContacts : List < MutableRawContact > ) : Contact sealed interface RawContact { val addresses : List < Address > } data class MutableRawContact ( override var addresses : MutableList < MutableAddress > ) : RawContact sealed interface Address { val formattedAddress : String? } data class MutableAddress ( override var formattedAddress : String? ) : Address Notice that there is a non-concrete declaration (i.e. Contact , RawContact , and Address ) and just one concrete implementation (i.e. MutableContact , MutableRawContact , and MutableAddress ). \u2139\ufe0f A val declaration can be overridden by a var . Keep in mind that val only requires getters whereas var requires both getters and setters. Therefore, a var cannot be overridden by a val . Or maybe there is a different reason Kotlin imposes this restriction. On a similar note, the List interface can be overridden to a MutableList . We, as API contributors, can avoid having to write seemingly duplicate functions and extensions! However! Can you see what's wrong with this setup? If we do this, we would either be deceiving consumers to think that the instances of \"immutable\" class signatures (i.e. Contact , RawContact , and Address ) are actually immutable OR we would have to let consumers know that the API does not really provide true immutability. Neither option is ideal (nor is it acceptable IMO). Consumers would have a reference to a Contact , which they may assume is immutable because of the usage of val instead of var , but in actuality the underlying implementation is mutable... This could be a cause of really hard to find bugs in multi-threaded usage. Consumers may use Contact with the assumption that it is immutable only to find that it can actually be mutated! We could fix this by just making the mutable implementation thread-safe but since that is the only implementation, consumers will be forced to use thread-safe code when they don't have to thereby negatively affecting performance. Keep in mind that thread safety is only one of several reasons for immutability. Those other reasons will be violated too. Consumers will be shocked if they ever do the following or something similar. fun x ( contact : Contact ) = when ( contact ) { is MutableContact -> {} // this is always true is Contact -> {} // this is always true } In any case, I have to admit, it is a nice trick that would save API contributors time. But that's just it! It's just a trick. A shortcut. A nice little time save at the cost of integrity. It is not worth it (IMO).","title":"Avoiding the cost... Shortcuts and pitfalls."},{"location":"dev-notes/#why-not-add-android-x-support-library-dependencies","text":"I want to keep the dependency list of this library to a minimum. The Contacts Provider is native to Android since the beginning. I want to honor that fact by avoiding adding dependencies here. I made a bit of an exception by adding the Dexter library for permissions handling for the permissions modules (not in the core modules). I'm tempted to remove the Dexter dependency and implement permissions handling myself because Dexter brings in a lot of other dependencies with it. However, it is not part of the core module so I'm able to live with this. TODO Remove/replace Dexter. It is no longer being maintained. Keeping dependencies to a minimum is just a small challenge I made up. We will see how long it can last! I left comments all over the code on when an androidx dependency may be useful. The most glaring example of this is @WorkerThread. Even with that, I'll hold off on adding the androidx annotation lib. I think we can all be consenting adults =) If the community strongly desires the addition of these support libs, then the community will win =)","title":"Why Not Add Android X / Support Library Dependencies?"},{"location":"accounts/associate-device-local-raw-contacts-to-an-account/","text":"Associate a local RawContact to an Account \u00b6 This library provides the AccountsLocalRawContactsUpdate API, which allows you to associate local RawContacts (those that are not associated with an Account) to an Account in order to enable syncing. An instance of the AccountsLocalRawContactsUpdate API is obtained by, val accountsLocalRawContactsUpdate = Contacts ( context ). accounts (). updateLocalRawContactsAccount () \u2139\ufe0f For more info on local RawContacts, read about Local (device-only) contacts . \u2139\ufe0f For more info on syncing, read Sync contact data across devices . Basic usage \u00b6 To associate/add the given local RawContacts to the given account, val updateResult = accountsLocalRawContactsUpdate . addToAccount ( account ) . localRawContacts ( rawContacts ) . commit () Executing the update \u00b6 To execute the update, . commit () Handling the update result \u00b6 The commit function returns a Result , To check if all updates succeeded, val allUpdatesSuccessful = updateResult . isSuccessful To check if a particular update succeeded, val firstUpdateSuccessful = updateResult . isSuccessful ( rawContact1 ) Handling update failure \u00b6 The update may fail for a particular RawContact for various reasons, updateResult . failureReason ( rawContact1 ) ?. let { when ( it ) { INVALID_ACCOUNT -> handleInvalidAccount () RAW_CONTACT_IS_NOT_LOCAL -> handleRawContactIsNotLocal () UNKNOWN -> handleUnknownFailure () } } Cancelling the update \u00b6 To cancel an update amid execution, . commit { returnTrueIfUpdateShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel update processing as soon as possible. The function is called numerous times during update processing to check if processing should stop or continue. This gives you the option to cancel the update. For example, to automatically cancel the update inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val updateResult = update . commit { ! isActive } } } Performing the update and result processing asynchronously \u00b6 Updates are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the update with permission \u00b6 These updates require the android.permission.GET_ACCOUNTS and android.permission.WRITE_CONTACTS . If not granted, the update will do nothing and return a failed result. To perform the update with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Profile data \u00b6 The AccountsLocalRawContactsUpdate API also supports updating the Profile (device owner) RawContacts. To get an instance of this API for Profile data updates, val profileDataUpdate = Contacts ( context ). accounts (). profile (). updateLocalRawContactsAccount () All updates will be limited to the Profile RawContacts, whether it exists or not. Developer notes (or for advanced users) \u00b6 Due to certain limitations and behaviors imposed by the Contacts Provider, this library only provides an API to support; Associate local RawContacts (those that are not associated with an Account) to an Account, allowing syncing between devices. The library does not provide an API that supports; Dissociate RawContacts from their Account such that they remain local to the device and not synced between devices. Transfer RawContacts from one Account to another. SyncColumns modifications \u00b6 This library supports modifying the SyncColumns.ACCOUNT_NAME and SyncColumns.ACCOUNT_TYPE of the RawContacts table in some cases only. In some cases does not work as intended and produces unwanted side-effects. It probably has something to do with syncing with remote servers and local Account / sync data not matching up similar to errors on network requests if the system time does not match network time. The motivation behind changing the Account columns of the RawContacts table rows is that it would allow users to; Associate local RawContacts (those that are not associated with an Account) to an Account, allowing syncing between devices. Dissociate RawContacts from their Account such that they remain local to the device and not synced between devices. Transfer RawContacts from one Account to another. When modifying the SyncColumns directly, the first works as intended. The second works with some unwanted side-effects. The third does not work at all and produces unwanted side-effects. These are the behaviors that I have found; Associating local RawContact A to Account X. Works as intended. RawContact A is now associated with Account X and is synced across devices. Dissociating RawContact A (setting the SyncColumns' Account name and type to null) from Account X. Partially works with some unwanted-side effects. Dissociates RawContact A from the device but not other devices. RawContact A is no longer visible in the native Contacts app UNLESS it retains the group membership to at least the default group from an Account. At this point, RawContact A is a local contact. Changes to this local RawContact A will not be synced across devices. If RawContact A is updated in another device and synced up to the server, then a syncing side-effect occurs because the RawContact A in the device is different from the RawContact A in the server. This causes the Contacts Provider to create another RawContact, resulting in a \"duplicate\". The two RawContact As may get aggregated to the same Contact depending on how similar they are. If local RawContact A is re-associated back to Account X, it will still no longer be synced. Associating RawContact A from original Account X to Account Y. Does not work and have bad side-effects. No change in other devices. For Lollipop (API 22) and below, RawContact A is no longer visible in the native Contacts app and syncing Account Y in system settings fails. For Marshmallow (API 23) and above, RawContact A is no longer visible in the native Contacts app. RawContact A is automatically deleted locally at some point by the Contacts Provider. Syncing Account Y in system settings succeeds. Given that associating originally local RawContacts to an Account is the only thing that actually works, it is the only function that will be exposed to consumers. If consumers want to transfer RawContacts from one Account to another, they can create a copy of a RawContact associated with the desired Account and then delete the original RawContact. Same idea can be used to transform an Account-associated RawContact to a local RawContact. Perhaps we can implement some functions in this library that does these things? We won't for now because the native Contacts app does not support these functions anyways. It can always be implemented later if the community really wants. Here are some other things to note. The Contacts Provider automatically creates a group membership to the default group of the target Account when the account changes. This occurs even if the group membership already exists resulting in duplicates. The Contacts Provider DOES NOT delete existing group memberships when the account changes. This has to be done manually to prevent duplicates.","title":"Associate a local RawContact to an Account"},{"location":"accounts/associate-device-local-raw-contacts-to-an-account/#associate-a-local-rawcontact-to-an-account","text":"This library provides the AccountsLocalRawContactsUpdate API, which allows you to associate local RawContacts (those that are not associated with an Account) to an Account in order to enable syncing. An instance of the AccountsLocalRawContactsUpdate API is obtained by, val accountsLocalRawContactsUpdate = Contacts ( context ). accounts (). updateLocalRawContactsAccount () \u2139\ufe0f For more info on local RawContacts, read about Local (device-only) contacts . \u2139\ufe0f For more info on syncing, read Sync contact data across devices .","title":"Associate a local RawContact to an Account"},{"location":"accounts/associate-device-local-raw-contacts-to-an-account/#basic-usage","text":"To associate/add the given local RawContacts to the given account, val updateResult = accountsLocalRawContactsUpdate . addToAccount ( account ) . localRawContacts ( rawContacts ) . commit ()","title":"Basic usage"},{"location":"accounts/associate-device-local-raw-contacts-to-an-account/#executing-the-update","text":"To execute the update, . commit ()","title":"Executing the update"},{"location":"accounts/associate-device-local-raw-contacts-to-an-account/#handling-the-update-result","text":"The commit function returns a Result , To check if all updates succeeded, val allUpdatesSuccessful = updateResult . isSuccessful To check if a particular update succeeded, val firstUpdateSuccessful = updateResult . isSuccessful ( rawContact1 )","title":"Handling the update result"},{"location":"accounts/associate-device-local-raw-contacts-to-an-account/#handling-update-failure","text":"The update may fail for a particular RawContact for various reasons, updateResult . failureReason ( rawContact1 ) ?. let { when ( it ) { INVALID_ACCOUNT -> handleInvalidAccount () RAW_CONTACT_IS_NOT_LOCAL -> handleRawContactIsNotLocal () UNKNOWN -> handleUnknownFailure () } }","title":"Handling update failure"},{"location":"accounts/associate-device-local-raw-contacts-to-an-account/#cancelling-the-update","text":"To cancel an update amid execution, . commit { returnTrueIfUpdateShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel update processing as soon as possible. The function is called numerous times during update processing to check if processing should stop or continue. This gives you the option to cancel the update. For example, to automatically cancel the update inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val updateResult = update . commit { ! isActive } } }","title":"Cancelling the update"},{"location":"accounts/associate-device-local-raw-contacts-to-an-account/#performing-the-update-and-result-processing-asynchronously","text":"Updates are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the update and result processing asynchronously"},{"location":"accounts/associate-device-local-raw-contacts-to-an-account/#performing-the-update-with-permission","text":"These updates require the android.permission.GET_ACCOUNTS and android.permission.WRITE_CONTACTS . If not granted, the update will do nothing and return a failed result. To perform the update with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the update with permission"},{"location":"accounts/associate-device-local-raw-contacts-to-an-account/#profile-data","text":"The AccountsLocalRawContactsUpdate API also supports updating the Profile (device owner) RawContacts. To get an instance of this API for Profile data updates, val profileDataUpdate = Contacts ( context ). accounts (). profile (). updateLocalRawContactsAccount () All updates will be limited to the Profile RawContacts, whether it exists or not.","title":"Profile data"},{"location":"accounts/associate-device-local-raw-contacts-to-an-account/#developer-notes-or-for-advanced-users","text":"Due to certain limitations and behaviors imposed by the Contacts Provider, this library only provides an API to support; Associate local RawContacts (those that are not associated with an Account) to an Account, allowing syncing between devices. The library does not provide an API that supports; Dissociate RawContacts from their Account such that they remain local to the device and not synced between devices. Transfer RawContacts from one Account to another.","title":"Developer notes (or for advanced users)"},{"location":"accounts/associate-device-local-raw-contacts-to-an-account/#synccolumns-modifications","text":"This library supports modifying the SyncColumns.ACCOUNT_NAME and SyncColumns.ACCOUNT_TYPE of the RawContacts table in some cases only. In some cases does not work as intended and produces unwanted side-effects. It probably has something to do with syncing with remote servers and local Account / sync data not matching up similar to errors on network requests if the system time does not match network time. The motivation behind changing the Account columns of the RawContacts table rows is that it would allow users to; Associate local RawContacts (those that are not associated with an Account) to an Account, allowing syncing between devices. Dissociate RawContacts from their Account such that they remain local to the device and not synced between devices. Transfer RawContacts from one Account to another. When modifying the SyncColumns directly, the first works as intended. The second works with some unwanted side-effects. The third does not work at all and produces unwanted side-effects. These are the behaviors that I have found; Associating local RawContact A to Account X. Works as intended. RawContact A is now associated with Account X and is synced across devices. Dissociating RawContact A (setting the SyncColumns' Account name and type to null) from Account X. Partially works with some unwanted-side effects. Dissociates RawContact A from the device but not other devices. RawContact A is no longer visible in the native Contacts app UNLESS it retains the group membership to at least the default group from an Account. At this point, RawContact A is a local contact. Changes to this local RawContact A will not be synced across devices. If RawContact A is updated in another device and synced up to the server, then a syncing side-effect occurs because the RawContact A in the device is different from the RawContact A in the server. This causes the Contacts Provider to create another RawContact, resulting in a \"duplicate\". The two RawContact As may get aggregated to the same Contact depending on how similar they are. If local RawContact A is re-associated back to Account X, it will still no longer be synced. Associating RawContact A from original Account X to Account Y. Does not work and have bad side-effects. No change in other devices. For Lollipop (API 22) and below, RawContact A is no longer visible in the native Contacts app and syncing Account Y in system settings fails. For Marshmallow (API 23) and above, RawContact A is no longer visible in the native Contacts app. RawContact A is automatically deleted locally at some point by the Contacts Provider. Syncing Account Y in system settings succeeds. Given that associating originally local RawContacts to an Account is the only thing that actually works, it is the only function that will be exposed to consumers. If consumers want to transfer RawContacts from one Account to another, they can create a copy of a RawContact associated with the desired Account and then delete the original RawContact. Same idea can be used to transform an Account-associated RawContact to a local RawContact. Perhaps we can implement some functions in this library that does these things? We won't for now because the native Contacts app does not support these functions anyways. It can always be implemented later if the community really wants. Here are some other things to note. The Contacts Provider automatically creates a group membership to the default group of the target Account when the account changes. This occurs even if the group membership already exists resulting in duplicates. The Contacts Provider DOES NOT delete existing group memberships when the account changes. This has to be done manually to prevent duplicates.","title":"SyncColumns modifications"},{"location":"accounts/query-accounts/","text":"Query for Accounts \u00b6 This library provides the AccountsQuery API that allows you to retrieve Account s from the AccountManager . An instance of the AccountsQuery API is obtained by, val query = Contacts ( context ). accounts (). query () A basic query \u00b6 To get all available accounts in the system, val accounts = Contacts ( context ). accounts (). query () . find () To get all available accounts in the system with an account type of \"com.google\" or \"com.yahoo\", val accounts = Contacts ( context ). accounts (). query () . withTypes ( \"com.google\" , \"com.yahoo\" ) . find () To get the account for a set of RawContacts, val account = Contacts ( context ). accounts (). query () . associatedWith ( rawContacts ) . find () To get all available accounts in the system with an account type of \"com.google\" or \"com.yahoo\" AND is associated with at least one of the given RawContacts, val accounts = Contacts ( context ). accounts (). query () . withTypes ( \"com.google\" , \"com.yahoo\" ) . associatedWith ( rawContacts ) . find () \u2139\ufe0f RawContacts that are not associated with an Account are local to the device. For more info, read about Local (device-only) contacts . Account for each specified RawContact \u00b6 When you perform a query that uses associatedWith without using withTypes , you are able to get the Account for each of the RawContact specified. val rawContactAccount = accounts . accountFor ( rawContact ) This allows you to get the accounts for multiple RawContacts in one API call =) Cancelling the query \u00b6 To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val contacts = query . find { ! isActive } } } Performing the query asynchronously \u00b6 Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the query with permission \u00b6 Queries require the android.permission.READ_CONTACTS and android.permission.GET_ACCOUNTS permissions. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Profile accounts \u00b6 The AccountsQuery API also supports querying the Profile (device owner) contact data. To get an instance of this API for Profile queries, val query = Contacts ( context ). accounts (). profile (). query () All queries will be limited to the Profile, whether it exists or not.","title":"Query for Accounts"},{"location":"accounts/query-accounts/#query-for-accounts","text":"This library provides the AccountsQuery API that allows you to retrieve Account s from the AccountManager . An instance of the AccountsQuery API is obtained by, val query = Contacts ( context ). accounts (). query ()","title":"Query for Accounts"},{"location":"accounts/query-accounts/#a-basic-query","text":"To get all available accounts in the system, val accounts = Contacts ( context ). accounts (). query () . find () To get all available accounts in the system with an account type of \"com.google\" or \"com.yahoo\", val accounts = Contacts ( context ). accounts (). query () . withTypes ( \"com.google\" , \"com.yahoo\" ) . find () To get the account for a set of RawContacts, val account = Contacts ( context ). accounts (). query () . associatedWith ( rawContacts ) . find () To get all available accounts in the system with an account type of \"com.google\" or \"com.yahoo\" AND is associated with at least one of the given RawContacts, val accounts = Contacts ( context ). accounts (). query () . withTypes ( \"com.google\" , \"com.yahoo\" ) . associatedWith ( rawContacts ) . find () \u2139\ufe0f RawContacts that are not associated with an Account are local to the device. For more info, read about Local (device-only) contacts .","title":"A basic query"},{"location":"accounts/query-accounts/#account-for-each-specified-rawcontact","text":"When you perform a query that uses associatedWith without using withTypes , you are able to get the Account for each of the RawContact specified. val rawContactAccount = accounts . accountFor ( rawContact ) This allows you to get the accounts for multiple RawContacts in one API call =)","title":"Account for each specified RawContact"},{"location":"accounts/query-accounts/#cancelling-the-query","text":"To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val contacts = query . find { ! isActive } } }","title":"Cancelling the query"},{"location":"accounts/query-accounts/#performing-the-query-asynchronously","text":"Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the query asynchronously"},{"location":"accounts/query-accounts/#performing-the-query-with-permission","text":"Queries require the android.permission.READ_CONTACTS and android.permission.GET_ACCOUNTS permissions. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the query with permission"},{"location":"accounts/query-accounts/#profile-accounts","text":"The AccountsQuery API also supports querying the Profile (device owner) contact data. To get an instance of this API for Profile queries, val query = Contacts ( context ). accounts (). profile (). query () All queries will be limited to the Profile, whether it exists or not.","title":"Profile accounts"},{"location":"accounts/query-raw-contacts/","text":"Query RawContacts \u00b6 This library provides the AccountsRawContactsQuery API that allows you to get a list of RawContacts matching a specific search criteria. More specifically, this query returns BlankRawContact s, which are RawContacts that contains no data (e.g. email, phone). It only contains critical information required for performing RawContact operations such as associating local RawContacts to an Account. \u2139\ufe0f For more info, read Associate local RawContacts to an Account . An instance of the AccountsRawContactsQuery API is obtained by, val query = Contacts ( context ). accounts (). queryRawContacts () A basic query \u00b6 To get all RawContacts as blanks, val rawContacts = Contacts ( context ). accounts (). queryRawContacts (). find () Specifying Accounts \u00b6 To limit the search to only those RawContacts associated with one of the given accounts, . accounts ( accounts ) For example, to limit the search to contacts belonging to only one account, . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) \u2139\ufe0f For more info, read Query for Accounts . If no accounts are specified (this function is not called or called with no Accounts), then all RawContacts of Contacts are included in the search. A null Account may be provided here, which results in RawContacts with no associated Account to be included in the search. RawContacts without an associated account are considered local contacts or device-only contacts, which are not synced. For more info, read about Local (device-only) contacts . \u2139\ufe0f This may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it. Ordering \u00b6 To order resulting RawContacts using one or more fields, . orderBy ( fieldOrder ) For example, to order RawContacts by account type, . orderBy ( RawContactsFields . AccountType . asc ()) String comparisons ignores case by default. Each orderBys provides ignoreCase as an optional parameter. Use RawContactsFields to construct the orderBys. Limiting and offsetting \u00b6 To limit the amount of RawContacts returned and/or offset (skip) a specified number of RawContacts, . limit ( limit ) . offset ( offset ) For example, to only get a maximum 20 RawContacts, skipping the first 20, . limit ( 20 ) . offset ( 20 ) This is useful for pagination =) \u2139\ufe0f It is recommended to limit the number of RawContacts when querying to increase performance and decrease memory cost. Executing the query \u00b6 To execute the query, . find () Cancelling the query \u00b6 To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val rawContacts = query . find { ! isActive } } } Performing the query asynchronously \u00b6 Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes. Performing the query with permission \u00b6 Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) RawContacts from more than one account in the same list \u00b6 When you perform a query that returns groups from more than one account, you will get everything in the same BlankRawContactsList . This list is just like any other List except it also provides an extra function that allows you to get a sublist with RawContacts belonging only to a particular account. val rawContactsFromAccount = blankRawContactsList . from ( account ) Getting Contacts and RawContacts from BlankRawContacts \u00b6 If you want to get the Contacts and all associated RawContacts and Data from a set of BlankRawContact s, val contacts = Contacts ( context ) . query () . where { RawContact . Id `in` blankRawContactIds } . find () \u2139\ufe0f For more info, read Query contacts (advanced) . If you need a more convenient way to convert the BlankRawContact s to RawContacts , use BlankRawContactToRawContact extensions. For more info, read Convenience functions . Profile RawContacts \u00b6 The AccountsRawContactsQuery API also supports querying the Profile (device owner) RawContacts. To get an instance of this API for Profile queries, val query = Contacts ( context ). accounts (). profile (). queryRawContacts () All queries will be limited to the Profile, whether it exists or not. Using the where function to specify matching criteria \u00b6 Use the contacts.core.RawContactsField combined with the extensions from contacts.core.Where to form WHERE clauses. \u2139\ufe0f This docs page will not provide a tutorial on database where clauses. It assumes that you know the basics. If you don't know the basics, then search for sqlite where clause . For example, to get a list of RawContacts with the given IDs, val favoriteRawContacts = Contacts ( context ) . accounts () . queryRawContacts () . where { Id `in` rawContactIds } . find () Limitations \u00b6 This library only provides basic WHERE functions. It does not cover the entirety of SQLite, though the community may add more over time <3 Furthermore, this library is constrained by rules and limitations set by the Contacts Provider and the behavior of the native Contacts app. One such rule/limitation has resulted in this library not providing WHERE functions such as isNull or isNullOrEmpty to prevent making misleading queries.","title":"Query RawContacts"},{"location":"accounts/query-raw-contacts/#query-rawcontacts","text":"This library provides the AccountsRawContactsQuery API that allows you to get a list of RawContacts matching a specific search criteria. More specifically, this query returns BlankRawContact s, which are RawContacts that contains no data (e.g. email, phone). It only contains critical information required for performing RawContact operations such as associating local RawContacts to an Account. \u2139\ufe0f For more info, read Associate local RawContacts to an Account . An instance of the AccountsRawContactsQuery API is obtained by, val query = Contacts ( context ). accounts (). queryRawContacts ()","title":"Query RawContacts"},{"location":"accounts/query-raw-contacts/#a-basic-query","text":"To get all RawContacts as blanks, val rawContacts = Contacts ( context ). accounts (). queryRawContacts (). find ()","title":"A basic query"},{"location":"accounts/query-raw-contacts/#specifying-accounts","text":"To limit the search to only those RawContacts associated with one of the given accounts, . accounts ( accounts ) For example, to limit the search to contacts belonging to only one account, . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) \u2139\ufe0f For more info, read Query for Accounts . If no accounts are specified (this function is not called or called with no Accounts), then all RawContacts of Contacts are included in the search. A null Account may be provided here, which results in RawContacts with no associated Account to be included in the search. RawContacts without an associated account are considered local contacts or device-only contacts, which are not synced. For more info, read about Local (device-only) contacts . \u2139\ufe0f This may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it.","title":"Specifying Accounts"},{"location":"accounts/query-raw-contacts/#ordering","text":"To order resulting RawContacts using one or more fields, . orderBy ( fieldOrder ) For example, to order RawContacts by account type, . orderBy ( RawContactsFields . AccountType . asc ()) String comparisons ignores case by default. Each orderBys provides ignoreCase as an optional parameter. Use RawContactsFields to construct the orderBys.","title":"Ordering"},{"location":"accounts/query-raw-contacts/#limiting-and-offsetting","text":"To limit the amount of RawContacts returned and/or offset (skip) a specified number of RawContacts, . limit ( limit ) . offset ( offset ) For example, to only get a maximum 20 RawContacts, skipping the first 20, . limit ( 20 ) . offset ( 20 ) This is useful for pagination =) \u2139\ufe0f It is recommended to limit the number of RawContacts when querying to increase performance and decrease memory cost.","title":"Limiting and offsetting"},{"location":"accounts/query-raw-contacts/#executing-the-query","text":"To execute the query, . find ()","title":"Executing the query"},{"location":"accounts/query-raw-contacts/#cancelling-the-query","text":"To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val rawContacts = query . find { ! isActive } } }","title":"Cancelling the query"},{"location":"accounts/query-raw-contacts/#performing-the-query-asynchronously","text":"Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes.","title":"Performing the query asynchronously"},{"location":"accounts/query-raw-contacts/#performing-the-query-with-permission","text":"Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the query with permission"},{"location":"accounts/query-raw-contacts/#rawcontacts-from-more-than-one-account-in-the-same-list","text":"When you perform a query that returns groups from more than one account, you will get everything in the same BlankRawContactsList . This list is just like any other List except it also provides an extra function that allows you to get a sublist with RawContacts belonging only to a particular account. val rawContactsFromAccount = blankRawContactsList . from ( account )","title":"RawContacts from more than one account in the same list"},{"location":"accounts/query-raw-contacts/#getting-contacts-and-rawcontacts-from-blankrawcontacts","text":"If you want to get the Contacts and all associated RawContacts and Data from a set of BlankRawContact s, val contacts = Contacts ( context ) . query () . where { RawContact . Id `in` blankRawContactIds } . find () \u2139\ufe0f For more info, read Query contacts (advanced) . If you need a more convenient way to convert the BlankRawContact s to RawContacts , use BlankRawContactToRawContact extensions. For more info, read Convenience functions .","title":"Getting Contacts and RawContacts from BlankRawContacts"},{"location":"accounts/query-raw-contacts/#profile-rawcontacts","text":"The AccountsRawContactsQuery API also supports querying the Profile (device owner) RawContacts. To get an instance of this API for Profile queries, val query = Contacts ( context ). accounts (). profile (). queryRawContacts () All queries will be limited to the Profile, whether it exists or not.","title":"Profile RawContacts"},{"location":"accounts/query-raw-contacts/#using-the-where-function-to-specify-matching-criteria","text":"Use the contacts.core.RawContactsField combined with the extensions from contacts.core.Where to form WHERE clauses. \u2139\ufe0f This docs page will not provide a tutorial on database where clauses. It assumes that you know the basics. If you don't know the basics, then search for sqlite where clause . For example, to get a list of RawContacts with the given IDs, val favoriteRawContacts = Contacts ( context ) . accounts () . queryRawContacts () . where { Id `in` rawContactIds } . find ()","title":"Using the where function to specify matching criteria"},{"location":"accounts/query-raw-contacts/#limitations","text":"This library only provides basic WHERE functions. It does not cover the entirety of SQLite, though the community may add more over time <3 Furthermore, this library is constrained by rules and limitations set by the Contacts Provider and the behavior of the native Contacts app. One such rule/limitation has resulted in this library not providing WHERE functions such as isNull or isNullOrEmpty to prevent making misleading queries.","title":"Limitations"},{"location":"async/async-execution-coroutines/","text":"Execute work outside of the UI thread using coroutines \u00b6 This library provides extensions in the async module that allow you to execute all core API functions outside of the main, UI thread. These extensions use Kotlin Coroutines . The extension functions are lightweight and mostly exist for Coroutine user's convenience. The extensions can be generalized in two categories; withContext and async . These use, you guessed it, Kotlin Coroutine's withContext and async functions respectively. For all core API functions that does blocking work in the call-site thread (e.g. query, insert, update, and deletes), there is a corresponding xxxWithContext and xxxAsync extension function. Using withContext extensions \u00b6 To perform an query, insert, update, and delete in order (sequential) outside the main UI thread, launch { val queryResult = query . findWithContext () val insertResult = insert . commitWithContext () val updateResult = update . commitWithContext () val deleteResult = delete . commitWithContext () } For each invocation of xxxWithContext , the current coroutine suspends, performs the operation in the given CoroutineContext (default is Dispatchers.IO if not specified), then returns the result. Computations automatically stops if the parent coroutine scope / job is cancelled. Using async extensions \u00b6 To perform an query, insert, update, and delete in parallel outside the main UI thread, launch { val deferredQueryResult = query . findAsynct () val deferredInsertResult = insert . commitAsync () val deferredUpdateResult = update . commitAsync () val deferredDeleteResult = delete . commitAsync () awaitAll ( deferredQueryResult , deferredInsertResult , deferredUpdateResult , deferredDeleteResult ) } For each invocation of xxxAsync , a CoroutineScope is created with the given CoroutineContext (default is Dispatchers.IO if not specified), performs the operation in that scope, then returns the Deferred result. Computations automatically stops if the parent coroutine scope / job is cancelled. Cancellations are supported \u00b6 To cancel a query amid execution, query . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val contacts = query . find { ! isActive } } } Or, using the coroutine extensions in the async module, launch { val contacts = query . findWithContext () } \u2139\ufe0f Most core API functions support cancellations, not just queries! Not compatible with Java \u00b6 Unlike the core module, the async module is not compatible with Java because it requires Kotlin Coroutines. These extensions are optional \u00b6 You are free to use the core APIs however you want with whatever libraries or frameworks you want that works with Java such as Reactive, AsyncTask (hope not), WorkManager, or your own DIY solution. Extensions for RxJava and Flow are in the roadmap \u00b6 If you prefer not to use Kotlin Coroutines and would rather use your own multi-threading mechanism, then you are free to use the core module without using the async module functions. However, if you prefer to use something that comes with the library to ensure first-class support, then you might be interested in waiting for extensions for RxJava and Kotlin Flow !","title":"Execute work outside of the UI thread using coroutines"},{"location":"async/async-execution-coroutines/#execute-work-outside-of-the-ui-thread-using-coroutines","text":"This library provides extensions in the async module that allow you to execute all core API functions outside of the main, UI thread. These extensions use Kotlin Coroutines . The extension functions are lightweight and mostly exist for Coroutine user's convenience. The extensions can be generalized in two categories; withContext and async . These use, you guessed it, Kotlin Coroutine's withContext and async functions respectively. For all core API functions that does blocking work in the call-site thread (e.g. query, insert, update, and deletes), there is a corresponding xxxWithContext and xxxAsync extension function.","title":"Execute work outside of the UI thread using coroutines"},{"location":"async/async-execution-coroutines/#using-withcontext-extensions","text":"To perform an query, insert, update, and delete in order (sequential) outside the main UI thread, launch { val queryResult = query . findWithContext () val insertResult = insert . commitWithContext () val updateResult = update . commitWithContext () val deleteResult = delete . commitWithContext () } For each invocation of xxxWithContext , the current coroutine suspends, performs the operation in the given CoroutineContext (default is Dispatchers.IO if not specified), then returns the result. Computations automatically stops if the parent coroutine scope / job is cancelled.","title":"Using withContext extensions"},{"location":"async/async-execution-coroutines/#using-async-extensions","text":"To perform an query, insert, update, and delete in parallel outside the main UI thread, launch { val deferredQueryResult = query . findAsynct () val deferredInsertResult = insert . commitAsync () val deferredUpdateResult = update . commitAsync () val deferredDeleteResult = delete . commitAsync () awaitAll ( deferredQueryResult , deferredInsertResult , deferredUpdateResult , deferredDeleteResult ) } For each invocation of xxxAsync , a CoroutineScope is created with the given CoroutineContext (default is Dispatchers.IO if not specified), performs the operation in that scope, then returns the Deferred result. Computations automatically stops if the parent coroutine scope / job is cancelled.","title":"Using async extensions"},{"location":"async/async-execution-coroutines/#cancellations-are-supported","text":"To cancel a query amid execution, query . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val contacts = query . find { ! isActive } } } Or, using the coroutine extensions in the async module, launch { val contacts = query . findWithContext () } \u2139\ufe0f Most core API functions support cancellations, not just queries!","title":"Cancellations are supported"},{"location":"async/async-execution-coroutines/#not-compatible-with-java","text":"Unlike the core module, the async module is not compatible with Java because it requires Kotlin Coroutines.","title":"Not compatible with Java"},{"location":"async/async-execution-coroutines/#these-extensions-are-optional","text":"You are free to use the core APIs however you want with whatever libraries or frameworks you want that works with Java such as Reactive, AsyncTask (hope not), WorkManager, or your own DIY solution.","title":"These extensions are optional"},{"location":"async/async-execution-coroutines/#extensions-for-rxjava-and-flow-are-in-the-roadmap","text":"If you prefer not to use Kotlin Coroutines and would rather use your own multi-threading mechanism, then you are free to use the core module without using the async module functions. However, if you prefer to use something that comes with the library to ensure first-class support, then you might be interested in waiting for extensions for RxJava and Kotlin Flow !","title":"Extensions for RxJava and Flow are in the roadmap"},{"location":"basics/delete-contacts/","text":"Delete Contacts \u00b6 This library provides the Delete API, which allows you to delete one or more Contacts or RawContacts. An instance of the Delete API is obtained by, val delete = Contacts ( context ). delete () \u2139\ufe0f If you want to delete the device owner Contact Profile, read Delete device owner Contact profile . \u2139\ufe0f If you want to delete a set of Data, read Delete existing sets of data . A basic delete \u00b6 To delete a set of Contact and all of its RawContacts, val deleteResult = delete . contacts ( contactToDelete ) . commit () If you want to delete a set of RawContacts, val deleteResult = delete . rawContacts ( contactToDelete ) . commit () You may specify contacts and rawContacts in the same delete operation. Note that Contacts are deleted automatically when all constituent RawContacts are deleted. Executing the delete \u00b6 To execute the delete, . commit () If you want to delete all given Contacts and RawContacts in a single atomic transaction, . commitInOneTransaction () The call to commitInOneTransaction will only succeed if ALL given Contacts and RawContacts are successfully deleted. If one delete fails, the entire operation will fail and everything will be reverted prior to the delete operation. In contrast, commit allows for some deletes to succeed and some to fail. Handling the delete result \u00b6 The commit and commitInOneTransaction functions returns a Result , To check if all deletes succeeded, val allDeletesSuccessful = deleteResult . isSuccessful To check if a particular delete succeeded, val firstDeleteSuccessful = deleteResult . isSuccessful ( mutableContact1 ) Performing the delete and result processing asynchronously \u00b6 Deletes are executed when the commit or commitInOneTransaction function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the delete with permission \u00b6 Deletes require the android.permission.WRITE_CONTACTS permissions. If not granted, the delete will do nothing and return a failed result. To perform the delete with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Custom data support \u00b6 The Delete API supports custom data. For more info, read Delete custom data . Data belonging to RawContacts/Contact are deleted \u00b6 When a RawContact is deleted, all of its data are also deleted. Contacts are deleted automatically when all constituent RawContacts are deleted \u00b6 Deleting a contact's Contacts row, RawContacts row(s), and associated Data row(s) are best explained in the documentation in ContactsContract.RawContacts ; When a raw contact is deleted, all of its Data rows as well as StatusUpdates, AggregationExceptions, PhoneLookup rows are deleted automatically. When all raw contacts associated with a Contacts row are deleted, the Contacts row itself is also deleted automatically. The invocation of resolver.delete(...), does not immediately delete a raw contacts row. Instead, it sets the ContactsContract.RawContactsColumns.DELETED flag on the raw contact and removes the raw contact from its aggregate contact. The sync adapter then deletes the raw contact from the server and finalizes phone-side deletion by calling resolver.delete(...) again and passing the ContactsContract#CALLER_IS_SYNCADAPTER query parameter. Some sync adapters are read-only, meaning that they only sync server-side changes to the phone, but not the reverse. If one of those raw contacts is marked for deletion, it will remain on the phone. However it will be effectively invisible, because it will not be part of any aggregate contact. TLDR To delete a contacts and all associated rows, simply delete all RawContact rows with the desired Contacts id. Deletion of the Contacts row and associated Data row(s) will be done automatically by the Contacts Provider.","title":"Delete contacts"},{"location":"basics/delete-contacts/#delete-contacts","text":"This library provides the Delete API, which allows you to delete one or more Contacts or RawContacts. An instance of the Delete API is obtained by, val delete = Contacts ( context ). delete () \u2139\ufe0f If you want to delete the device owner Contact Profile, read Delete device owner Contact profile . \u2139\ufe0f If you want to delete a set of Data, read Delete existing sets of data .","title":"Delete Contacts"},{"location":"basics/delete-contacts/#a-basic-delete","text":"To delete a set of Contact and all of its RawContacts, val deleteResult = delete . contacts ( contactToDelete ) . commit () If you want to delete a set of RawContacts, val deleteResult = delete . rawContacts ( contactToDelete ) . commit () You may specify contacts and rawContacts in the same delete operation. Note that Contacts are deleted automatically when all constituent RawContacts are deleted.","title":"A basic delete"},{"location":"basics/delete-contacts/#executing-the-delete","text":"To execute the delete, . commit () If you want to delete all given Contacts and RawContacts in a single atomic transaction, . commitInOneTransaction () The call to commitInOneTransaction will only succeed if ALL given Contacts and RawContacts are successfully deleted. If one delete fails, the entire operation will fail and everything will be reverted prior to the delete operation. In contrast, commit allows for some deletes to succeed and some to fail.","title":"Executing the delete"},{"location":"basics/delete-contacts/#handling-the-delete-result","text":"The commit and commitInOneTransaction functions returns a Result , To check if all deletes succeeded, val allDeletesSuccessful = deleteResult . isSuccessful To check if a particular delete succeeded, val firstDeleteSuccessful = deleteResult . isSuccessful ( mutableContact1 )","title":"Handling the delete result"},{"location":"basics/delete-contacts/#performing-the-delete-and-result-processing-asynchronously","text":"Deletes are executed when the commit or commitInOneTransaction function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the delete and result processing asynchronously"},{"location":"basics/delete-contacts/#performing-the-delete-with-permission","text":"Deletes require the android.permission.WRITE_CONTACTS permissions. If not granted, the delete will do nothing and return a failed result. To perform the delete with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the delete with permission"},{"location":"basics/delete-contacts/#custom-data-support","text":"The Delete API supports custom data. For more info, read Delete custom data .","title":"Custom data support"},{"location":"basics/delete-contacts/#data-belonging-to-rawcontactscontact-are-deleted","text":"When a RawContact is deleted, all of its data are also deleted.","title":"Data belonging to RawContacts/Contact are deleted"},{"location":"basics/delete-contacts/#contacts-are-deleted-automatically-when-all-constituent-rawcontacts-are-deleted","text":"Deleting a contact's Contacts row, RawContacts row(s), and associated Data row(s) are best explained in the documentation in ContactsContract.RawContacts ; When a raw contact is deleted, all of its Data rows as well as StatusUpdates, AggregationExceptions, PhoneLookup rows are deleted automatically. When all raw contacts associated with a Contacts row are deleted, the Contacts row itself is also deleted automatically. The invocation of resolver.delete(...), does not immediately delete a raw contacts row. Instead, it sets the ContactsContract.RawContactsColumns.DELETED flag on the raw contact and removes the raw contact from its aggregate contact. The sync adapter then deletes the raw contact from the server and finalizes phone-side deletion by calling resolver.delete(...) again and passing the ContactsContract#CALLER_IS_SYNCADAPTER query parameter. Some sync adapters are read-only, meaning that they only sync server-side changes to the phone, but not the reverse. If one of those raw contacts is marked for deletion, it will remain on the phone. However it will be effectively invisible, because it will not be part of any aggregate contact. TLDR To delete a contacts and all associated rows, simply delete all RawContact rows with the desired Contacts id. Deletion of the Contacts row and associated Data row(s) will be done automatically by the Contacts Provider.","title":"Contacts are deleted automatically when all constituent RawContacts are deleted"},{"location":"basics/insert-contacts/","text":"Insert contacts \u00b6 This library provides the Insert API that allows you to insert one or more RawContacts and Data. An instance of the Insert API is obtained by, val insert = Contacts ( context ). insert () \u2139\ufe0f If you want to create/insert the device owner Contact Profile, read Insert device owner Contact profile . \u2139\ufe0f If you want to insert Data into a new or existing contact, read Insert data into new or existing contacts . A basic insert \u00b6 To create/insert a contact with a name of \"John Doe\" who works at Amazon with a work email of \"john.doe@amazon.com\" (in Kotlin), val insertResult = Contacts ( context ) . insert () . rawContacts ( NewRawContact (). apply { name = NewName (). apply { givenName = \"John\" familyName = \"Doe\" } organization = NewOrganization (). apply { company = \"Amazon\" title = \"Superstar\" } emails . add ( NewEmail (). apply { address = \"john.doe@amazon.com\" type = EmailEntity . Type . WORK }) }) . commit () Or alternatively, in a more Kotlinized style using named arguments, val insertResult = Contacts ( context ) . insert () . rawContacts ( NewRawContact ( name = NewName ( givenName = \"John\" , familyName = \"Doe\" ), organization = NewOrganization ( company = \"Amazon\" , title = \"Superstar\" ), emails = mutableListOf ( NewEmail ( address = \"john.doe@amazon.com\" , type = EmailEntity . Type . WORK )) )) . commit () Or alternatively, using extension functions, val insertResult = Contacts ( context ) . insert () . rawContact { setName { givenName = \"John\" familyName = \"Doe\" } setOrganization { company = \"Amazon\" title = \"Superstar\" } addEmail { address = \"john.doe@amazon.com\" type = EmailEntity . Type . WORK } } . commit () Allowing blanks \u00b6 The API allows you to specify if you want to be able to insert blank contacts or not, . allowBlanks ( true | false ) For more info, read about Blank contacts . Blank data are not inserted \u00b6 Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are ignored and are not inserted by insert APIs. For more info, read about Blank data . Associating an Account \u00b6 New RawContacts can be associated with an Account in order to enable syncing, . forAccount ( account ) For example, to associated the new RawContact to an account, . forAccount ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) \u2139\ufe0f For more info, read Query for Accounts . Local RawContacts \u00b6 If no Account is provided, or null is provided, or if an incorrect account is provided, the RawContacts inserted will not be associated with an Account. RawContacts inserted without an associated account are considered local or device-only contacts, which are not synced. \u2139\ufe0f For more info, read Sync contact data across devices . There are also certain data kinds that are ignored on insert or update if the RawContact is local. \u2139\ufe0f For more info, read about Local (device-only) contacts . Including only specific data \u00b6 To include only the given set of fields (data) in each of the insert operation, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations . Executing the insert \u00b6 To execute the insert, . commit () Handling the insert result \u00b6 The commit function returns a Result , val contactsApi = Contacts ( context ) val newRawContact1 = NewRawContact (...) val newRawContact2 = NewRawContact (...) val insertResult = contactsApi . insert () . rawContacts ( newRawContact1 , newRawContact2 ) . commit () To check if all inserts succeeded, val allInsertsSuccessful = insertResult . isSuccessful To check if a particular insert succeeded, val firstInsertSuccessful = insertResult . isSuccessful ( newRawContact1 ) To get the RawContact IDs of all the newly created RawContacts, val allRawContactIds = insertResult . rawContactIds To get the RawContact ID of a particular RawContact, val secondRawContactId = insertResult . rawContactId ( newRawContact2 ) Once you have the RawContact IDs, you can retrieve the newly created Contacts via the Query API, val contacts = contactsApi . query () . where { RawContact . Id `in` allRawContactIds } . find () \u2139\ufe0f For more info, read Query contacts (advanced) . Alternatively, you may use the extensions provided in InsertResult . To get all newly created Contacts, val contacts = insertResult . contacts ( contactsApi ) To get a particular contact, val contact = insertResult . contacts ( contactsApi , newRawContact1 ) To instead get the RawContacts directly, val rawContacts = insertResult . rawContacts ( contactsApi ) To get a particular RawContact, val rawContact = insertResult . rawContact ( contactsApi , newRawContact2 ) Cancelling the insert \u00b6 To cancel an insert amid execution, . commit { returnTrueIfInsertShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel insert processing as soon as possible. The function is called numerous times during insert processing to check if processing should stop or continue. This gives you the option to cancel the insert. For example, to automatically cancel the insert inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val insertResult = insert . commit { ! isActive } } } Performing the insert and result processing asynchronously \u00b6 Inserts are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the insert with permission \u00b6 Inserts require the android.permission.WRITE_CONTACTS and android.permission.GET_ACCOUNTS permissions. If not granted, the insert will do nothing and return a failed result. To perform the insert with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Custom data support \u00b6 The Insert API supports custom data. For more info, read Insert custom data into new or existing contacts . RawContact and Contacts aggregation \u00b6 As per documentation in android.provider.ContactsContract.Contacts , A Contact cannot be created explicitly. When a raw contact is inserted, the provider will first try to find a Contact representing the same person. If one is found, the raw contact's RawContacts#CONTACT_ID column gets the _ID of the aggregate Contact. If no match is found, the provider automatically inserts a new Contact and puts its _ID into the RawContacts#CONTACT_ID column of the newly inserted raw contact. Insert a new RawContact with data of every kind \u00b6 Unless you are allowing blanks, you only need to provide at least one data kind when inserting a new contact in order for the operation to succeed. If you want to provide data of every kind, which is useful when implementing a contact creation screen, val accountToAddContactTo = Account ( \"vestrel00@pixar.com\" , \"com.pixar\" ) val insertResult = Contacts ( context ) . insert () . forAccount () . rawContact { setName { givenName = \"Buzz\" familyName = \"Lightyear\" } setNickname { name = \"Buzz\" } setOrganization { title = \"Space Toy\" company = \"Pixar\" } addPhone { number = \"(555) 555-5555\" type = PhoneEntity . Type . CUSTOM label = \"Fake Number\" } setSipAddress { sipAddress = \"sip:buzz.lightyear@pixar.com\" } addEmail { address = \"buzz.lightyear@pixar.com\" type = EmailEntity . Type . WORK } addEmail { address = \"buzz@lightyear.net\" type = EmailEntity . Type . HOME } addAddress { formattedAddress = \"1200 Park Ave\" type = AddressEntity . Type . WORK } addIm { data = \"buzzlightyear@skype.com\" protocol = ImEntity . Protocol . SKYPE } addWebsite { url = \"https://www.pixar.com\" } addWebsite { url = \"https://www.disney.com\" } addEvent { date = EventDate . from ( year = 1995 , month = 10 , dayOfMonth = 22 ) type = EventEntity . Type . BIRTHDAY } addRelation { name = \"Childhood friend\" type = RelationEntity . Type . CUSTOM label = \"Imaginary Friend\" } groupMemberships . addAll ( contactsApi . groups () . query () . accounts ( accountToAddContactTo ) . where { ( Favorites equalTo true ) or ( Title contains \"friend\" ) } . find () . newMemberships () ) setNote { note = \"The best toy in the world!\" } } . commit () Inserting photos and thumbnails \u00b6 Full-sized photos (and by API design thumbnails) can only be inserted after inserting the contact. For more info, read Get set remove full-sized and thumbnail contact photos .","title":"Insert contacts"},{"location":"basics/insert-contacts/#insert-contacts","text":"This library provides the Insert API that allows you to insert one or more RawContacts and Data. An instance of the Insert API is obtained by, val insert = Contacts ( context ). insert () \u2139\ufe0f If you want to create/insert the device owner Contact Profile, read Insert device owner Contact profile . \u2139\ufe0f If you want to insert Data into a new or existing contact, read Insert data into new or existing contacts .","title":"Insert contacts"},{"location":"basics/insert-contacts/#a-basic-insert","text":"To create/insert a contact with a name of \"John Doe\" who works at Amazon with a work email of \"john.doe@amazon.com\" (in Kotlin), val insertResult = Contacts ( context ) . insert () . rawContacts ( NewRawContact (). apply { name = NewName (). apply { givenName = \"John\" familyName = \"Doe\" } organization = NewOrganization (). apply { company = \"Amazon\" title = \"Superstar\" } emails . add ( NewEmail (). apply { address = \"john.doe@amazon.com\" type = EmailEntity . Type . WORK }) }) . commit () Or alternatively, in a more Kotlinized style using named arguments, val insertResult = Contacts ( context ) . insert () . rawContacts ( NewRawContact ( name = NewName ( givenName = \"John\" , familyName = \"Doe\" ), organization = NewOrganization ( company = \"Amazon\" , title = \"Superstar\" ), emails = mutableListOf ( NewEmail ( address = \"john.doe@amazon.com\" , type = EmailEntity . Type . WORK )) )) . commit () Or alternatively, using extension functions, val insertResult = Contacts ( context ) . insert () . rawContact { setName { givenName = \"John\" familyName = \"Doe\" } setOrganization { company = \"Amazon\" title = \"Superstar\" } addEmail { address = \"john.doe@amazon.com\" type = EmailEntity . Type . WORK } } . commit ()","title":"A basic insert"},{"location":"basics/insert-contacts/#allowing-blanks","text":"The API allows you to specify if you want to be able to insert blank contacts or not, . allowBlanks ( true | false ) For more info, read about Blank contacts .","title":"Allowing blanks"},{"location":"basics/insert-contacts/#blank-data-are-not-inserted","text":"Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are ignored and are not inserted by insert APIs. For more info, read about Blank data .","title":"Blank data are not inserted"},{"location":"basics/insert-contacts/#associating-an-account","text":"New RawContacts can be associated with an Account in order to enable syncing, . forAccount ( account ) For example, to associated the new RawContact to an account, . forAccount ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) \u2139\ufe0f For more info, read Query for Accounts .","title":"Associating an Account"},{"location":"basics/insert-contacts/#local-rawcontacts","text":"If no Account is provided, or null is provided, or if an incorrect account is provided, the RawContacts inserted will not be associated with an Account. RawContacts inserted without an associated account are considered local or device-only contacts, which are not synced. \u2139\ufe0f For more info, read Sync contact data across devices . There are also certain data kinds that are ignored on insert or update if the RawContact is local. \u2139\ufe0f For more info, read about Local (device-only) contacts .","title":"Local RawContacts"},{"location":"basics/insert-contacts/#including-only-specific-data","text":"To include only the given set of fields (data) in each of the insert operation, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations .","title":"Including only specific data"},{"location":"basics/insert-contacts/#executing-the-insert","text":"To execute the insert, . commit ()","title":"Executing the insert"},{"location":"basics/insert-contacts/#handling-the-insert-result","text":"The commit function returns a Result , val contactsApi = Contacts ( context ) val newRawContact1 = NewRawContact (...) val newRawContact2 = NewRawContact (...) val insertResult = contactsApi . insert () . rawContacts ( newRawContact1 , newRawContact2 ) . commit () To check if all inserts succeeded, val allInsertsSuccessful = insertResult . isSuccessful To check if a particular insert succeeded, val firstInsertSuccessful = insertResult . isSuccessful ( newRawContact1 ) To get the RawContact IDs of all the newly created RawContacts, val allRawContactIds = insertResult . rawContactIds To get the RawContact ID of a particular RawContact, val secondRawContactId = insertResult . rawContactId ( newRawContact2 ) Once you have the RawContact IDs, you can retrieve the newly created Contacts via the Query API, val contacts = contactsApi . query () . where { RawContact . Id `in` allRawContactIds } . find () \u2139\ufe0f For more info, read Query contacts (advanced) . Alternatively, you may use the extensions provided in InsertResult . To get all newly created Contacts, val contacts = insertResult . contacts ( contactsApi ) To get a particular contact, val contact = insertResult . contacts ( contactsApi , newRawContact1 ) To instead get the RawContacts directly, val rawContacts = insertResult . rawContacts ( contactsApi ) To get a particular RawContact, val rawContact = insertResult . rawContact ( contactsApi , newRawContact2 )","title":"Handling the insert result"},{"location":"basics/insert-contacts/#cancelling-the-insert","text":"To cancel an insert amid execution, . commit { returnTrueIfInsertShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel insert processing as soon as possible. The function is called numerous times during insert processing to check if processing should stop or continue. This gives you the option to cancel the insert. For example, to automatically cancel the insert inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val insertResult = insert . commit { ! isActive } } }","title":"Cancelling the insert"},{"location":"basics/insert-contacts/#performing-the-insert-and-result-processing-asynchronously","text":"Inserts are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the insert and result processing asynchronously"},{"location":"basics/insert-contacts/#performing-the-insert-with-permission","text":"Inserts require the android.permission.WRITE_CONTACTS and android.permission.GET_ACCOUNTS permissions. If not granted, the insert will do nothing and return a failed result. To perform the insert with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the insert with permission"},{"location":"basics/insert-contacts/#custom-data-support","text":"The Insert API supports custom data. For more info, read Insert custom data into new or existing contacts .","title":"Custom data support"},{"location":"basics/insert-contacts/#rawcontact-and-contacts-aggregation","text":"As per documentation in android.provider.ContactsContract.Contacts , A Contact cannot be created explicitly. When a raw contact is inserted, the provider will first try to find a Contact representing the same person. If one is found, the raw contact's RawContacts#CONTACT_ID column gets the _ID of the aggregate Contact. If no match is found, the provider automatically inserts a new Contact and puts its _ID into the RawContacts#CONTACT_ID column of the newly inserted raw contact.","title":"RawContact and Contacts aggregation"},{"location":"basics/insert-contacts/#insert-a-new-rawcontact-with-data-of-every-kind","text":"Unless you are allowing blanks, you only need to provide at least one data kind when inserting a new contact in order for the operation to succeed. If you want to provide data of every kind, which is useful when implementing a contact creation screen, val accountToAddContactTo = Account ( \"vestrel00@pixar.com\" , \"com.pixar\" ) val insertResult = Contacts ( context ) . insert () . forAccount () . rawContact { setName { givenName = \"Buzz\" familyName = \"Lightyear\" } setNickname { name = \"Buzz\" } setOrganization { title = \"Space Toy\" company = \"Pixar\" } addPhone { number = \"(555) 555-5555\" type = PhoneEntity . Type . CUSTOM label = \"Fake Number\" } setSipAddress { sipAddress = \"sip:buzz.lightyear@pixar.com\" } addEmail { address = \"buzz.lightyear@pixar.com\" type = EmailEntity . Type . WORK } addEmail { address = \"buzz@lightyear.net\" type = EmailEntity . Type . HOME } addAddress { formattedAddress = \"1200 Park Ave\" type = AddressEntity . Type . WORK } addIm { data = \"buzzlightyear@skype.com\" protocol = ImEntity . Protocol . SKYPE } addWebsite { url = \"https://www.pixar.com\" } addWebsite { url = \"https://www.disney.com\" } addEvent { date = EventDate . from ( year = 1995 , month = 10 , dayOfMonth = 22 ) type = EventEntity . Type . BIRTHDAY } addRelation { name = \"Childhood friend\" type = RelationEntity . Type . CUSTOM label = \"Imaginary Friend\" } groupMemberships . addAll ( contactsApi . groups () . query () . accounts ( accountToAddContactTo ) . where { ( Favorites equalTo true ) or ( Title contains \"friend\" ) } . find () . newMemberships () ) setNote { note = \"The best toy in the world!\" } } . commit ()","title":"Insert a new RawContact with data of every kind"},{"location":"basics/insert-contacts/#inserting-photos-and-thumbnails","text":"Full-sized photos (and by API design thumbnails) can only be inserted after inserting the contact. For more info, read Get set remove full-sized and thumbnail contact photos .","title":"Inserting photos and thumbnails"},{"location":"basics/query-contacts-advanced/","text":"Query contacts (advanced) \u00b6 This library provides the Query API that allows you to get a list of Contacts matching a specific search criteria. All RawContacts of matching Contacts are included in the resulting Contact instances. This provides a great deal of granularity and customizations when providing matching criteria via the where function. An instance of the Query API is obtained by, val query = Contacts ( context ). query () \u2139\ufe0f For a broader, and more native Contacts app like query, use the BroadQuery API, read Query contacts . \u2139\ufe0f If you want to query Data directly instead of Contacts, read Query specific data kinds . \u2139\ufe0f If you want to get the device owner Contact Profile, read Query device owner Contact profile . An advanced query \u00b6 To retrieve the first 5 contacts (including only the contact id, display name, and phone numbers in the results) ordered by display name in descending order, matching ALL of these rules; a first name starting with \"leo\" has emails from gmail or hotmail lives in the US has been born prior to making this query is favorited (starred) has a nickname of \"DarEdEvil\" (case sensitive) works for Facebook has a note belongs to the account of \"john.doe@gmail.com\" or \"john.doe@myspace.com\" val contacts = Contacts ( context ) . query () . where { ( Name . GivenName startsWith \"leo\" ) and ( Email . Address { endsWith ( \"gmail.com\" ) or endsWith ( \"hotmail.com\" ) }) and ( Address . Country equalToIgnoreCase \"us\" ) and ( Event { ( Date lessThan Date (). toWhereString ()) and ( Type equalTo EventEntity . Type . BIRTHDAY ) }) and ( Contact . Options . Starred equalTo true ) and ( Nickname . Name equalTo \"DarEdEvil\" ) and ( Organization . Company `in` listOf ( \"facebook\" , \"FB\" )) and ( Note . Note . isNotNullOrEmpty ()) } . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" ), Account ( \"john.doe@myspace.com\" , \"com.myspace\" ), ) . include { setOf ( Contact . Id , Contact . DisplayNamePrimary , Phone . Number ) } . orderBy ( ContactsFields . DisplayNamePrimary . desc ()) . offset ( 0 ) . limit ( 5 ) . find () A basic query \u00b6 This query API may also be used to make basic, simpler queries. To get all contacts ordered by the primary display name, val contacts = Contacts ( context ) . query () . orderBy ( ContactsFields . DisplayNamePrimary . asc ()) . find () To get all contacts with a phone number AND email, val contacts = Contacts ( context ) . query () ... . where { Phone . Number . isNotNullOrEmpty () and Email . Address . isNotNullOrEmpty () } . find () \u2139\ufe0f Phone numbers are a special case because the Contacts Provider keeps track of the existence of a phone number for any given contact. Use Contact.HasPhoneNumber equalTo true instead for a more optimized query. To get a list of contacts with the given IDs, val contacts = Contacts ( context ) . query () ... . where { Contact . Id `in` contactIds } . find () To get a Contact by lookup key, read about Contact lookup key vs ID . Including blank contacts \u00b6 The API allows you to specify if you want to include blank contacts or not, . includeBlanks ( true | false ) For more info, read Blank contacts . Specifying Accounts \u00b6 To limit the search to only those contacts associated with one of the given accounts, . accounts ( accounts ) For example, to limit the search to contacts belonging to only one account, . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) \u2139\ufe0f For more info, read Query for Accounts . The Contacts returned may still contain RawContacts / data that belongs to other accounts not specified in the given accounts because Contacts may be made up of more than one RawContact from different Accounts. This is the same behavior as the native Contacts app. If no accounts are specified (this function is not called or called with no Accounts), then all RawContacts of Contacts are included in the search. A null Account may be provided here, which results in RawContacts with no associated Account to be included in the search. RawContacts without an associated account are considered local contacts or device-only contacts, which are not synced. For more info, read about Local (device-only) contacts . \u2139\ufe0f This may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it. Including only specific data \u00b6 To include only the given set of fields (data) in each of the matching contacts, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations . Specifying Groups \u00b6 To limit the search to only those RawContacts associated with at least one of the given groups, . where { GroupMembership . GroupId `in` groups . mapNotNull { it . id } } \u2139\ufe0f For more info, read Query groups . Contacts returned may still contain RawContacts / data that belongs to other groups not specified in the given groups because Contacts may be made up of more than one RawContact from different Groups. This is the same behavior as the native Contacts app. If no groups are specified, then all RawContacts of Contacts are included in the search. \u2139\ufe0f This may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it. Ordering \u00b6 To order resulting Contacts using one or more fields, . orderBy ( fieldOrder ) For example, to order contacts by favorite/starred status such that favorite/starred contacts appear first in the list AND order by display name primary in ascending order (from a to z ignoring case), . orderBy ( ContactsFields . Options . Starred . desc (), ContactsFields . DisplayNamePrimary . asc () ) String comparisons ignores case by default. Each orderBys provides ignoreCase as an optional parameter. Use ContactsFields to construct the orderBys. \u2139\ufe0f If you need to sort a collection of Contacts outside of a database query using any field (in addition to ContactsFields ), use contacts.core.util.ContactsComparator . For more info, read Convenience functions . Limiting and offsetting \u00b6 To limit the amount of contacts returned and/or offset (skip) a specified number of contacts, . limit ( limit ) . offset ( offset ) For example, to only get a maximum 20 contacts, skipping the first 20, . limit ( 20 ) . offset ( 20 ) This is useful for pagination =) \u2139\ufe0f It is recommended to limit the number of contacts when querying to increase performance and decrease memory cost. Executing the query \u00b6 To execute the query, . find () Cancelling the query \u00b6 To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val contacts = query . find { ! isActive } } } Performing the query asynchronously \u00b6 Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes. Performing the query with permission \u00b6 Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Custom data support \u00b6 The Query API supports custom data. For more info, read Query custom data . Using the where function to specify matching criteria \u00b6 Use the contacts.core.Fields combined with the extensions from contacts.core.Where to form WHERE clauses. \u2139\ufe0f This docs page will not provide a tutorial on database where clauses. It assumes that you know the basics. If you don't know the basics, then search for sqlite where clause . For example, to get all contacts with a phone number AND email, val contacts = Contacts ( context ) . query () ... . where { Phone . Number . isNotNullOrEmpty () and Email . Address . isNotNullOrEmpty () } . find () To get a list of contacts with the given IDs, val contacts = Contacts ( context ) . query () ... . where { Contact . Id `in` contactIds } . find () Performance \u00b6 Using where may require one or more additional queries, internally performed by the API, which increases the time it takes for the query to complete. Therefore, you should only use where if you actually need it. For every usage of the and operator where the left-hand-side and right-hand-side are different data kinds, an internal database query is performed. This is due to the way the Data table is structured in relation to Contacts. For example, Email . Address . isNotNull () and Phone . Number . isNotNull () and Address . FormattedAddress . isNotNull () The above will require two additional internal database queries in order to simplify the query such that it can actually provide matching Contacts. Using the or operator does not have this performance hit. Limitations \u00b6 This library only provides basic WHERE functions. It does not cover the entirety of SQLite, though the community may add more over time <3 Furthermore, this library is constrained by rules and limitations set by the Contacts Provider and the behavior of the native Contacts app. One such rule/limitation has resulted in this library not providing WHERE functions such as isNull or isNullOrEmpty to prevent making misleading queries. Removing a piece of existing data results in the deletion of the row in the Data table if that row no longer contains any meaningful data. This is the behavior of the native Contacts app. Therefore, querying for null fields is not possible. For example, there may be no Data rows that exist where the email address is null. Thus, a query to search for all contacts with no email addresses may return 0 contacts even if there are some contacts that do not have at least one email address. If you want to match contacts that has no particular type of data, you will have to make two queries. One to get contacts that have that particular type of data and another to get contacts that were not part of the first query results. For example, val contactsWithEmails = query . include ( Fields . Contact . Id ) . where { Email . Address . isNotNullOrEmpty () } . find () val contactIdsWithEmails = contactsWithEmails . mapNotNull { it . id } val contactsWithoutEmails = query . where { Contact . Id notIn contactIdsWithEmails } . find () There is a special case with phone numbers. The ContactsContract provides a field that is true if the contact has at least one phone number; Fields.Contact.HasPhoneNumber . The phone number is the only kind of data that the ContactsContract provides with an indexed value such as this. The ContactsContract does NOT provide things like \"hasEmail\", \"hasWebsite\", etc. Regardless, this library provide functions to match contacts that \"has at least one instance of a kind of data\". The HasPhoneNumber field is not necessary to get contacts that have a phone number. However, this does provide an easy way to get contacts that have no phone numbers without having to make two queries. For example, val contactsWithNoPhoneNumbers = query . where { Contact . HasPhoneNumber notEqualTo true } . find () Blank Contacts and the where function \u00b6 The where function is only used to query the Data table. Some contacts do not have any Data table rows. However, this library exposes some fields that belong to other tables, accessible via the Data table with joins; Fields.Contact Fields.RawContact Using these fields in the where clause does not have any effect in matching blank Contacts or blank RawContacts simply because they have no Data rows containing these joined fields. For more info, read about Blank contacts .","title":"Query contacts (advanced)"},{"location":"basics/query-contacts-advanced/#query-contacts-advanced","text":"This library provides the Query API that allows you to get a list of Contacts matching a specific search criteria. All RawContacts of matching Contacts are included in the resulting Contact instances. This provides a great deal of granularity and customizations when providing matching criteria via the where function. An instance of the Query API is obtained by, val query = Contacts ( context ). query () \u2139\ufe0f For a broader, and more native Contacts app like query, use the BroadQuery API, read Query contacts . \u2139\ufe0f If you want to query Data directly instead of Contacts, read Query specific data kinds . \u2139\ufe0f If you want to get the device owner Contact Profile, read Query device owner Contact profile .","title":"Query contacts (advanced)"},{"location":"basics/query-contacts-advanced/#an-advanced-query","text":"To retrieve the first 5 contacts (including only the contact id, display name, and phone numbers in the results) ordered by display name in descending order, matching ALL of these rules; a first name starting with \"leo\" has emails from gmail or hotmail lives in the US has been born prior to making this query is favorited (starred) has a nickname of \"DarEdEvil\" (case sensitive) works for Facebook has a note belongs to the account of \"john.doe@gmail.com\" or \"john.doe@myspace.com\" val contacts = Contacts ( context ) . query () . where { ( Name . GivenName startsWith \"leo\" ) and ( Email . Address { endsWith ( \"gmail.com\" ) or endsWith ( \"hotmail.com\" ) }) and ( Address . Country equalToIgnoreCase \"us\" ) and ( Event { ( Date lessThan Date (). toWhereString ()) and ( Type equalTo EventEntity . Type . BIRTHDAY ) }) and ( Contact . Options . Starred equalTo true ) and ( Nickname . Name equalTo \"DarEdEvil\" ) and ( Organization . Company `in` listOf ( \"facebook\" , \"FB\" )) and ( Note . Note . isNotNullOrEmpty ()) } . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" ), Account ( \"john.doe@myspace.com\" , \"com.myspace\" ), ) . include { setOf ( Contact . Id , Contact . DisplayNamePrimary , Phone . Number ) } . orderBy ( ContactsFields . DisplayNamePrimary . desc ()) . offset ( 0 ) . limit ( 5 ) . find ()","title":"An advanced query"},{"location":"basics/query-contacts-advanced/#a-basic-query","text":"This query API may also be used to make basic, simpler queries. To get all contacts ordered by the primary display name, val contacts = Contacts ( context ) . query () . orderBy ( ContactsFields . DisplayNamePrimary . asc ()) . find () To get all contacts with a phone number AND email, val contacts = Contacts ( context ) . query () ... . where { Phone . Number . isNotNullOrEmpty () and Email . Address . isNotNullOrEmpty () } . find () \u2139\ufe0f Phone numbers are a special case because the Contacts Provider keeps track of the existence of a phone number for any given contact. Use Contact.HasPhoneNumber equalTo true instead for a more optimized query. To get a list of contacts with the given IDs, val contacts = Contacts ( context ) . query () ... . where { Contact . Id `in` contactIds } . find () To get a Contact by lookup key, read about Contact lookup key vs ID .","title":"A basic query"},{"location":"basics/query-contacts-advanced/#including-blank-contacts","text":"The API allows you to specify if you want to include blank contacts or not, . includeBlanks ( true | false ) For more info, read Blank contacts .","title":"Including blank contacts"},{"location":"basics/query-contacts-advanced/#specifying-accounts","text":"To limit the search to only those contacts associated with one of the given accounts, . accounts ( accounts ) For example, to limit the search to contacts belonging to only one account, . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) \u2139\ufe0f For more info, read Query for Accounts . The Contacts returned may still contain RawContacts / data that belongs to other accounts not specified in the given accounts because Contacts may be made up of more than one RawContact from different Accounts. This is the same behavior as the native Contacts app. If no accounts are specified (this function is not called or called with no Accounts), then all RawContacts of Contacts are included in the search. A null Account may be provided here, which results in RawContacts with no associated Account to be included in the search. RawContacts without an associated account are considered local contacts or device-only contacts, which are not synced. For more info, read about Local (device-only) contacts . \u2139\ufe0f This may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it.","title":"Specifying Accounts"},{"location":"basics/query-contacts-advanced/#including-only-specific-data","text":"To include only the given set of fields (data) in each of the matching contacts, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations .","title":"Including only specific data"},{"location":"basics/query-contacts-advanced/#specifying-groups","text":"To limit the search to only those RawContacts associated with at least one of the given groups, . where { GroupMembership . GroupId `in` groups . mapNotNull { it . id } } \u2139\ufe0f For more info, read Query groups . Contacts returned may still contain RawContacts / data that belongs to other groups not specified in the given groups because Contacts may be made up of more than one RawContact from different Groups. This is the same behavior as the native Contacts app. If no groups are specified, then all RawContacts of Contacts are included in the search. \u2139\ufe0f This may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it.","title":"Specifying Groups"},{"location":"basics/query-contacts-advanced/#ordering","text":"To order resulting Contacts using one or more fields, . orderBy ( fieldOrder ) For example, to order contacts by favorite/starred status such that favorite/starred contacts appear first in the list AND order by display name primary in ascending order (from a to z ignoring case), . orderBy ( ContactsFields . Options . Starred . desc (), ContactsFields . DisplayNamePrimary . asc () ) String comparisons ignores case by default. Each orderBys provides ignoreCase as an optional parameter. Use ContactsFields to construct the orderBys. \u2139\ufe0f If you need to sort a collection of Contacts outside of a database query using any field (in addition to ContactsFields ), use contacts.core.util.ContactsComparator . For more info, read Convenience functions .","title":"Ordering"},{"location":"basics/query-contacts-advanced/#limiting-and-offsetting","text":"To limit the amount of contacts returned and/or offset (skip) a specified number of contacts, . limit ( limit ) . offset ( offset ) For example, to only get a maximum 20 contacts, skipping the first 20, . limit ( 20 ) . offset ( 20 ) This is useful for pagination =) \u2139\ufe0f It is recommended to limit the number of contacts when querying to increase performance and decrease memory cost.","title":"Limiting and offsetting"},{"location":"basics/query-contacts-advanced/#executing-the-query","text":"To execute the query, . find ()","title":"Executing the query"},{"location":"basics/query-contacts-advanced/#cancelling-the-query","text":"To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val contacts = query . find { ! isActive } } }","title":"Cancelling the query"},{"location":"basics/query-contacts-advanced/#performing-the-query-asynchronously","text":"Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes.","title":"Performing the query asynchronously"},{"location":"basics/query-contacts-advanced/#performing-the-query-with-permission","text":"Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the query with permission"},{"location":"basics/query-contacts-advanced/#custom-data-support","text":"The Query API supports custom data. For more info, read Query custom data .","title":"Custom data support"},{"location":"basics/query-contacts-advanced/#using-the-where-function-to-specify-matching-criteria","text":"Use the contacts.core.Fields combined with the extensions from contacts.core.Where to form WHERE clauses. \u2139\ufe0f This docs page will not provide a tutorial on database where clauses. It assumes that you know the basics. If you don't know the basics, then search for sqlite where clause . For example, to get all contacts with a phone number AND email, val contacts = Contacts ( context ) . query () ... . where { Phone . Number . isNotNullOrEmpty () and Email . Address . isNotNullOrEmpty () } . find () To get a list of contacts with the given IDs, val contacts = Contacts ( context ) . query () ... . where { Contact . Id `in` contactIds } . find ()","title":"Using the where function to specify matching criteria"},{"location":"basics/query-contacts-advanced/#performance","text":"Using where may require one or more additional queries, internally performed by the API, which increases the time it takes for the query to complete. Therefore, you should only use where if you actually need it. For every usage of the and operator where the left-hand-side and right-hand-side are different data kinds, an internal database query is performed. This is due to the way the Data table is structured in relation to Contacts. For example, Email . Address . isNotNull () and Phone . Number . isNotNull () and Address . FormattedAddress . isNotNull () The above will require two additional internal database queries in order to simplify the query such that it can actually provide matching Contacts. Using the or operator does not have this performance hit.","title":"Performance"},{"location":"basics/query-contacts-advanced/#limitations","text":"This library only provides basic WHERE functions. It does not cover the entirety of SQLite, though the community may add more over time <3 Furthermore, this library is constrained by rules and limitations set by the Contacts Provider and the behavior of the native Contacts app. One such rule/limitation has resulted in this library not providing WHERE functions such as isNull or isNullOrEmpty to prevent making misleading queries. Removing a piece of existing data results in the deletion of the row in the Data table if that row no longer contains any meaningful data. This is the behavior of the native Contacts app. Therefore, querying for null fields is not possible. For example, there may be no Data rows that exist where the email address is null. Thus, a query to search for all contacts with no email addresses may return 0 contacts even if there are some contacts that do not have at least one email address. If you want to match contacts that has no particular type of data, you will have to make two queries. One to get contacts that have that particular type of data and another to get contacts that were not part of the first query results. For example, val contactsWithEmails = query . include ( Fields . Contact . Id ) . where { Email . Address . isNotNullOrEmpty () } . find () val contactIdsWithEmails = contactsWithEmails . mapNotNull { it . id } val contactsWithoutEmails = query . where { Contact . Id notIn contactIdsWithEmails } . find () There is a special case with phone numbers. The ContactsContract provides a field that is true if the contact has at least one phone number; Fields.Contact.HasPhoneNumber . The phone number is the only kind of data that the ContactsContract provides with an indexed value such as this. The ContactsContract does NOT provide things like \"hasEmail\", \"hasWebsite\", etc. Regardless, this library provide functions to match contacts that \"has at least one instance of a kind of data\". The HasPhoneNumber field is not necessary to get contacts that have a phone number. However, this does provide an easy way to get contacts that have no phone numbers without having to make two queries. For example, val contactsWithNoPhoneNumbers = query . where { Contact . HasPhoneNumber notEqualTo true } . find ()","title":"Limitations"},{"location":"basics/query-contacts-advanced/#blank-contacts-and-the-where-function","text":"The where function is only used to query the Data table. Some contacts do not have any Data table rows. However, this library exposes some fields that belong to other tables, accessible via the Data table with joins; Fields.Contact Fields.RawContact Using these fields in the where clause does not have any effect in matching blank Contacts or blank RawContacts simply because they have no Data rows containing these joined fields. For more info, read about Blank contacts .","title":"Blank Contacts and the where function"},{"location":"basics/query-contacts/","text":"Query contacts \u00b6 This library provides the BroadQuery API that allows you to get the exact same search results as the native Contacts app! This query lets the Contacts Provider perform the search using its own custom matching algorithm via the wherePartiallyMatches function. This type of query is the basis of an app that does a broad search of the Contacts Provider. The technique is useful for apps that want to implement functionality similar to the People app's contact list screen. An instance of the BroadQuery API is obtained by, val query = Contacts ( context ). broadQuery () \u2139\ufe0f For a more granular, advanced queries, use the Query API; Query contacts (advanced) . \u2139\ufe0f If you want to query Data directly instead of Contacts, read Query specific data kinds . \u2139\ufe0f If you want to get the device owner Contact Profile, read Query device owner Contact profile . A basic query \u00b6 To get all contacts ordered by the primary display name, val contacts = Contacts ( context ) . broadQuery () . orderBy ( ContactsFields . DisplayNamePrimary . asc ()) . find () To get all contacts that have any data (e.g. name, email, phone, address, organization, note, etc) that at least partially matches a given searchText , val contacts = Contacts ( context ) . broadQuery () . wherePartiallyMatches ( searchText ) . find () Including blank contacts \u00b6 The API allows you to specify if you want to include blank contacts or not, . includeBlanks ( true | false ) For more info, read about Blank contacts . Specifying Accounts \u00b6 To limit the search to only those contacts associated with one of the given accounts, . accounts ( accounts ) For example, to limit the search to contacts belonging to only one account, . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) \u2139\ufe0f For more info, read Query for Accounts . The Contacts returned may still contain RawContacts / data that belongs to other accounts not specified in the given accounts because Contacts may be made up of more than one RawContact from different Accounts. This is the same behavior as the native Contacts app. If no accounts are specified (this function is not called or called with no Accounts), then all RawContacts of Contacts are included in the search. A null Account may be provided here, which results in RawContacts with no associated Account to be included in the search. RawContacts without an associated account are considered local contacts or device-only contacts, which are not synced. For more info, read about Local (device-only) contacts . \u2139\ufe0f This may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it. Specifying Groups \u00b6 To limit the search to only those RawContacts associated with at least one of the given groups, . groups ( groups ) For example, to limit the search to only favorites, . groups ( favoritesGroup ) \u2139\ufe0f For more info, read Query groups . Contacts returned may still contain RawContacts / data that belongs to other groups not specified in the given groups because Contacts may be made up of more than one RawContact from different Groups. This is the same behavior as the native Contacts app. If no groups are specified (this function is not called or called with no Groups), then all RawContacts of Contacts are included in the search. \u2139\ufe0f This may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it. Including only specific data \u00b6 To include only the given set of fields (data) in each of the matching contacts, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations . Ordering \u00b6 To order resulting Contacts using one or more fields, . orderBy ( fieldOrder ) For example, to order contacts by favorite/starred status such that favorite/starred contacts appear first in the list AND order by display name primary in ascending order (from a to z ignoring case), . orderBy ( ContactsFields . Options . Starred . desc (), ContactsFields . DisplayNamePrimary . asc () ) String comparisons ignores case by default. Each orderBys provides ignoreCase as an optional parameter. Use ContactsFields to construct the orderBys. \u2139\ufe0f If you need to sort a collection of Contacts outside of a database query using any field (in addition to ContactsFields ), use contacts.core.util.ContactsComparator . For more info, read Convenience functions . Limiting and offsetting \u00b6 To limit the amount of contacts returned and/or offset (skip) a specified number of contacts, . limit ( limit ) . offset ( offset ) For example, to only get a maximum 20 contacts, skipping the first 20, . limit ( 20 ) . offset ( 20 ) This is useful for pagination =) \u2139\ufe0f It is recommended to limit the number of contacts when querying to increase performance and decrease memory cost. Executing the query \u00b6 To execute the query, . find () Cancelling the query \u00b6 To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val contacts = query . find { ! isActive } } } Performing the query asynchronously \u00b6 Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes. Performing the query with permission \u00b6 Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Custom data support \u00b6 The BroadQuery API does not include custom data in the matching process. However, you may still use the include function with custom data. For more info, read Query custom data . Using the match and wherePartiallyMatches functions to specify matching criteria \u00b6 The BroadQuery API lets the Contacts Provider perform the search using its own custom matching algorithm via the wherePartiallyMatches function. There are several different types of matching algorithms that can be used. The type is set via the match function. Matching is case-insensitive (case is ignored). Custom data are not included in the matching process! To match custom data, use Query . Match.ANY \u00b6 Most, but not all, Contact data are included in the matching process. Some are not probably because some data may result in unintentional matching. Any contact data is included in the matching process. This is the default. Use this if you want to get the same results when searching contacts using the AOSP Contacts app and the Google Contacts app. Most, but not all, contact data are included in the matching process. E.G. name, email, phone, address, organization, note, etc. Data matching is more sophisticated under the hood than Query . The Contacts Provider matches parts of several types of data in segments. For example, a Contact having the email \"hologram@gram.net\" will be matched with the following texts; h HOLO @g @gram.net gram@ net holo.net hologram.net But will NOT be matched with the following texts; olo @ gram@gram am@gram.net Similarly, a Contact having the name \"Zack Air\" will be matched with the following texts; z zack zack, air air, zack za a , z , a ,a But will NOT be matched with the following texts; ack ir , Another example is a Contact having the note \"Lots of spa ces.\" will be matched with the following texts; l lots lots of of lots ces spa lots of. lo o sp ce . . . . . But will NOT be matched with the following texts; . ots Several types of data are matched in segments. E.G. A Contact with display name \"Bell Zee\" and phone numbers \"987\", \"1 23\", and \"456\" will be matched with \"be bell ze 9 123 1 98 456\". Match.PHONE \u00b6 Only phones or (contact display name + any phones) are included in the matching process. Use this if you want to get contacts that have a matching phone number or matching ( Contact.displayNamePrimary + any phone number). If you are attempting to matching contacts with phone numbers using Query , then you will most likely find it to difficult and tricky because the normalizedNumber could be null and matching formatted numbers (e.g. (718) 737-1991) would require some special regular expressions. This match might just be what you need =) Only the Contact.displayNamePrimary and the phone number/normalizedNumber are included in the matching process. For example, a contact with Contact.displayNamePrimary of \"Bob Dole\" and phone number \"(718) 737-1991\" (regardless of the value of normalizedNumber) will be matched with the following texts; 718 7187371991 7.1-8.7-3.7-19(91) bob dole Notice that \"bob\" and \"dole\" will trigger a match because the display name matches and the contact has a phone number. The following texts will NOT trigger a match because the comparison begins at the beginning of the string and not in the middle or end; 737 1991 Match.EMAIL \u00b6 Only emails or (contact display name + any emails) are included in the matching process. Only the Contact.displayNamePrimary and the email address are included in the matching process. For example, the search text \"bob\" will match the following contacts; Robert Parr (bob@incredibles.com) Bob Parr (incredible@android.com) Notice that the contact Bob Parr is also matched because the display name matches and an email exist (even though it does not match). The following search texts will NOT trigger a match because the comparison begins at the beginning of the string and not in the middle or end; android gmail @ .com Developer notes (or for advanced users) \u00b6 Matching only by phone number or email address is possible thanks to the following filter Uris defined in ContactsContract , which exist for this specific purpose. ContactsContract { Contacts { CONTENT_FILTER_URI } // Default used by BroadQuery CommonDataKinds { Phone { CONTENT_FILTER_URI } Email { CONTENT_FILTER_URI } } } These special filter URIs are only available for the phone and email common data kinds. Note that the EMAIL and PHONE additionally matches the contact display name. Comparison table \u00b6 I've done some preliminary testing on the differences between the different matching/filter algorithms. So, given the following contacts... Display name: Robert Parr Email: bob@incredibles.com Display name: Bob Parr Email: incredible@android.com Display name: Bob Dole Phone: (718) 737-1991 Display name: vestrel00@gmail.com Email: vestrel00@gmail.com Display name: 646-123-4567 Phone: 646-123-4567 Display name: Secret agent. Address: Dole street Company: 718 Note: Agent code is 646000. His skills are incredible! Here are some search terms followed by matching contacts based on the type of Match used. Search term ANY PHONE EMAIL bob 1, 2, 3 3 1, 2 incredible 1, 2, 6 2 android 2 gmail 4 .com 1, 2, 4 @ 7187371991 3 3 7.1-8.7-3.7-19(91) 3 3 646 5, 6 5 646-646 6 718 3, 6 3 1991 4567 000 dole 3, 6 3 The above table gives us some insight on how sophisticated the matching (or search / indexing) algorithm is. For the search term \"bob\", PHONE matches contact 3. Display name matches and contact has a phone even though it does match. EMAIL matches contact 1, 2. 1 has a matching email \"bob\". 2 is also matched because the name matches even though the email does not. On the other hand, 3 is NOT matched even though the name matches because 3 has no email. Adding an email to 3 will cause 3 to be matched. For the search term \"incredible\", EMAIL matches 2 (incredible@android.com) but NOT 1 (bob@incredibles.com). This means that email matching does not use contains but rather a form of startsWith . TLDR ANY matches any contact data; name, email, phone, address, organization, note, etc. EMAIL matches emails or (display name + any email) PHONE matches phones or (display name + any phone) EMAIL and PHONE matching is NOT as simple as using the Query API with .where { [Email|Phone] contains searchTerm }","title":"Query contacts"},{"location":"basics/query-contacts/#query-contacts","text":"This library provides the BroadQuery API that allows you to get the exact same search results as the native Contacts app! This query lets the Contacts Provider perform the search using its own custom matching algorithm via the wherePartiallyMatches function. This type of query is the basis of an app that does a broad search of the Contacts Provider. The technique is useful for apps that want to implement functionality similar to the People app's contact list screen. An instance of the BroadQuery API is obtained by, val query = Contacts ( context ). broadQuery () \u2139\ufe0f For a more granular, advanced queries, use the Query API; Query contacts (advanced) . \u2139\ufe0f If you want to query Data directly instead of Contacts, read Query specific data kinds . \u2139\ufe0f If you want to get the device owner Contact Profile, read Query device owner Contact profile .","title":"Query contacts"},{"location":"basics/query-contacts/#a-basic-query","text":"To get all contacts ordered by the primary display name, val contacts = Contacts ( context ) . broadQuery () . orderBy ( ContactsFields . DisplayNamePrimary . asc ()) . find () To get all contacts that have any data (e.g. name, email, phone, address, organization, note, etc) that at least partially matches a given searchText , val contacts = Contacts ( context ) . broadQuery () . wherePartiallyMatches ( searchText ) . find ()","title":"A basic query"},{"location":"basics/query-contacts/#including-blank-contacts","text":"The API allows you to specify if you want to include blank contacts or not, . includeBlanks ( true | false ) For more info, read about Blank contacts .","title":"Including blank contacts"},{"location":"basics/query-contacts/#specifying-accounts","text":"To limit the search to only those contacts associated with one of the given accounts, . accounts ( accounts ) For example, to limit the search to contacts belonging to only one account, . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) \u2139\ufe0f For more info, read Query for Accounts . The Contacts returned may still contain RawContacts / data that belongs to other accounts not specified in the given accounts because Contacts may be made up of more than one RawContact from different Accounts. This is the same behavior as the native Contacts app. If no accounts are specified (this function is not called or called with no Accounts), then all RawContacts of Contacts are included in the search. A null Account may be provided here, which results in RawContacts with no associated Account to be included in the search. RawContacts without an associated account are considered local contacts or device-only contacts, which are not synced. For more info, read about Local (device-only) contacts . \u2139\ufe0f This may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it.","title":"Specifying Accounts"},{"location":"basics/query-contacts/#specifying-groups","text":"To limit the search to only those RawContacts associated with at least one of the given groups, . groups ( groups ) For example, to limit the search to only favorites, . groups ( favoritesGroup ) \u2139\ufe0f For more info, read Query groups . Contacts returned may still contain RawContacts / data that belongs to other groups not specified in the given groups because Contacts may be made up of more than one RawContact from different Groups. This is the same behavior as the native Contacts app. If no groups are specified (this function is not called or called with no Groups), then all RawContacts of Contacts are included in the search. \u2139\ufe0f This may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it.","title":"Specifying Groups"},{"location":"basics/query-contacts/#including-only-specific-data","text":"To include only the given set of fields (data) in each of the matching contacts, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations .","title":"Including only specific data"},{"location":"basics/query-contacts/#ordering","text":"To order resulting Contacts using one or more fields, . orderBy ( fieldOrder ) For example, to order contacts by favorite/starred status such that favorite/starred contacts appear first in the list AND order by display name primary in ascending order (from a to z ignoring case), . orderBy ( ContactsFields . Options . Starred . desc (), ContactsFields . DisplayNamePrimary . asc () ) String comparisons ignores case by default. Each orderBys provides ignoreCase as an optional parameter. Use ContactsFields to construct the orderBys. \u2139\ufe0f If you need to sort a collection of Contacts outside of a database query using any field (in addition to ContactsFields ), use contacts.core.util.ContactsComparator . For more info, read Convenience functions .","title":"Ordering"},{"location":"basics/query-contacts/#limiting-and-offsetting","text":"To limit the amount of contacts returned and/or offset (skip) a specified number of contacts, . limit ( limit ) . offset ( offset ) For example, to only get a maximum 20 contacts, skipping the first 20, . limit ( 20 ) . offset ( 20 ) This is useful for pagination =) \u2139\ufe0f It is recommended to limit the number of contacts when querying to increase performance and decrease memory cost.","title":"Limiting and offsetting"},{"location":"basics/query-contacts/#executing-the-query","text":"To execute the query, . find ()","title":"Executing the query"},{"location":"basics/query-contacts/#cancelling-the-query","text":"To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val contacts = query . find { ! isActive } } }","title":"Cancelling the query"},{"location":"basics/query-contacts/#performing-the-query-asynchronously","text":"Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes.","title":"Performing the query asynchronously"},{"location":"basics/query-contacts/#performing-the-query-with-permission","text":"Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the query with permission"},{"location":"basics/query-contacts/#custom-data-support","text":"The BroadQuery API does not include custom data in the matching process. However, you may still use the include function with custom data. For more info, read Query custom data .","title":"Custom data support"},{"location":"basics/query-contacts/#using-the-match-and-wherepartiallymatches-functions-to-specify-matching-criteria","text":"The BroadQuery API lets the Contacts Provider perform the search using its own custom matching algorithm via the wherePartiallyMatches function. There are several different types of matching algorithms that can be used. The type is set via the match function. Matching is case-insensitive (case is ignored). Custom data are not included in the matching process! To match custom data, use Query .","title":"Using the match and wherePartiallyMatches functions to specify matching criteria"},{"location":"basics/query-contacts/#matchany","text":"Most, but not all, Contact data are included in the matching process. Some are not probably because some data may result in unintentional matching. Any contact data is included in the matching process. This is the default. Use this if you want to get the same results when searching contacts using the AOSP Contacts app and the Google Contacts app. Most, but not all, contact data are included in the matching process. E.G. name, email, phone, address, organization, note, etc. Data matching is more sophisticated under the hood than Query . The Contacts Provider matches parts of several types of data in segments. For example, a Contact having the email \"hologram@gram.net\" will be matched with the following texts; h HOLO @g @gram.net gram@ net holo.net hologram.net But will NOT be matched with the following texts; olo @ gram@gram am@gram.net Similarly, a Contact having the name \"Zack Air\" will be matched with the following texts; z zack zack, air air, zack za a , z , a ,a But will NOT be matched with the following texts; ack ir , Another example is a Contact having the note \"Lots of spa ces.\" will be matched with the following texts; l lots lots of of lots ces spa lots of. lo o sp ce . . . . . But will NOT be matched with the following texts; . ots Several types of data are matched in segments. E.G. A Contact with display name \"Bell Zee\" and phone numbers \"987\", \"1 23\", and \"456\" will be matched with \"be bell ze 9 123 1 98 456\".","title":"Match.ANY"},{"location":"basics/query-contacts/#matchphone","text":"Only phones or (contact display name + any phones) are included in the matching process. Use this if you want to get contacts that have a matching phone number or matching ( Contact.displayNamePrimary + any phone number). If you are attempting to matching contacts with phone numbers using Query , then you will most likely find it to difficult and tricky because the normalizedNumber could be null and matching formatted numbers (e.g. (718) 737-1991) would require some special regular expressions. This match might just be what you need =) Only the Contact.displayNamePrimary and the phone number/normalizedNumber are included in the matching process. For example, a contact with Contact.displayNamePrimary of \"Bob Dole\" and phone number \"(718) 737-1991\" (regardless of the value of normalizedNumber) will be matched with the following texts; 718 7187371991 7.1-8.7-3.7-19(91) bob dole Notice that \"bob\" and \"dole\" will trigger a match because the display name matches and the contact has a phone number. The following texts will NOT trigger a match because the comparison begins at the beginning of the string and not in the middle or end; 737 1991","title":"Match.PHONE"},{"location":"basics/query-contacts/#matchemail","text":"Only emails or (contact display name + any emails) are included in the matching process. Only the Contact.displayNamePrimary and the email address are included in the matching process. For example, the search text \"bob\" will match the following contacts; Robert Parr (bob@incredibles.com) Bob Parr (incredible@android.com) Notice that the contact Bob Parr is also matched because the display name matches and an email exist (even though it does not match). The following search texts will NOT trigger a match because the comparison begins at the beginning of the string and not in the middle or end; android gmail @ .com","title":"Match.EMAIL"},{"location":"basics/query-contacts/#developer-notes-or-for-advanced-users","text":"Matching only by phone number or email address is possible thanks to the following filter Uris defined in ContactsContract , which exist for this specific purpose. ContactsContract { Contacts { CONTENT_FILTER_URI } // Default used by BroadQuery CommonDataKinds { Phone { CONTENT_FILTER_URI } Email { CONTENT_FILTER_URI } } } These special filter URIs are only available for the phone and email common data kinds. Note that the EMAIL and PHONE additionally matches the contact display name.","title":"Developer notes (or for advanced users)"},{"location":"basics/query-contacts/#comparison-table","text":"I've done some preliminary testing on the differences between the different matching/filter algorithms. So, given the following contacts... Display name: Robert Parr Email: bob@incredibles.com Display name: Bob Parr Email: incredible@android.com Display name: Bob Dole Phone: (718) 737-1991 Display name: vestrel00@gmail.com Email: vestrel00@gmail.com Display name: 646-123-4567 Phone: 646-123-4567 Display name: Secret agent. Address: Dole street Company: 718 Note: Agent code is 646000. His skills are incredible! Here are some search terms followed by matching contacts based on the type of Match used. Search term ANY PHONE EMAIL bob 1, 2, 3 3 1, 2 incredible 1, 2, 6 2 android 2 gmail 4 .com 1, 2, 4 @ 7187371991 3 3 7.1-8.7-3.7-19(91) 3 3 646 5, 6 5 646-646 6 718 3, 6 3 1991 4567 000 dole 3, 6 3 The above table gives us some insight on how sophisticated the matching (or search / indexing) algorithm is. For the search term \"bob\", PHONE matches contact 3. Display name matches and contact has a phone even though it does match. EMAIL matches contact 1, 2. 1 has a matching email \"bob\". 2 is also matched because the name matches even though the email does not. On the other hand, 3 is NOT matched even though the name matches because 3 has no email. Adding an email to 3 will cause 3 to be matched. For the search term \"incredible\", EMAIL matches 2 (incredible@android.com) but NOT 1 (bob@incredibles.com). This means that email matching does not use contains but rather a form of startsWith . TLDR ANY matches any contact data; name, email, phone, address, organization, note, etc. EMAIL matches emails or (display name + any email) PHONE matches phones or (display name + any phone) EMAIL and PHONE matching is NOT as simple as using the Query API with .where { [Email|Phone] contains searchTerm }","title":"Comparison table"},{"location":"basics/update-contacts/","text":"Update contacts \u00b6 This library provides the Update API that allows you to updates one or more contacts in the Contacts Provider database to ensure that it contains the same data as the contacts and raw contacts provided in memory. An instance of the Update API is obtained by, val update = Contacts ( context ). update () \u2139\ufe0f If you want to update the device owner Contact Profile, read Update device owner Contact profile . \u2139\ufe0f If you want to update a set of Data, read Update existing sets of data . A basic update \u00b6 To update a Contact and all of its RawContacts, val updateResult = Contacts ( context ) . update () . contacts ( johnDoe . mutableCopy { setOrganization { company = \"Microsoft\" title = \"Newb\" } emails (). first (). apply { address = \"john.doe@microsoft.com\" } }) . commit () To update a RawContact directly, val updateResult = Contacts ( context ) . update () . rawContacts ( johnDoeFromGmail . mutableCopy { setOrganization { company = \"Microsoft\" title = \"Newb\" } emails (). first (). apply { address = \"john.doe@microsoft.com\" } }) . commit () Deleting blanks \u00b6 The API allows you to specify if you want the update operation to delete blank contacts or not, . deleteBlanks ( true | false ) For more info, read about Blank contacts . Blank data are deleted \u00b6 Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are deleted by update APIs, unless the corresponding fields are not included in the operation. For more info, read about Blank data . Including only specific data \u00b6 To perform update operations only the given set of fields (data), . include ( fields ) For example, to perform updates on only email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations . Executing the update \u00b6 To execute the update, . commit () Handling the update result \u00b6 The commit function returns a Result , val contactsApi = Contacts ( context ) val mutableContact1 = contact1 . mutableCopy { ... } val mutableContact2 = contact2 . mutableCopy { ... } val updateResult = contactsApi . update () . contacts ( mutableContact1 , mutableContact2 ) . commit () To check if all updates succeeded, val allUpdatesSuccessful = updateResult . isSuccessful To check if a particular update succeeded, val firstUpdateSuccessful = updateResult . isSuccessful ( mutableContact1 ) Once you have performed the updates, you can retrieve the updated Contacts references via the Query API, val updatedContacts = contactsApi . query () . where { Contact . Id `in` listOf ( contact1 . id ) } . find () \u2139\ufe0f For more info, read Query contacts (advanced) . Alternatively, you may use the extensions provided in ContactRefresh and RawContactRefresh . To get the updated Contact and all of its RawContacts and Data, val updatedContact1 = contact1 . refresh ( contactsApi ) To get an updated RawContact and Data, val updatedRawContact1 = contact1 . rawContacts . first (). refresh ( contactsApi ) Cancelling the update \u00b6 To cancel an update amid execution, . commit { returnTrueIfUpdateShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel update processing as soon as possible. The function is called numerous times during update processing to check if processing should stop or continue. This gives you the option to cancel the update. For example, to automatically cancel the update inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val updateResult = update . commit { ! isActive } } } Performing the update and result processing asynchronously \u00b6 Updates are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the update with permission \u00b6 Updates require the android.permission.WRITE_CONTACTS permissions. If not granted, the update will do nothing and return a failed result. To perform the update with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Custom data support \u00b6 The Update API supports custom data. For more info, read Update custom data . Modifiable Contact fields \u00b6 As per documentation in android.provider.ContactsContract.Contacts , \u2139\ufe0f Only certain columns of Contact are modifiable: STARRED, CUSTOM_RINGTONE, SEND_TO_VOICEMAIL. Changing any of these columns on the Contact also changes them on all constituent raw contacts. The rest of the APIs provided in this library allow you to modify Data fields (e.g. Email, Phone, etc). Essentially, anything that the Contacts Provider allows for modification =) Updating photos and thumbnails \u00b6 Full-sized photos (and by API design thumbnails) can be set using other functions. For more info, read Get set remove full-sized and thumbnail contact photos . Local RawContacts \u00b6 Updates to local RawContacts are not synced! \u2139\ufe0f For more info, read Sync contact data across devices . There are also certain data kinds that are ignored on insert or update if the RawContact is local. \u2139\ufe0f For more info, read about Local (device-only) contacts .","title":"Update contacts"},{"location":"basics/update-contacts/#update-contacts","text":"This library provides the Update API that allows you to updates one or more contacts in the Contacts Provider database to ensure that it contains the same data as the contacts and raw contacts provided in memory. An instance of the Update API is obtained by, val update = Contacts ( context ). update () \u2139\ufe0f If you want to update the device owner Contact Profile, read Update device owner Contact profile . \u2139\ufe0f If you want to update a set of Data, read Update existing sets of data .","title":"Update contacts"},{"location":"basics/update-contacts/#a-basic-update","text":"To update a Contact and all of its RawContacts, val updateResult = Contacts ( context ) . update () . contacts ( johnDoe . mutableCopy { setOrganization { company = \"Microsoft\" title = \"Newb\" } emails (). first (). apply { address = \"john.doe@microsoft.com\" } }) . commit () To update a RawContact directly, val updateResult = Contacts ( context ) . update () . rawContacts ( johnDoeFromGmail . mutableCopy { setOrganization { company = \"Microsoft\" title = \"Newb\" } emails (). first (). apply { address = \"john.doe@microsoft.com\" } }) . commit ()","title":"A basic update"},{"location":"basics/update-contacts/#deleting-blanks","text":"The API allows you to specify if you want the update operation to delete blank contacts or not, . deleteBlanks ( true | false ) For more info, read about Blank contacts .","title":"Deleting blanks"},{"location":"basics/update-contacts/#blank-data-are-deleted","text":"Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are deleted by update APIs, unless the corresponding fields are not included in the operation. For more info, read about Blank data .","title":"Blank data are deleted"},{"location":"basics/update-contacts/#including-only-specific-data","text":"To perform update operations only the given set of fields (data), . include ( fields ) For example, to perform updates on only email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations .","title":"Including only specific data"},{"location":"basics/update-contacts/#executing-the-update","text":"To execute the update, . commit ()","title":"Executing the update"},{"location":"basics/update-contacts/#handling-the-update-result","text":"The commit function returns a Result , val contactsApi = Contacts ( context ) val mutableContact1 = contact1 . mutableCopy { ... } val mutableContact2 = contact2 . mutableCopy { ... } val updateResult = contactsApi . update () . contacts ( mutableContact1 , mutableContact2 ) . commit () To check if all updates succeeded, val allUpdatesSuccessful = updateResult . isSuccessful To check if a particular update succeeded, val firstUpdateSuccessful = updateResult . isSuccessful ( mutableContact1 ) Once you have performed the updates, you can retrieve the updated Contacts references via the Query API, val updatedContacts = contactsApi . query () . where { Contact . Id `in` listOf ( contact1 . id ) } . find () \u2139\ufe0f For more info, read Query contacts (advanced) . Alternatively, you may use the extensions provided in ContactRefresh and RawContactRefresh . To get the updated Contact and all of its RawContacts and Data, val updatedContact1 = contact1 . refresh ( contactsApi ) To get an updated RawContact and Data, val updatedRawContact1 = contact1 . rawContacts . first (). refresh ( contactsApi )","title":"Handling the update result"},{"location":"basics/update-contacts/#cancelling-the-update","text":"To cancel an update amid execution, . commit { returnTrueIfUpdateShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel update processing as soon as possible. The function is called numerous times during update processing to check if processing should stop or continue. This gives you the option to cancel the update. For example, to automatically cancel the update inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val updateResult = update . commit { ! isActive } } }","title":"Cancelling the update"},{"location":"basics/update-contacts/#performing-the-update-and-result-processing-asynchronously","text":"Updates are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the update and result processing asynchronously"},{"location":"basics/update-contacts/#performing-the-update-with-permission","text":"Updates require the android.permission.WRITE_CONTACTS permissions. If not granted, the update will do nothing and return a failed result. To perform the update with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the update with permission"},{"location":"basics/update-contacts/#custom-data-support","text":"The Update API supports custom data. For more info, read Update custom data .","title":"Custom data support"},{"location":"basics/update-contacts/#modifiable-contact-fields","text":"As per documentation in android.provider.ContactsContract.Contacts , \u2139\ufe0f Only certain columns of Contact are modifiable: STARRED, CUSTOM_RINGTONE, SEND_TO_VOICEMAIL. Changing any of these columns on the Contact also changes them on all constituent raw contacts. The rest of the APIs provided in this library allow you to modify Data fields (e.g. Email, Phone, etc). Essentially, anything that the Contacts Provider allows for modification =)","title":"Modifiable Contact fields"},{"location":"basics/update-contacts/#updating-photos-and-thumbnails","text":"Full-sized photos (and by API design thumbnails) can be set using other functions. For more info, read Get set remove full-sized and thumbnail contact photos .","title":"Updating photos and thumbnails"},{"location":"basics/update-contacts/#local-rawcontacts","text":"Updates to local RawContacts are not synced! \u2139\ufe0f For more info, read Sync contact data across devices . There are also certain data kinds that are ignored on insert or update if the RawContact is local. \u2139\ufe0f For more info, read about Local (device-only) contacts .","title":"Local RawContacts"},{"location":"blockednumbers/about-blocked-numbers/","text":"Blocked numbers \u00b6 The Android 7.0 (API 24) release introduced the Blocked Numbers content provider that stores a list of phone numbers the user has specified should not be able to contact them via telephony communications (calls, SMS, MMS). This library provides the following APIs that allow you to read/write blocked numbers; BlockedNumbersQuery BlockedNumbersInsert BlockedNumbersDelete Blocked number data \u00b6 Blocked number data consists of the number and normalizedNumber . The BlockedNumber.number is the phone number to block as the user entered it. It may or may not be formatted (e.g. (012) 345-6789). \u2139\ufe0f Other than regular phone numbers, the blocked number provider can also store addresses (such as email) from which a user can receive messages, and calls. The BlockedNumber.normalizedNumber is the number 's E164 representation (e.g. +10123456789). This value can be omitted in which case the provider will try to automatically infer it. (It'll be left null if the provider fails to infer.) If present, number has to be set as well (it will be ignored otherwise). If you want to set this value yourself, you may want to look at android.telephony.PhoneNumberUtils . \u2139\ufe0f This may contain an email if number is an email. Privileges to read/write blocked numbers directly \u00b6 Reading and writing directly to the Blocked Numbers database table can only be done by certain privileged apps. The Blocked Number APIs this library provides will only work if all of the following requirements are met; your app must is a system app and/or the default dialer/phone app and/or the default SMS/messaging app the current user (if in a multi-user environment) must be allowed to read/write blocked numbers the runtime OS version is at least Android 7.0 (N) (API 24) To check if all of the requirements specified above are met, val canReadAndWriteBlockedNumbers = Contacts ( context ). blockedNumbers (). privileges . canReadAndWrite () Starting with Android 11 (API 30), you must include the following to your app's manifest in order to successfully use this function and therefore the bocked number APIs provided in this library . \u2139\ufe0f The above is required to be able to check if your app is the default SMS/messaging app. Use the builtin Blocked Numbers activity \u00b6 If your app does not have the privilege to read/write directly to the blocked number provider, you may instead launch the builtin system Blocked numbers activity. It provides a fully functional UI allowing users to see, add, and remove blocked numbers. It is the same activity used by the native ( AOSP) Contacts app and Google Contacts app when accessing the \"Blocked numbers\". Contacts ( context ). blockedNumbers (). startBlockedNumbersActivity ( activity ) If the activity is null, the builtin blocked numbers activity will be launched as a new task, separate from the current application instance. If it is provided, then the activity will be part of the current application's stack/history. Blocked numbers have been introduced in Android 7.0 (N) (API 24). Therefore, this will do nothing for versions lower than API 24. Using the DefaultDialerRequest extensions \u00b6 The most common way for 3rd party apps (apps that don't come pre-installed by the OEM) to get direct read/write access to the blocked numbers table is to be set as the default dialer/phone or SMS/messaging app. The contacts.ui.util.DefaultDialerRequest.kt in the ui module` provides extension functions that allow you to prompt the user to set your app as the default dialer/phone app. To use it, Activity { fun onRequestToBeTheDefaultDialerAppClicked () { requestToBeTheDefaultDialerApp () } override fun onActivityResult ( requestCode : Int , resultCode : Int , data : Intent?) { super . onActivityResult ( requestCode , resultCode , data ) onRequestToBeDefaultDialerAppResult ( requestCode , resultCode ) { // You are now able to use the BlockedNumbersQuery, BlockedNumbersInsert, and // BlockedNumbersDelete APIs. } } } Your app must have an activity with following intent filters in your manifest. Otherwise, this will do nothing. The above intent filters do NOT need to be added to the activity where the extension functions are invoked. It can be placed in any activity within the application. To check if your app is the default dialer/phone app, Context . isDefaultDialerApp () If your app is not a dialer/phone app , then you should not set it as the default dialer/phone app. Otherwise, users of your app may get confused as to why you are prompting them for this privilege. If you still want to read/write blocked numbers directly, you may still use this method. However, make it clear to your users as to why you are doing this despite your app not being a dialer/phone app. Update an existing blocked number entry \u00b6 Update operations are not supported by the Blocked Number provider. Use delete and insert instead. Debugging \u00b6 To look at all of the rows in the Blocked Numbers table, use the Context.logBlockedNumbersTable function in the debug module. For more info, read Debug the Blocked Number Provider tables .","title":"About blocked numbers"},{"location":"blockednumbers/about-blocked-numbers/#blocked-numbers","text":"The Android 7.0 (API 24) release introduced the Blocked Numbers content provider that stores a list of phone numbers the user has specified should not be able to contact them via telephony communications (calls, SMS, MMS). This library provides the following APIs that allow you to read/write blocked numbers; BlockedNumbersQuery BlockedNumbersInsert BlockedNumbersDelete","title":"Blocked numbers"},{"location":"blockednumbers/about-blocked-numbers/#blocked-number-data","text":"Blocked number data consists of the number and normalizedNumber . The BlockedNumber.number is the phone number to block as the user entered it. It may or may not be formatted (e.g. (012) 345-6789). \u2139\ufe0f Other than regular phone numbers, the blocked number provider can also store addresses (such as email) from which a user can receive messages, and calls. The BlockedNumber.normalizedNumber is the number 's E164 representation (e.g. +10123456789). This value can be omitted in which case the provider will try to automatically infer it. (It'll be left null if the provider fails to infer.) If present, number has to be set as well (it will be ignored otherwise). If you want to set this value yourself, you may want to look at android.telephony.PhoneNumberUtils . \u2139\ufe0f This may contain an email if number is an email.","title":"Blocked number data"},{"location":"blockednumbers/about-blocked-numbers/#privileges-to-readwrite-blocked-numbers-directly","text":"Reading and writing directly to the Blocked Numbers database table can only be done by certain privileged apps. The Blocked Number APIs this library provides will only work if all of the following requirements are met; your app must is a system app and/or the default dialer/phone app and/or the default SMS/messaging app the current user (if in a multi-user environment) must be allowed to read/write blocked numbers the runtime OS version is at least Android 7.0 (N) (API 24) To check if all of the requirements specified above are met, val canReadAndWriteBlockedNumbers = Contacts ( context ). blockedNumbers (). privileges . canReadAndWrite () Starting with Android 11 (API 30), you must include the following to your app's manifest in order to successfully use this function and therefore the bocked number APIs provided in this library . \u2139\ufe0f The above is required to be able to check if your app is the default SMS/messaging app.","title":"Privileges to read/write blocked numbers directly"},{"location":"blockednumbers/about-blocked-numbers/#use-the-builtin-blocked-numbers-activity","text":"If your app does not have the privilege to read/write directly to the blocked number provider, you may instead launch the builtin system Blocked numbers activity. It provides a fully functional UI allowing users to see, add, and remove blocked numbers. It is the same activity used by the native ( AOSP) Contacts app and Google Contacts app when accessing the \"Blocked numbers\". Contacts ( context ). blockedNumbers (). startBlockedNumbersActivity ( activity ) If the activity is null, the builtin blocked numbers activity will be launched as a new task, separate from the current application instance. If it is provided, then the activity will be part of the current application's stack/history. Blocked numbers have been introduced in Android 7.0 (N) (API 24). Therefore, this will do nothing for versions lower than API 24.","title":"Use the builtin Blocked Numbers activity"},{"location":"blockednumbers/about-blocked-numbers/#using-the-defaultdialerrequest-extensions","text":"The most common way for 3rd party apps (apps that don't come pre-installed by the OEM) to get direct read/write access to the blocked numbers table is to be set as the default dialer/phone or SMS/messaging app. The contacts.ui.util.DefaultDialerRequest.kt in the ui module` provides extension functions that allow you to prompt the user to set your app as the default dialer/phone app. To use it, Activity { fun onRequestToBeTheDefaultDialerAppClicked () { requestToBeTheDefaultDialerApp () } override fun onActivityResult ( requestCode : Int , resultCode : Int , data : Intent?) { super . onActivityResult ( requestCode , resultCode , data ) onRequestToBeDefaultDialerAppResult ( requestCode , resultCode ) { // You are now able to use the BlockedNumbersQuery, BlockedNumbersInsert, and // BlockedNumbersDelete APIs. } } } Your app must have an activity with following intent filters in your manifest. Otherwise, this will do nothing. The above intent filters do NOT need to be added to the activity where the extension functions are invoked. It can be placed in any activity within the application. To check if your app is the default dialer/phone app, Context . isDefaultDialerApp () If your app is not a dialer/phone app , then you should not set it as the default dialer/phone app. Otherwise, users of your app may get confused as to why you are prompting them for this privilege. If you still want to read/write blocked numbers directly, you may still use this method. However, make it clear to your users as to why you are doing this despite your app not being a dialer/phone app.","title":"Using the DefaultDialerRequest extensions"},{"location":"blockednumbers/about-blocked-numbers/#update-an-existing-blocked-number-entry","text":"Update operations are not supported by the Blocked Number provider. Use delete and insert instead.","title":"Update an existing blocked number entry"},{"location":"blockednumbers/about-blocked-numbers/#debugging","text":"To look at all of the rows in the Blocked Numbers table, use the Context.logBlockedNumbersTable function in the debug module. For more info, read Debug the Blocked Number Provider tables .","title":"Debugging"},{"location":"blockednumbers/delete-blocked-numbers/","text":"Delete blocked numbers \u00b6 This library provides the BlockedNumbersDelete API that allows you to delete existing BlockedNumbers. An instance of the BlockedNumbersDelete API is obtained by, val delete = Contacts ( context ). blockedNumbers (). delete () Note that blocked number deletions will only work for privileged apps. For more info, read about Blocked numbers . A basic delete \u00b6 To delete a set of existing blocked numbers, val deleteResult = Contacts ( context ) . blockedNumbers () . delete () . blockedNumbers ( existingBlockedNumbers ) . commit () Executing the delete \u00b6 To execute the delete, . commit () If you want to delete all given blockedNumbers in a single atomic transaction, . commitInOneTransaction () The call to commitInOneTransaction will only succeed if ALL given blocked numbers are successfully deleted. If one delete fails, the entire operation will fail and everything will be reverted prior to the delete operation. In contrast, commit allows for some deletes to succeed and some to fail. Handling the delete result \u00b6 The commit and commitInOneTransaction functions returns a Result , To check if all deletes succeeded, val allDeletesSuccessful = deleteResult . isSuccessful To check if a particular delete succeeded, val firstDeleteSuccessful = deleteResult . isSuccessful ( blockedNumber1 ) Performing the delete and result processing asynchronously \u00b6 Deletes are executed when the commit or commitInOneTransaction function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the delete with permission \u00b6 There are no permissions required for blocked numbers. However, there are privileges that must be acquired. For more info, read about Blocked numbers .","title":"Delete blocked numbers"},{"location":"blockednumbers/delete-blocked-numbers/#delete-blocked-numbers","text":"This library provides the BlockedNumbersDelete API that allows you to delete existing BlockedNumbers. An instance of the BlockedNumbersDelete API is obtained by, val delete = Contacts ( context ). blockedNumbers (). delete () Note that blocked number deletions will only work for privileged apps. For more info, read about Blocked numbers .","title":"Delete blocked numbers"},{"location":"blockednumbers/delete-blocked-numbers/#a-basic-delete","text":"To delete a set of existing blocked numbers, val deleteResult = Contacts ( context ) . blockedNumbers () . delete () . blockedNumbers ( existingBlockedNumbers ) . commit ()","title":"A basic delete"},{"location":"blockednumbers/delete-blocked-numbers/#executing-the-delete","text":"To execute the delete, . commit () If you want to delete all given blockedNumbers in a single atomic transaction, . commitInOneTransaction () The call to commitInOneTransaction will only succeed if ALL given blocked numbers are successfully deleted. If one delete fails, the entire operation will fail and everything will be reverted prior to the delete operation. In contrast, commit allows for some deletes to succeed and some to fail.","title":"Executing the delete"},{"location":"blockednumbers/delete-blocked-numbers/#handling-the-delete-result","text":"The commit and commitInOneTransaction functions returns a Result , To check if all deletes succeeded, val allDeletesSuccessful = deleteResult . isSuccessful To check if a particular delete succeeded, val firstDeleteSuccessful = deleteResult . isSuccessful ( blockedNumber1 )","title":"Handling the delete result"},{"location":"blockednumbers/delete-blocked-numbers/#performing-the-delete-and-result-processing-asynchronously","text":"Deletes are executed when the commit or commitInOneTransaction function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the delete and result processing asynchronously"},{"location":"blockednumbers/delete-blocked-numbers/#performing-the-delete-with-permission","text":"There are no permissions required for blocked numbers. However, there are privileges that must be acquired. For more info, read about Blocked numbers .","title":"Performing the delete with permission"},{"location":"blockednumbers/insert-blocked-numbers/","text":"Insert blocked numbers \u00b6 This library provides the BlockedNumbersInsert API that allows you to create/insert blocked numbers. An instance of the BlockedNumbersInsert API is obtained by, val insert = Contacts ( context ). blockedNumbers (). insert () Note that blocked number insertions will only work for privileged apps. For more info, read about Blocked numbers . A basic insert \u00b6 To create/insert a new blocked number, val insertResult = Contacts ( context ) . blockedNumbers () . insert () . blockedNumber { number = \"(555) 555-5555\" } . commit () If you need to insert multiple blocked numbers, val newBlockedNumber1 = NewBlockedNumber ( number = \"(555) 555-5555\" ) val newBlockedNumber2 = NewBlockedNumber ( number = \"(123) 456-7890\" ) val insertResult = Contacts ( context ) . blockedNumbers () . insert () . blockedNumbers ( newBlockedNumber1 , newBlockedNumber2 ) . commit () Executing the insert \u00b6 To execute the insert, . commit () Handling the insert result \u00b6 The commit function returns a Result . To check if all inserts succeeded, val allInsertsSuccessful = insertResult . isSuccessful To check if a particular insert succeeded, val firstInsertSuccessful = insertResult . isSuccessful ( newBlockedNumber1 ) To get the BlockedNumber IDs of all the newly created BlockedNumbers, val allBlockedNumberIds = insertResult . blockedNumberIds To get the BlockedNumber ID of a particular BlockedNumber, val secondBlockedNumberId = insertResult . blockedNumberId ( newBlockedNumber2 ) Once you have the BlockedNumber IDs, you can retrieve the newly created BlockedNumbers via the BlockedNumbersQuery API, val blockedNumbers = contactsApi . blockedNumbers () . query () . where { Id `in` allBlockedNumberIds } . find () \u2139\ufe0f For more info, read Query blocked numbers . Alternatively, you may use the extensions provided in BlockedNumbersInsertResult . To get all newly created BlockedNumbers, val blockedNumbers = insertResult . blockedNumbers ( contactsApi ) To get a particular blockedNumber, val blockedNumber = insertResult . blockedNumber ( contactsApi , newBlockedNumber1 ) Handling insert failure \u00b6 The insert may fail for a particular blocked number for various reasons, insertResult . failureReason ( newBlockedNumber1 ) ?. let { when ( it ) { NUMBER_ALREADY_BLOCKED -> tellUserTheNumberIsAlreadyBlocked () NUMBER_IS_BLANK -> promptUserProvideANonBlankNumber () UNKNOWN -> showGenericErrorMessage () } } Cancelling the insert \u00b6 To cancel an insert amid execution, . commit { returnTrueIfInsertShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel insert processing as soon as possible. The function is called numerous times during insert processing to check if processing should stop or continue. This gives you the option to cancel the insert. For example, to automatically cancel the insert inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val insertResult = insert . commit { ! isActive } } } Performing the insert and result processing asynchronously \u00b6 Inserts are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the insert with permission \u00b6 There are no permissions required for blocked numbers. However, there are privileges that must be acquired. For more info, read about Blocked numbers .","title":"Insert blocked numbers"},{"location":"blockednumbers/insert-blocked-numbers/#insert-blocked-numbers","text":"This library provides the BlockedNumbersInsert API that allows you to create/insert blocked numbers. An instance of the BlockedNumbersInsert API is obtained by, val insert = Contacts ( context ). blockedNumbers (). insert () Note that blocked number insertions will only work for privileged apps. For more info, read about Blocked numbers .","title":"Insert blocked numbers"},{"location":"blockednumbers/insert-blocked-numbers/#a-basic-insert","text":"To create/insert a new blocked number, val insertResult = Contacts ( context ) . blockedNumbers () . insert () . blockedNumber { number = \"(555) 555-5555\" } . commit () If you need to insert multiple blocked numbers, val newBlockedNumber1 = NewBlockedNumber ( number = \"(555) 555-5555\" ) val newBlockedNumber2 = NewBlockedNumber ( number = \"(123) 456-7890\" ) val insertResult = Contacts ( context ) . blockedNumbers () . insert () . blockedNumbers ( newBlockedNumber1 , newBlockedNumber2 ) . commit ()","title":"A basic insert"},{"location":"blockednumbers/insert-blocked-numbers/#executing-the-insert","text":"To execute the insert, . commit ()","title":"Executing the insert"},{"location":"blockednumbers/insert-blocked-numbers/#handling-the-insert-result","text":"The commit function returns a Result . To check if all inserts succeeded, val allInsertsSuccessful = insertResult . isSuccessful To check if a particular insert succeeded, val firstInsertSuccessful = insertResult . isSuccessful ( newBlockedNumber1 ) To get the BlockedNumber IDs of all the newly created BlockedNumbers, val allBlockedNumberIds = insertResult . blockedNumberIds To get the BlockedNumber ID of a particular BlockedNumber, val secondBlockedNumberId = insertResult . blockedNumberId ( newBlockedNumber2 ) Once you have the BlockedNumber IDs, you can retrieve the newly created BlockedNumbers via the BlockedNumbersQuery API, val blockedNumbers = contactsApi . blockedNumbers () . query () . where { Id `in` allBlockedNumberIds } . find () \u2139\ufe0f For more info, read Query blocked numbers . Alternatively, you may use the extensions provided in BlockedNumbersInsertResult . To get all newly created BlockedNumbers, val blockedNumbers = insertResult . blockedNumbers ( contactsApi ) To get a particular blockedNumber, val blockedNumber = insertResult . blockedNumber ( contactsApi , newBlockedNumber1 )","title":"Handling the insert result"},{"location":"blockednumbers/insert-blocked-numbers/#handling-insert-failure","text":"The insert may fail for a particular blocked number for various reasons, insertResult . failureReason ( newBlockedNumber1 ) ?. let { when ( it ) { NUMBER_ALREADY_BLOCKED -> tellUserTheNumberIsAlreadyBlocked () NUMBER_IS_BLANK -> promptUserProvideANonBlankNumber () UNKNOWN -> showGenericErrorMessage () } }","title":"Handling insert failure"},{"location":"blockednumbers/insert-blocked-numbers/#cancelling-the-insert","text":"To cancel an insert amid execution, . commit { returnTrueIfInsertShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel insert processing as soon as possible. The function is called numerous times during insert processing to check if processing should stop or continue. This gives you the option to cancel the insert. For example, to automatically cancel the insert inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val insertResult = insert . commit { ! isActive } } }","title":"Cancelling the insert"},{"location":"blockednumbers/insert-blocked-numbers/#performing-the-insert-and-result-processing-asynchronously","text":"Inserts are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the insert and result processing asynchronously"},{"location":"blockednumbers/insert-blocked-numbers/#performing-the-insert-with-permission","text":"There are no permissions required for blocked numbers. However, there are privileges that must be acquired. For more info, read about Blocked numbers .","title":"Performing the insert with permission"},{"location":"blockednumbers/query-blocked-numbers/","text":"Query blocked numbers \u00b6 This library provides the BlockedNumbersQuery API that allows you to get blocked numbers. An instance of the BlockedNumbersQuery API is obtained by, val query = Contacts ( context ). blockedNumbers (). query () Note that blocked number queries will only work for privileged apps. For more info, read about Blocked numbers . A basic query \u00b6 To get all of the blocked numbers, val blockedNumbers = Contacts ( context ) . blockedNumbers () . query () . find () Ordering \u00b6 To order resulting BlockedNumbers using one or more fields, . orderBy ( fieldOrder ) For example, to order blocked numbers by number, . orderBy ( BlockedNumbersFields . Number . asc ()) String comparisons ignores case by default. Each orderBys provides ignoreCase as an optional parameter. Use BlockedNumbersFields to construct the orderBys. Limiting and offsetting \u00b6 To limit the amount of blocked numbers returned and/or offset (skip) a specified number of blocked numbers, . limit ( limit ) . offset ( offset ) For example, to only get a maximum 20 blocked numbers, skipping the first 20, . limit ( 20 ) . offset ( 20 ) This is useful for pagination =) \u2139\ufe0f It is recommended to limit the number of blocked numbers when querying to increase performance and decrease memory cost. Executing the query \u00b6 To execute the query, . find () Cancelling the query \u00b6 To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val blockedNumbers = query . find { ! isActive } } } Performing the query asynchronously \u00b6 Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the query with permission \u00b6 There are no permissions required for blocked numbers. However, there are privileges that must be acquired. For more info, read about Blocked numbers . Using the where function to specify matching criteria \u00b6 Use the contacts.core.BlockedNumbersFields combined with the extensions from contacts.core.Where to form WHERE clauses. \u2139\ufe0f This docs page will not provide a tutorial on database where clauses. It assumes that you know the basics. If you don't know the basics, then search for sqlite where clause . For example, to find blocked numbers that contains \"555\", . where { Number contains \"555\" } To get a list of blocked numbers by IDs, . where { Id `in` blockedNumberIds }","title":"Query blocked numbers"},{"location":"blockednumbers/query-blocked-numbers/#query-blocked-numbers","text":"This library provides the BlockedNumbersQuery API that allows you to get blocked numbers. An instance of the BlockedNumbersQuery API is obtained by, val query = Contacts ( context ). blockedNumbers (). query () Note that blocked number queries will only work for privileged apps. For more info, read about Blocked numbers .","title":"Query blocked numbers"},{"location":"blockednumbers/query-blocked-numbers/#a-basic-query","text":"To get all of the blocked numbers, val blockedNumbers = Contacts ( context ) . blockedNumbers () . query () . find ()","title":"A basic query"},{"location":"blockednumbers/query-blocked-numbers/#ordering","text":"To order resulting BlockedNumbers using one or more fields, . orderBy ( fieldOrder ) For example, to order blocked numbers by number, . orderBy ( BlockedNumbersFields . Number . asc ()) String comparisons ignores case by default. Each orderBys provides ignoreCase as an optional parameter. Use BlockedNumbersFields to construct the orderBys.","title":"Ordering"},{"location":"blockednumbers/query-blocked-numbers/#limiting-and-offsetting","text":"To limit the amount of blocked numbers returned and/or offset (skip) a specified number of blocked numbers, . limit ( limit ) . offset ( offset ) For example, to only get a maximum 20 blocked numbers, skipping the first 20, . limit ( 20 ) . offset ( 20 ) This is useful for pagination =) \u2139\ufe0f It is recommended to limit the number of blocked numbers when querying to increase performance and decrease memory cost.","title":"Limiting and offsetting"},{"location":"blockednumbers/query-blocked-numbers/#executing-the-query","text":"To execute the query, . find ()","title":"Executing the query"},{"location":"blockednumbers/query-blocked-numbers/#cancelling-the-query","text":"To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val blockedNumbers = query . find { ! isActive } } }","title":"Cancelling the query"},{"location":"blockednumbers/query-blocked-numbers/#performing-the-query-asynchronously","text":"Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the query asynchronously"},{"location":"blockednumbers/query-blocked-numbers/#performing-the-query-with-permission","text":"There are no permissions required for blocked numbers. However, there are privileges that must be acquired. For more info, read about Blocked numbers .","title":"Performing the query with permission"},{"location":"blockednumbers/query-blocked-numbers/#using-the-where-function-to-specify-matching-criteria","text":"Use the contacts.core.BlockedNumbersFields combined with the extensions from contacts.core.Where to form WHERE clauses. \u2139\ufe0f This docs page will not provide a tutorial on database where clauses. It assumes that you know the basics. If you don't know the basics, then search for sqlite where clause . For example, to find blocked numbers that contains \"555\", . where { Number contains \"555\" } To get a list of blocked numbers by IDs, . where { Id `in` blockedNumberIds }","title":"Using the where function to specify matching criteria"},{"location":"customdata/delete-custom-data/","text":"Delete custom data \u00b6 This library provides several APIs that supports deleting custom data. DataDelete Delete existing sets of data Delete Delete Contacts ProfileDelete Delete device owner Contact profile Update Update contacts ProfileUpdate Update device owner Contact profile To help illustrate how custom data integrates with these update APIs, we'll use the HandleName and Gender custom data. \u2139\ufe0f For more info about custom data, read Integrate custom data . Deleting custom data via Contacts/RawContacts \u00b6 Custom data, just like regular data kinds, are attached to a RawContact. They follow the same rules as regular data kinds. \u2139\ufe0f or more info, read about API Entities . For example, you are able to delete existing handle names and the gender of an existing RawContact, mutableRawContact . removeHandleName ( contactsApi , handleName ) mutableRawContact . setGender ( contactsApi , null ) There are also extensions that allow you to delete custom data of an existing RawContact via a Contact, which can be made up of one or more RawContacts, mutableContact . removeHandleName ( contactsApi , handleName ) mutableContact . setGender ( contactsApi , null ) Once you have removed custom data, you can perform the update operation on the RawContact to commit your changes into the database using Update or ProfileUpdate . You may also delete an entire Contact or RawContact using Delete or ProfileDelete in order delete all associated data. Deleting set of custom data directly \u00b6 All custom data are compatible with the DataDelete API, which allows you to delete sets of existing regular and custom data kinds. For example, to delete a set of HandleName s and Gender s, val handleNames : List < MutableHandleName > val genders : List < MutableGender > val deleteResult = Contacts ( this ) . data () . delete () . data ( handleNames + genders ) . commit () For more info, read Delete existing sets of data .","title":"Delete custom data"},{"location":"customdata/delete-custom-data/#delete-custom-data","text":"This library provides several APIs that supports deleting custom data. DataDelete Delete existing sets of data Delete Delete Contacts ProfileDelete Delete device owner Contact profile Update Update contacts ProfileUpdate Update device owner Contact profile To help illustrate how custom data integrates with these update APIs, we'll use the HandleName and Gender custom data. \u2139\ufe0f For more info about custom data, read Integrate custom data .","title":"Delete custom data"},{"location":"customdata/delete-custom-data/#deleting-custom-data-via-contactsrawcontacts","text":"Custom data, just like regular data kinds, are attached to a RawContact. They follow the same rules as regular data kinds. \u2139\ufe0f or more info, read about API Entities . For example, you are able to delete existing handle names and the gender of an existing RawContact, mutableRawContact . removeHandleName ( contactsApi , handleName ) mutableRawContact . setGender ( contactsApi , null ) There are also extensions that allow you to delete custom data of an existing RawContact via a Contact, which can be made up of one or more RawContacts, mutableContact . removeHandleName ( contactsApi , handleName ) mutableContact . setGender ( contactsApi , null ) Once you have removed custom data, you can perform the update operation on the RawContact to commit your changes into the database using Update or ProfileUpdate . You may also delete an entire Contact or RawContact using Delete or ProfileDelete in order delete all associated data.","title":"Deleting custom data via Contacts/RawContacts"},{"location":"customdata/delete-custom-data/#deleting-set-of-custom-data-directly","text":"All custom data are compatible with the DataDelete API, which allows you to delete sets of existing regular and custom data kinds. For example, to delete a set of HandleName s and Gender s, val handleNames : List < MutableHandleName > val genders : List < MutableGender > val deleteResult = Contacts ( this ) . data () . delete () . data ( handleNames + genders ) . commit () For more info, read Delete existing sets of data .","title":"Deleting set of custom data directly"},{"location":"customdata/insert-custom-data/","text":"Insert custom data into new or existing contacts \u00b6 Regular and custom data can only be created/inserted into the database whenever inserting or updating new or existing contacts. This library provides several insert and update APIs that support custom data integration. Insert Insert contacts ProfileInsert Insert device owner Contact profile Update Update contacts ProfileUpdate Update device owner Contact profile To help illustrate how custom data integrates with these query APIs, we'll use the HandleName and Gender custom data. \u2139\ufe0f For more info, read Integrate the gender custom data and Integrate the handle name custom data . Creating/inserting custom data into a RawContact \u00b6 Custom data, just like regular data kinds, are attached to a RawContact. They follow the same rules as regular data kinds. \u2139\ufe0f For more info, read about API Entities . For example, you are able to add handle names and set gender of a new RawContact, newRawContact . addHandleName ( contactsApi ) { handle = \"dude91\" } newRawContact . setGender ( contactsApi ) { type = GenderEntity . Type . MALE } Once you have created/insert the custom data into the RawContact, you can perform the insert operation on the new RawContact to commit your changes into the database. For more info, read Insert data into new or existing contacts . The include function and custom data \u00b6 All of the above mentioned APIs provide an include function that allows you to include only a given set of fields (data) to be processed in the insert operation. Custom data entries provides fields that can be used in this function. By default, not calling the include function will include all fields, including custom data fields. For example, to specifically include only HandleName and Gender fields, . include ( HandleNameFields . all + GenderFields . all ) For more info, read Include only certain fields for read and write operations . Blank data are not inserted \u00b6 Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are ignored and are not inserted by insert APIs. For more info, read about Blank data .","title":"Insert custom data into new or existing contacts"},{"location":"customdata/insert-custom-data/#insert-custom-data-into-new-or-existing-contacts","text":"Regular and custom data can only be created/inserted into the database whenever inserting or updating new or existing contacts. This library provides several insert and update APIs that support custom data integration. Insert Insert contacts ProfileInsert Insert device owner Contact profile Update Update contacts ProfileUpdate Update device owner Contact profile To help illustrate how custom data integrates with these query APIs, we'll use the HandleName and Gender custom data. \u2139\ufe0f For more info, read Integrate the gender custom data and Integrate the handle name custom data .","title":"Insert custom data into new or existing contacts"},{"location":"customdata/insert-custom-data/#creatinginserting-custom-data-into-a-rawcontact","text":"Custom data, just like regular data kinds, are attached to a RawContact. They follow the same rules as regular data kinds. \u2139\ufe0f For more info, read about API Entities . For example, you are able to add handle names and set gender of a new RawContact, newRawContact . addHandleName ( contactsApi ) { handle = \"dude91\" } newRawContact . setGender ( contactsApi ) { type = GenderEntity . Type . MALE } Once you have created/insert the custom data into the RawContact, you can perform the insert operation on the new RawContact to commit your changes into the database. For more info, read Insert data into new or existing contacts .","title":"Creating/inserting custom data into a RawContact"},{"location":"customdata/insert-custom-data/#the-include-function-and-custom-data","text":"All of the above mentioned APIs provide an include function that allows you to include only a given set of fields (data) to be processed in the insert operation. Custom data entries provides fields that can be used in this function. By default, not calling the include function will include all fields, including custom data fields. For example, to specifically include only HandleName and Gender fields, . include ( HandleNameFields . all + GenderFields . all ) For more info, read Include only certain fields for read and write operations .","title":"The include function and custom data"},{"location":"customdata/insert-custom-data/#blank-data-are-not-inserted","text":"Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are ignored and are not inserted by insert APIs. For more info, read about Blank data .","title":"Blank data are not inserted"},{"location":"customdata/integrate-custom-data-from-other-apps/","text":"Integrate custom data from other apps \u00b6 If you are looking to create and integrate your own custom data, read Integrate custom data . If you are looking to integrate custom data from other apps, you are in the right place! There are a lot of other apps out there that provide their own custom data, such as Google Contacts and WhatsApp . There are two parts to \"integrating custom data\"; Providing create (insert), read (query), update, and delete (CRUD) APIs for custom data associated with RawContacts. Providing sync adapters to sync custom data across devices. This library only handles the first part. Other (third party) apps typically provide sync adapters to sync their custom data across devices. This library does not interfere with any syncing functionality of custom data from other apps. What this library does is allow you and others to easily read and write custom data from other apps in your own apps. Research what custom data the third party app's are adding, if any \u00b6 The hardest part will be researching what custom data a particular third party app provides, what they are used for, and how they behave. Here are some things you can do to find the answers. Install and log into the app you are interested in researching. Then, use the debug module functions in your app to log the Data table via Context.logDataTable() . Look for any mime types that look like like they belong to the app. Figure out how where in the app's UI the data is shown and/or how the app uses it in general. Deconstruct the APK and look for res/xml/contacts.xml and other places in code where custom data may reside. A good place to look will be in sync adapter related classes . Search the internet for any official documentation on the custom data added by the app. There is a high chance that this does not exist. Search the internet for other people's research on the app's custom data, if any. The first strategy is the most effective strategy to take because you are able to experience first-hand and play around with the custom data and document everything about it. Nothing beats first-hand research! The second strategy is a bit more hacky and advanced and time consuming but it could pay off. The third strategy is optimistic but could end up being the most useful if you are able to locate official documentation from the app developers themselves. The fourth strategy could be unreliable as it depends on other people's knowledge, which could be inaccurate. Integrate the third party app custom data with this library \u00b6 Once you have figured out all of the details of all of the custom data (mimetypes) that the third party app adds, you may proceed to write the code that will allow you and others to perform read and write operations on it using the CRUD APIs provided in this library. To proceed, read Integrate custom data . Example, Google Contacts app custom data \u00b6 Issue #165: Google Contacts app custom data integrates custom data from the Google Contacts app into this library. You may use it as an example on how to get started with the research and also what code to write after the research has been completed. Consider adding your integration of third party apps' custom data to this library \u00b6 Let's say that you have written the code that integrates custom data from a third party app into your app using this library. That's great and all but your app will be the only app that will be able to use it! In the spirit of open source, please feel free to add your third party app custom data integration into this library so that other people using this library can optionally integrate it into their own apps! Please create a GitHub issue and file a pull request!","title":"Integrate custom data from other apps"},{"location":"customdata/integrate-custom-data-from-other-apps/#integrate-custom-data-from-other-apps","text":"If you are looking to create and integrate your own custom data, read Integrate custom data . If you are looking to integrate custom data from other apps, you are in the right place! There are a lot of other apps out there that provide their own custom data, such as Google Contacts and WhatsApp . There are two parts to \"integrating custom data\"; Providing create (insert), read (query), update, and delete (CRUD) APIs for custom data associated with RawContacts. Providing sync adapters to sync custom data across devices. This library only handles the first part. Other (third party) apps typically provide sync adapters to sync their custom data across devices. This library does not interfere with any syncing functionality of custom data from other apps. What this library does is allow you and others to easily read and write custom data from other apps in your own apps.","title":"Integrate custom data from other apps"},{"location":"customdata/integrate-custom-data-from-other-apps/#research-what-custom-data-the-third-party-apps-are-adding-if-any","text":"The hardest part will be researching what custom data a particular third party app provides, what they are used for, and how they behave. Here are some things you can do to find the answers. Install and log into the app you are interested in researching. Then, use the debug module functions in your app to log the Data table via Context.logDataTable() . Look for any mime types that look like like they belong to the app. Figure out how where in the app's UI the data is shown and/or how the app uses it in general. Deconstruct the APK and look for res/xml/contacts.xml and other places in code where custom data may reside. A good place to look will be in sync adapter related classes . Search the internet for any official documentation on the custom data added by the app. There is a high chance that this does not exist. Search the internet for other people's research on the app's custom data, if any. The first strategy is the most effective strategy to take because you are able to experience first-hand and play around with the custom data and document everything about it. Nothing beats first-hand research! The second strategy is a bit more hacky and advanced and time consuming but it could pay off. The third strategy is optimistic but could end up being the most useful if you are able to locate official documentation from the app developers themselves. The fourth strategy could be unreliable as it depends on other people's knowledge, which could be inaccurate.","title":"Research what custom data the third party app's are adding, if any"},{"location":"customdata/integrate-custom-data-from-other-apps/#integrate-the-third-party-app-custom-data-with-this-library","text":"Once you have figured out all of the details of all of the custom data (mimetypes) that the third party app adds, you may proceed to write the code that will allow you and others to perform read and write operations on it using the CRUD APIs provided in this library. To proceed, read Integrate custom data .","title":"Integrate the third party app custom data with this library"},{"location":"customdata/integrate-custom-data-from-other-apps/#example-google-contacts-app-custom-data","text":"Issue #165: Google Contacts app custom data integrates custom data from the Google Contacts app into this library. You may use it as an example on how to get started with the research and also what code to write after the research has been completed.","title":"Example, Google Contacts app custom data"},{"location":"customdata/integrate-custom-data-from-other-apps/#consider-adding-your-integration-of-third-party-apps-custom-data-to-this-library","text":"Let's say that you have written the code that integrates custom data from a third party app into your app using this library. That's great and all but your app will be the only app that will be able to use it! In the spirit of open source, please feel free to add your third party app custom data integration into this library so that other people using this library can optionally integrate it into their own apps! Please create a GitHub issue and file a pull request!","title":"Consider adding your integration of third party apps' custom data to this library"},{"location":"customdata/integrate-custom-data/","text":"Integrate custom data \u00b6 If you are looking to integrate custom data from other apps, read Integrate custom data from other apps . If you are looking to create and integrate your own custom data, you are in the right place! There are two parts to \"integrating custom data\"; Providing create (insert), read (query), update, and delete (CRUD) APIs for custom data associated with RawContacts. Providing sync adapters to sync custom data across devices. This library only handles the first part. If you want to sync your custom data, then you need to implement a sync adapter to interface with your remote server. That is out of scope of this library. In order to create and integrate your own custom data for use in your own apps, there is a bit of boilerplate code that needs to be written. Thankfully none of this stuff is difficult! Here are the steps, in chronological order, on how to define and use your own custom data, Define the mimetype Define the entities Define the fields Implement the cursor Implement the mapper Implement the operation Define the count restriction Define RawContact getters and setters Define Contact getters and setters Define exceptions Implement the field mapper Define the data query function Define the custom data entry Define the custom data entry registration Register your custom data with the Contacts API instance Use your custom data in queries, inserts, updates, and deletes \u2139\ufe0f Maybe someday someone with code generation experience (or I'll learn how to do it), will create annotations and annotation processors to eliminate having to manually write this stuff =) To help illustrate the above steps, we'll use the HandleName and Gender custom data provided in this library's customdata-handlename and customdata-gender respectively as an example. \u2139\ufe0f For more specifics on these custom data, read Integrate the gender custom data and Integrate the handle name custom data . At the bottom of this page, we'll also discuss, Consider adding your custom data to this library Custom data without sync adapters will not be synced Displaying your custom data in other Contacts apps Summary of limitations \u2139\ufe0f Some of the code used in these examples are in Kotlin. If you would like a Java version of this page, create an issue in GitHub. You are also free to file a pull request with your own page. In the event that a Java version of this page is created, this quote block should be replaced with a link to that page. 1. Define the mimetype \u00b6 The mimetype is a string that describes what kind of data a row in the Data table represents. For Gender , internal object GenderMimeType : MimeType . Custom () { // Following Contacts Provider convention of \"vnd.android.cursor.item/.\" override val value : String = \"vnd.android.cursor.item/contacts.entities.custom.gender\" } For HandleName , internal object HandleNameMimeType : MimeType . Custom () { // Following Contacts Provider convention of \"vnd.android.cursor.item/.\" override val value : String = \"vnd.android.cursor.item/contacts.entities.custom.handlename\" } Do not change the mimetype value! If you have already deployed apps to production that use these mimetype values, then changing them could result in \"data loss\". Old rows in the Data table will not be compatible if the mimetype value changes. You can certainly perform migrations by creating a new custom data altogether and migrating your old custom data to your new one. Do not use built-in mimetypes! The Contacts Provider has predefined the mimetypes for all of the common data kinds it supports (e.g. email). Make sure that your custom data does not use any of those. You can take a look at built-in mimetypes in contacs.core.entities.MimeType.kt . But, here they are for your convenience =) Builtin data kind mimetype Address \"vnd.android.cursor.item/postal-address_v2\" Email \"vnd.android.cursor.item/email_v2\" Event \"vnd.android.cursor.item/contact_event\" GroupMembership \"vnd.android.cursor.item/group_membership\" Im \"vnd.android.cursor.item/im\" Name \"vnd.android.cursor.item/name\" Nickname \"vnd.android.cursor.item/nickname\" Note \"vnd.android.cursor.item/note\" Organization \"vnd.android.cursor.item/organization\" Phone \"vnd.android.cursor.item/phone_v2\" Photo \"vnd.android.cursor.item/photo\" Relation \"vnd.android.cursor.item/relation\" SipAddress \"vnd.android.cursor.item/sip_address\" Website \"vnd.android.cursor.item/website\" 2. Define the entities \u00b6 The entities are the main code that users of your custom data will be exposed to. The properties model/represent the fields/columns in the Data table. Due to the length of the Gender.kt and HandleName.kt files, I will not be copy-pasting them here. Please take a look at those files instead. A few things to note, Either inherit from CustomDataEntity or CustomDataEntityWithTypeAndLabel . Implement the mimeType using the mimetype you defined in the previous step. Implement the isBlank using the contacts.core.entities.propertiesAreAllNullOrBlank function. Put the properties that you consider to be important such that if they are null, then the data is useless (blank). Define an immutable class so that instances can be returned on queries. These would also need to inherit from ExistingCustomDataEntity and ImmutableCustomDataEntityWithMutableType (or ImmutableCustomDataEntityWithNullableMutableType ). All properties and types defined here must be immutable ( val ). Define a mutable class so that instances can be updated. These would also need to inherit from ExistingCustomDataEntity . Only modifiable fields should have properties and types defined as mutable ( var ). Define a \"new\" class so that instances can be inserted. These would also need to inherit from NewCustomDataEntity . Only modifiable fields should have properties and types defined as mutable ( var ). Properties that map to your custom data fields should be nullable ( ? ). The following properties should always be immutable ( val ); id , rawContactId , contactId , isPrimary , isSuperPrimary , and isRedacted . Be mindful of what properties should be redacted when implementing the redactedCopy function. All entity class must implement Parecelable . 3. Define the fields \u00b6 Fields (or columns) represent (or map to) one of the properties you defined in the previous step. These are used in queries, inserts, and update operations. For Gender , data class GenderField internal constructor ( private val columnName : ColumnName ) : AbstractCustomDataField ( columnName ) { override val customMimeType : MimeType . Custom = GenderMimeType } object GenderFields : AbstractCustomDataFieldSet < GenderField > () { @JvmField val Type = GenderField ( ColumnName . TYPE ) @JvmField val Label = GenderField ( ColumnName . LABEL ) override val all : Set < GenderField > = setOf ( Type , Label ) override val forMatching : Set < GenderField > = emptySet () } For HandleName , data class HandleNameField internal constructor ( private val columnName : ColumnName ) : AbstractCustomDataField ( columnName ) { override val customMimeType : MimeType . Custom = HandleNameMimeType } object HandleNameFields : AbstractCustomDataFieldSet < HandleNameField > () { @JvmField val Handle = HandleNameField ( ColumnName . DATA ) override val all : Set < HandleNameField > = setOf ( Handle ) override val forMatching : Set < HandleNameField > = setOf ( Handle ) } A few things to note, You need to define a AbstractCustomDataField and a AbstractCustomDataFieldSet . Annotate your field instances with @JvmField to make it more accessible for Java users. This is only helpful if you are writing code for other people to use. Carefully choose what to put in all and forMatching . If you are using ColumnName.BLOB , do not put it in all or forMatching ! For more info, read the in-code documentation on it. 4. Implement the cursor \u00b6 Cursors read the values from the Data table and convert them into the types you want (e.g. String). For Gender , internal class GenderDataCursor ( cursor : Cursor , includeFields : Set < GenderField > ) : AbstractCustomDataCursor < GenderField > ( cursor , includeFields ) { val type : GenderEntity . Type ? by type ( GenderFields . Type , typeFromValue = GenderEntity . Type :: fromValue ) val label : String? by string ( GenderFields . Label ) } For HandleName , internal class HandleNameDataCursor ( cursor : Cursor , includeFields : Set < HandleNameField > ) : AbstractCustomDataCursor < HandleNameField > ( cursor , includeFields ) { val handle : String? by string ( HandleNameFields . Handle ) } A few things to note, Inheritors of AbstractCustomDataCursor have access to several regular and delegate functions that extract data. All of them are defined in contacts.core.entities.cursor.AbstractEntityCursor . If you are using Java, you are only able to use the regular functions. The delegate functions are prettier but use Kotlin reflection, which could slightly affect runtime performance. You can either extract nullable or non-nullable values using these functions. 5. Implement the mapper \u00b6 Mappers use the cursors implemented in the previous step in order to create instances of your custom data entities. For Gender , internal class GenderMapperFactory : AbstractCustomDataEntityMapper . Factory < GenderField , GenderDataCursor , Gender > { override fun create ( cursor : Cursor , includeFields : Set < GenderField > ): AbstractCustomDataEntityMapper < GenderField , GenderDataCursor , Gender > = GenderMapper ( GenderDataCursor ( cursor , includeFields )) } private class GenderMapper ( cursor : GenderDataCursor ) : AbstractCustomDataEntityMapper < GenderField , GenderDataCursor , Gender > ( cursor ) { override fun value ( cursor : GenderDataCursor ) = Gender ( id = cursor . dataId , rawContactId = cursor . rawContactId , contactId = cursor . contactId , isPrimary = cursor . isPrimary , isSuperPrimary = cursor . isSuperPrimary , type = cursor . type , label = cursor . label , isRedacted = false ) } For HandleName , internal class HandleNameMapperFactory : AbstractCustomDataEntityMapper . Factory < HandleNameField , HandleNameDataCursor , HandleName > { override fun create ( cursor : Cursor , includeFields : Set < HandleNameField > ): AbstractCustomDataEntityMapper < HandleNameField , HandleNameDataCursor , HandleName > = HandleNameMapper ( HandleNameDataCursor ( cursor , includeFields )) } private class HandleNameMapper ( cursor : HandleNameDataCursor ) : AbstractCustomDataEntityMapper < HandleNameField , HandleNameDataCursor , HandleName > ( cursor ) { override fun value ( cursor : HandleNameDataCursor ) = HandleName ( id = cursor . dataId , rawContactId = cursor . rawContactId , contactId = cursor . contactId , isPrimary = cursor . isPrimary , isSuperPrimary = cursor . isSuperPrimary , handle = cursor . handle , isRedacted = false ) } A few things to note, This requires definitions and implementations done in the previous steps. If you are having compile-time issues at this point, make sure that you did not skip a step! Ensure that isRedacted is set to false (unless you are already performing the redaction) here. 6. Implement the operation \u00b6 Operations are used for inserts and updates from in-memory instances of your entities to the database. For Gender , internal class GenderOperationFactory : AbstractCustomDataOperation . Factory < GenderField , GenderEntity > { override fun create ( isProfile : Boolean , includeFields : Set < GenderField > ): AbstractCustomDataOperation < GenderField , GenderEntity > = GenderOperation ( isProfile , includeFields ) } private class GenderOperation ( isProfile : Boolean , includeFields : Set < GenderField > ) : AbstractCustomDataOperation < GenderField , GenderEntity > ( isProfile , includeFields ) { override val mimeType : MimeType . Custom = GenderMimeType override fun setCustomData ( data : GenderEntity , setValue : ( field : GenderField , value : Any? ) -> Unit ) { setValue ( GenderFields . Type , data . type ?. value ) setValue ( GenderFields . Label , data . label ) } } For HandleName , internal class HandleNameOperationFactory : AbstractCustomDataOperation . Factory < HandleNameField , HandleNameEntity > { override fun create ( isProfile : Boolean , includeFields : Set < HandleNameField > ): AbstractCustomDataOperation < HandleNameField , HandleNameEntity > = HandleNameOperation ( isProfile , includeFields ) } private class HandleNameOperation ( isProfile : Boolean , includeFields : Set < HandleNameField > ) : AbstractCustomDataOperation < HandleNameField , HandleNameEntity > ( isProfile , includeFields ) { override val mimeType : MimeType . Custom = HandleNameMimeType override fun setCustomData ( data : HandleNameEntity , setValue : ( field : HandleNameField , value : Any? ) -> Unit ) { setValue ( HandleNameFields . Handle , data . handle ) } } A few things to note, You just need to use your custom data fields and the corresponding data property it maps to in the setValue function provided in the setCustomData function. 7. Define the count restriction \u00b6 The count restriction defines whether a RawContact can have 0 or 1 of your custom data or if it can have 0, 1, or more. For Gender , /** * A RawContact may have at most 1 gender. */ internal val GENDER_COUNT_RESTRICTION = CustomDataCountRestriction . AT_MOST_ONE For HandleName , /** * A RawContact may have 0, 1, or more handle names. */ internal val HANDLE_NAME_COUNT_RESTRICTION = CustomDataCountRestriction . NO_LIMIT 8. Define RawContact getters and setters \u00b6 In order for you or your consumers to be able to get and set your custom data in instances of RawContacts they belong to, you must define a set of getters and setters. Due to the length of the RawContactGender.kt and RawContactHandleName.kt files, I will not be copy-pasting them here. Please take a look at those files instead. A few things to note, For getters, use the Contacts.customDataRegistry.customDataEntitiesFor function to extract the custom data instance(s) for the RawContact with your custom mimetype. Consider returning Sequence for the getters for optimizations in Kotlin. For setters use, the Contacts.customDataRegistry.putCustomDataEntityInto function to set the custom data instance into the RawContact. the Contacts.customDataRegistry.removeAllCustomDataEntityFrom function to remove the custom data instance from the RawContact. Define getters and setters for RawContact , MutableRawContact , and NewRawContact . Ensure to match the type of RawContact with the type of the custom data. For example, RawContact -> Gender , HandleName MutableRawContact -> MutableGenderEntity , MutableHandleNameEntity When setting/adding a new custom data entity, MutableRawContact -> NewGender , NewHandleName NewRawContact -> NewGender , NewHandleName Setters for custom data with count restriction of AT_MOST_ONE should use setXXX for the function name. Setters for custom data with count restriction of NO_LIMIT should use addXXX and removeXXX for the function names. 9. Define Contact getters and setters \u00b6 Defining getters and setters for RawContacts is the bare minimum. However, if you want to add some convenience functions so that you can access RawContact getters and setters from a Contact, then you are free (and recommended) to do so. Due to the length of the ContactGender.kt and ContactHandleName.kt files, I will not be copy-pasting them here. Please take a look at those files instead. A few things to note, For getters, consider returning Sequence for optimizations in Kotlin. For setters, use the first RawContact (in case there are more than one). Consider returning Sequence for the getters for optimizations in Kotlin. Define getters and setters for Contact and MutableContact . Ensure to match the type of Contact with the type of the custom data. For example, Contact -> Gender , HandleName MutableContact -> MutableGenderEntity , MutableHandleNameEntity When setting/adding a new custom data entity, MutableContact -> NewGender , NewHandleName 10. Define exceptions \u00b6 Whether you are building this custom data just for your own app or for others to use, it is useful to define a subclass of CustomDataException to help identify errors in certain custom data integrations. For Gender , class GenderDataException ( message : String ) : CustomDataException ( message ) For HandleName , class HandleNameDataException ( message : String ) : CustomDataException ( message ) 11. Implement the field mapper \u00b6 A field mapper maps your custom data field to the corresponding property in your custom data entity. For Gender , internal class GenderFieldMapper : CustomDataFieldMapper < GenderField , GenderEntity > { override fun valueOf ( field : GenderField , customDataEntity : GenderEntity ): String? = when ( field ) { GenderFields . Type -> customDataEntity . type ?. ordinal ?. toString () GenderFields . Label -> customDataEntity . label else -> throw GenderDataException ( \"Unrecognized gender field $ field \" ) } } For HandleName , internal class HandleNameFieldMapper : CustomDataFieldMapper < HandleNameField , HandleNameEntity > { override fun valueOf ( field : HandleNameField , customDataEntity : HandleNameEntity ): String? = when ( field ) { HandleNameFields . Handle -> customDataEntity . handle else -> throw HandleNameDataException ( \"Unrecognized handle name field $ field \" ) } } A few things to note, You should throw an instance of your custom data exception in the case that there is no mapping from a field to a property. This ensures that your custom data integration will fail and fail-fast in case you forget to add a mapping to a property. 12. Define the data query function \u00b6 These (extension) functions on the DataQueryFactory allows you and your consumers to use the DataQuery API to specifically query for only your custom data kind instead of Contacts. For Gender , fun DataQueryFactory . genders (): DataQuery < GenderField , GenderFields , Gender > = customData ( GenderMimeType ) For HandleName , fun DataQueryFactory . handleNames (): DataQuery < HandleNameField , HandleNameFields , HandleName > = customData ( HandleNameMimeType ) For more info on the DataQuery API, read Query specific data kinds and Query custom data . 13. Define the custom data entry \u00b6 The entry puts everything together so that it can be handed off to the custom data registry to integrate your custom data with all of the APIs provided in the library. For Gender , internal class GenderEntry : Entry < GenderField , GenderDataCursor , GenderEntity , Gender > { override val mimeType = GenderMimeType override val fieldSet = GenderFields override val fieldMapper = GenderFieldMapper () override val countRestriction = GENDER_COUNT_RESTRICTION override val mapperFactory = GenderMapperFactory () override val operationFactory = GenderOperationFactory () } For HandleName , internal class HandleNameEntry : Entry < HandleNameField , HandleNameDataCursor , HandleNameEntity , HandleName > { override val mimeType = HandleNameMimeType override val fieldSet = HandleNameFields override val fieldMapper = HandleNameFieldMapper () override val countRestriction = HANDLE_NAME_COUNT_RESTRICTION override val mapperFactory = HandleNameMapperFactory () override val operationFactory = HandleNameOperationFactory () } 14. Define the custom data entry registration \u00b6 The entry registration provides a way for you to keep your Entry internal to your library module. \u2139\ufe0f In Java, the closest thing to this is package-private. This is not necessary to implement. Feel free to make your Entry public so that it can be handed off to the custom data registry. For Gender , class GenderRegistration : CustomDataRegistry . EntryRegistration { override fun registerTo ( customDataRegistry : CustomDataRegistry ) { customDataRegistry . register ( GenderEntry ()) } } For HandleName , class HandleNameRegistration : CustomDataRegistry . EntryRegistration { override fun registerTo ( customDataRegistry : CustomDataRegistry ) { customDataRegistry . register ( HandleNameEntry ()) } } 15. Register your custom data with the Contacts API instance \u00b6 There are two ways to register your custom data. Either using the entry registration defined in the previous step or the entry itself defined in the step prior. Using Gender and HandleName entry registration, val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( GenderRegistration (), HandleNameRegistration () ) ) Alternatively, val contactsApi = Contacts ( context ) GenderRegistration (). registerTo ( contactsApi . customDataRegistry ) HandleNameRegistration (). registerTo ( contactsApi . customDataRegistry ) Using Gender and HandleName entry, \u2139\ufe0f This is not possible with Gender and HandleName as their entries are internal. This is for demonstration purposes only. val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( GenderEntry (), HandleNameEntry () ) ) 16. Use your custom data in queries, inserts, updates, and deletes \u00b6 Once you have registered your custom data with the Contacts API instance, the API instance is now able to perform read and write operations on it. Query custom data Insert custom data into new or existing contacts Update custom data Delete custom data Consider adding your custom data to this library \u00b6 Let's say that you have created your own custom data in your own app. That's great and all but your app will be the only app that will be able to perform operations on it (unless the mimetype value you are using is also used by others). This is definitely something you want to do if you don't really want others to mess with your custom data (though you can't really stop others). If you want to add your custom data to this library so that other people using this library can optionally integrate it into their own apps, please create a GitHub issue and file a pull request! Custom data without sync adapters will not be synced \u00b6 Custom data provided by this library such as those in those in the customdata-gender , customdata-handlename , customdata-pokemon , and customdata-rpg modules are not synced because there are no sync adapters and a remote service to store those data. Therefore, they are not synced across devices and will remain local to the device regardless of Account sync settings. It is up to you to implement your own sync adapters for your own custom data. For more info, read Sync contact data across devices . Displaying your custom data in other Contacts apps \u00b6 If you want your custom data to be visible in the Android Open Source Project (AOSP) Contacts app (the default Contacts app that comes with a vanilla version of Android) and the Google Contacts app , then read this section. This is optional. If you only want your custom data to be visible in your application, then you should NOT do the things described in this part of the guide. \u2139\ufe0f The Google Contacts app keeps its \"File as\" custom data invisible to other Contacts apps such as the AOSP Contacts app. However, it exposes the \"Custom field+label\" custom data by doing the things described in this section. Important! The first criteria for being able to show your custom data in the Contacts app is to define and implement your own sync adapter. If you do not have a sync adapter implementation, your custom data will not be shown in the Contacts app! Again, this library does not provide any sync adapters. That is for you to implement based on your account services. This library provides you and users of your library an easy, uniform way to perform read and write operations on your custom data. The act of syncing is up to you. The official documentation on custom data rows is as follows, By creating and using your own custom MIME types, you can insert, edit, delete, and retrieve your own data rows in the ContactsContract.Data table. Your rows are limited to using the column defined in ContactsContract.DataColumns , although you can map your own type-specific column names to the default column names. In the device's contacts application, the data for your rows is displayed but can't be edited or deleted, and users can't add additional data. To allow users to modify your custom data rows, you must provide an editor activity in your own application. To display your custom data, provide a contacts.xml file containing a element and one or more of its child elements. This is described in more detail in the section element. Let's break down the official documentation. Contacts applications such as the Android Open Source Project (AOSP) Contacts app (the default Contacts app that comes with a vanilla version of Android) and the Google Contacts app (and other Contacts app that support this feature) shows custom data from other apps when viewing contact details. Custom data from other apps are viewable but not editable in order to preserve and respect the rules surrounding those custom data managed by other apps. This library allows you to read (query) and write (insert, update, delete) custom data from other apps. It is up to you whether you want to follow the same limitations imposed by the AOSP and Google Contacts app. In order to show your custom data in the AOSP Contacts app and Google Contacts app (and other Contacts app that support this feature), you must add an xml file in your app; res/xml/contacts.xml . The res/xml/contacts.xml template looks like this, The full official documentation for each of those tags and attributes within each tag are available by clicking this link . For example, the bare-minimum contacts.xml for showing Gender and HandleName custom data in the AOSP and Google Contacts app is the following, A few things to note, The value of android:mimeType corresponds to the String value defined in GenderMimeType and HandleNameMimeType as seen in the previous sections of this guide. The value of android:summaryColumn and android:detailColumn corresponds to the values defined in contacts.core.Fields.kt#AbstractCustomDataField.ColumnName that are used by GenderFields and HandleNameFields . These values, as raw strings, are; data1 , data2 , data3 ,... data15 Again, in order for your custom data to be shown in the Contacts app, you must also provide a sync adapter implementation. For more info, read Sync contact data across devices . Summary of limitations \u00b6 To reiterate, this library does not provide a remote server or sync adapters to interface with that server. This library provides create (insert), read (query), update, and delete (CRUD) APIs for pretty, type-safe, and well-documented read and write operations on all data kinds, including custom data. This means that if you do not implement your own sync adapter for your custom data, then your custom data... will NOT be synced across devices will NOT be shown in AOSP and Google Contacts apps, and other Contacts apps that show custom data from other apps You may still do creative things with custom data without sync adapters as long as you understand these limitations. This library provides CRUD API integration with custom data with no sync adapters; customdata-gender customdata-handlename customdata-pokemon customdata-rpg Also provided are CRUD API integration with custom data from other apps that do have sync adapters; customdata-googlecontacts \u2139\ufe0f Please update the above list whenever adding new custom data modules.","title":"Integrate custom data"},{"location":"customdata/integrate-custom-data/#integrate-custom-data","text":"If you are looking to integrate custom data from other apps, read Integrate custom data from other apps . If you are looking to create and integrate your own custom data, you are in the right place! There are two parts to \"integrating custom data\"; Providing create (insert), read (query), update, and delete (CRUD) APIs for custom data associated with RawContacts. Providing sync adapters to sync custom data across devices. This library only handles the first part. If you want to sync your custom data, then you need to implement a sync adapter to interface with your remote server. That is out of scope of this library. In order to create and integrate your own custom data for use in your own apps, there is a bit of boilerplate code that needs to be written. Thankfully none of this stuff is difficult! Here are the steps, in chronological order, on how to define and use your own custom data, Define the mimetype Define the entities Define the fields Implement the cursor Implement the mapper Implement the operation Define the count restriction Define RawContact getters and setters Define Contact getters and setters Define exceptions Implement the field mapper Define the data query function Define the custom data entry Define the custom data entry registration Register your custom data with the Contacts API instance Use your custom data in queries, inserts, updates, and deletes \u2139\ufe0f Maybe someday someone with code generation experience (or I'll learn how to do it), will create annotations and annotation processors to eliminate having to manually write this stuff =) To help illustrate the above steps, we'll use the HandleName and Gender custom data provided in this library's customdata-handlename and customdata-gender respectively as an example. \u2139\ufe0f For more specifics on these custom data, read Integrate the gender custom data and Integrate the handle name custom data . At the bottom of this page, we'll also discuss, Consider adding your custom data to this library Custom data without sync adapters will not be synced Displaying your custom data in other Contacts apps Summary of limitations \u2139\ufe0f Some of the code used in these examples are in Kotlin. If you would like a Java version of this page, create an issue in GitHub. You are also free to file a pull request with your own page. In the event that a Java version of this page is created, this quote block should be replaced with a link to that page.","title":"Integrate custom data"},{"location":"customdata/integrate-custom-data/#1-define-the-mimetype","text":"The mimetype is a string that describes what kind of data a row in the Data table represents. For Gender , internal object GenderMimeType : MimeType . Custom () { // Following Contacts Provider convention of \"vnd.android.cursor.item/.\" override val value : String = \"vnd.android.cursor.item/contacts.entities.custom.gender\" } For HandleName , internal object HandleNameMimeType : MimeType . Custom () { // Following Contacts Provider convention of \"vnd.android.cursor.item/.\" override val value : String = \"vnd.android.cursor.item/contacts.entities.custom.handlename\" } Do not change the mimetype value! If you have already deployed apps to production that use these mimetype values, then changing them could result in \"data loss\". Old rows in the Data table will not be compatible if the mimetype value changes. You can certainly perform migrations by creating a new custom data altogether and migrating your old custom data to your new one. Do not use built-in mimetypes! The Contacts Provider has predefined the mimetypes for all of the common data kinds it supports (e.g. email). Make sure that your custom data does not use any of those. You can take a look at built-in mimetypes in contacs.core.entities.MimeType.kt . But, here they are for your convenience =) Builtin data kind mimetype Address \"vnd.android.cursor.item/postal-address_v2\" Email \"vnd.android.cursor.item/email_v2\" Event \"vnd.android.cursor.item/contact_event\" GroupMembership \"vnd.android.cursor.item/group_membership\" Im \"vnd.android.cursor.item/im\" Name \"vnd.android.cursor.item/name\" Nickname \"vnd.android.cursor.item/nickname\" Note \"vnd.android.cursor.item/note\" Organization \"vnd.android.cursor.item/organization\" Phone \"vnd.android.cursor.item/phone_v2\" Photo \"vnd.android.cursor.item/photo\" Relation \"vnd.android.cursor.item/relation\" SipAddress \"vnd.android.cursor.item/sip_address\" Website \"vnd.android.cursor.item/website\"","title":"1. Define the mimetype"},{"location":"customdata/integrate-custom-data/#2-define-the-entities","text":"The entities are the main code that users of your custom data will be exposed to. The properties model/represent the fields/columns in the Data table. Due to the length of the Gender.kt and HandleName.kt files, I will not be copy-pasting them here. Please take a look at those files instead. A few things to note, Either inherit from CustomDataEntity or CustomDataEntityWithTypeAndLabel . Implement the mimeType using the mimetype you defined in the previous step. Implement the isBlank using the contacts.core.entities.propertiesAreAllNullOrBlank function. Put the properties that you consider to be important such that if they are null, then the data is useless (blank). Define an immutable class so that instances can be returned on queries. These would also need to inherit from ExistingCustomDataEntity and ImmutableCustomDataEntityWithMutableType (or ImmutableCustomDataEntityWithNullableMutableType ). All properties and types defined here must be immutable ( val ). Define a mutable class so that instances can be updated. These would also need to inherit from ExistingCustomDataEntity . Only modifiable fields should have properties and types defined as mutable ( var ). Define a \"new\" class so that instances can be inserted. These would also need to inherit from NewCustomDataEntity . Only modifiable fields should have properties and types defined as mutable ( var ). Properties that map to your custom data fields should be nullable ( ? ). The following properties should always be immutable ( val ); id , rawContactId , contactId , isPrimary , isSuperPrimary , and isRedacted . Be mindful of what properties should be redacted when implementing the redactedCopy function. All entity class must implement Parecelable .","title":"2. Define the entities"},{"location":"customdata/integrate-custom-data/#3-define-the-fields","text":"Fields (or columns) represent (or map to) one of the properties you defined in the previous step. These are used in queries, inserts, and update operations. For Gender , data class GenderField internal constructor ( private val columnName : ColumnName ) : AbstractCustomDataField ( columnName ) { override val customMimeType : MimeType . Custom = GenderMimeType } object GenderFields : AbstractCustomDataFieldSet < GenderField > () { @JvmField val Type = GenderField ( ColumnName . TYPE ) @JvmField val Label = GenderField ( ColumnName . LABEL ) override val all : Set < GenderField > = setOf ( Type , Label ) override val forMatching : Set < GenderField > = emptySet () } For HandleName , data class HandleNameField internal constructor ( private val columnName : ColumnName ) : AbstractCustomDataField ( columnName ) { override val customMimeType : MimeType . Custom = HandleNameMimeType } object HandleNameFields : AbstractCustomDataFieldSet < HandleNameField > () { @JvmField val Handle = HandleNameField ( ColumnName . DATA ) override val all : Set < HandleNameField > = setOf ( Handle ) override val forMatching : Set < HandleNameField > = setOf ( Handle ) } A few things to note, You need to define a AbstractCustomDataField and a AbstractCustomDataFieldSet . Annotate your field instances with @JvmField to make it more accessible for Java users. This is only helpful if you are writing code for other people to use. Carefully choose what to put in all and forMatching . If you are using ColumnName.BLOB , do not put it in all or forMatching ! For more info, read the in-code documentation on it.","title":"3. Define the fields"},{"location":"customdata/integrate-custom-data/#4-implement-the-cursor","text":"Cursors read the values from the Data table and convert them into the types you want (e.g. String). For Gender , internal class GenderDataCursor ( cursor : Cursor , includeFields : Set < GenderField > ) : AbstractCustomDataCursor < GenderField > ( cursor , includeFields ) { val type : GenderEntity . Type ? by type ( GenderFields . Type , typeFromValue = GenderEntity . Type :: fromValue ) val label : String? by string ( GenderFields . Label ) } For HandleName , internal class HandleNameDataCursor ( cursor : Cursor , includeFields : Set < HandleNameField > ) : AbstractCustomDataCursor < HandleNameField > ( cursor , includeFields ) { val handle : String? by string ( HandleNameFields . Handle ) } A few things to note, Inheritors of AbstractCustomDataCursor have access to several regular and delegate functions that extract data. All of them are defined in contacts.core.entities.cursor.AbstractEntityCursor . If you are using Java, you are only able to use the regular functions. The delegate functions are prettier but use Kotlin reflection, which could slightly affect runtime performance. You can either extract nullable or non-nullable values using these functions.","title":"4. Implement the cursor"},{"location":"customdata/integrate-custom-data/#5-implement-the-mapper","text":"Mappers use the cursors implemented in the previous step in order to create instances of your custom data entities. For Gender , internal class GenderMapperFactory : AbstractCustomDataEntityMapper . Factory < GenderField , GenderDataCursor , Gender > { override fun create ( cursor : Cursor , includeFields : Set < GenderField > ): AbstractCustomDataEntityMapper < GenderField , GenderDataCursor , Gender > = GenderMapper ( GenderDataCursor ( cursor , includeFields )) } private class GenderMapper ( cursor : GenderDataCursor ) : AbstractCustomDataEntityMapper < GenderField , GenderDataCursor , Gender > ( cursor ) { override fun value ( cursor : GenderDataCursor ) = Gender ( id = cursor . dataId , rawContactId = cursor . rawContactId , contactId = cursor . contactId , isPrimary = cursor . isPrimary , isSuperPrimary = cursor . isSuperPrimary , type = cursor . type , label = cursor . label , isRedacted = false ) } For HandleName , internal class HandleNameMapperFactory : AbstractCustomDataEntityMapper . Factory < HandleNameField , HandleNameDataCursor , HandleName > { override fun create ( cursor : Cursor , includeFields : Set < HandleNameField > ): AbstractCustomDataEntityMapper < HandleNameField , HandleNameDataCursor , HandleName > = HandleNameMapper ( HandleNameDataCursor ( cursor , includeFields )) } private class HandleNameMapper ( cursor : HandleNameDataCursor ) : AbstractCustomDataEntityMapper < HandleNameField , HandleNameDataCursor , HandleName > ( cursor ) { override fun value ( cursor : HandleNameDataCursor ) = HandleName ( id = cursor . dataId , rawContactId = cursor . rawContactId , contactId = cursor . contactId , isPrimary = cursor . isPrimary , isSuperPrimary = cursor . isSuperPrimary , handle = cursor . handle , isRedacted = false ) } A few things to note, This requires definitions and implementations done in the previous steps. If you are having compile-time issues at this point, make sure that you did not skip a step! Ensure that isRedacted is set to false (unless you are already performing the redaction) here.","title":"5. Implement the mapper"},{"location":"customdata/integrate-custom-data/#6-implement-the-operation","text":"Operations are used for inserts and updates from in-memory instances of your entities to the database. For Gender , internal class GenderOperationFactory : AbstractCustomDataOperation . Factory < GenderField , GenderEntity > { override fun create ( isProfile : Boolean , includeFields : Set < GenderField > ): AbstractCustomDataOperation < GenderField , GenderEntity > = GenderOperation ( isProfile , includeFields ) } private class GenderOperation ( isProfile : Boolean , includeFields : Set < GenderField > ) : AbstractCustomDataOperation < GenderField , GenderEntity > ( isProfile , includeFields ) { override val mimeType : MimeType . Custom = GenderMimeType override fun setCustomData ( data : GenderEntity , setValue : ( field : GenderField , value : Any? ) -> Unit ) { setValue ( GenderFields . Type , data . type ?. value ) setValue ( GenderFields . Label , data . label ) } } For HandleName , internal class HandleNameOperationFactory : AbstractCustomDataOperation . Factory < HandleNameField , HandleNameEntity > { override fun create ( isProfile : Boolean , includeFields : Set < HandleNameField > ): AbstractCustomDataOperation < HandleNameField , HandleNameEntity > = HandleNameOperation ( isProfile , includeFields ) } private class HandleNameOperation ( isProfile : Boolean , includeFields : Set < HandleNameField > ) : AbstractCustomDataOperation < HandleNameField , HandleNameEntity > ( isProfile , includeFields ) { override val mimeType : MimeType . Custom = HandleNameMimeType override fun setCustomData ( data : HandleNameEntity , setValue : ( field : HandleNameField , value : Any? ) -> Unit ) { setValue ( HandleNameFields . Handle , data . handle ) } } A few things to note, You just need to use your custom data fields and the corresponding data property it maps to in the setValue function provided in the setCustomData function.","title":"6. Implement the operation"},{"location":"customdata/integrate-custom-data/#7-define-the-count-restriction","text":"The count restriction defines whether a RawContact can have 0 or 1 of your custom data or if it can have 0, 1, or more. For Gender , /** * A RawContact may have at most 1 gender. */ internal val GENDER_COUNT_RESTRICTION = CustomDataCountRestriction . AT_MOST_ONE For HandleName , /** * A RawContact may have 0, 1, or more handle names. */ internal val HANDLE_NAME_COUNT_RESTRICTION = CustomDataCountRestriction . NO_LIMIT","title":"7. Define the count restriction"},{"location":"customdata/integrate-custom-data/#8-define-rawcontact-getters-and-setters","text":"In order for you or your consumers to be able to get and set your custom data in instances of RawContacts they belong to, you must define a set of getters and setters. Due to the length of the RawContactGender.kt and RawContactHandleName.kt files, I will not be copy-pasting them here. Please take a look at those files instead. A few things to note, For getters, use the Contacts.customDataRegistry.customDataEntitiesFor function to extract the custom data instance(s) for the RawContact with your custom mimetype. Consider returning Sequence for the getters for optimizations in Kotlin. For setters use, the Contacts.customDataRegistry.putCustomDataEntityInto function to set the custom data instance into the RawContact. the Contacts.customDataRegistry.removeAllCustomDataEntityFrom function to remove the custom data instance from the RawContact. Define getters and setters for RawContact , MutableRawContact , and NewRawContact . Ensure to match the type of RawContact with the type of the custom data. For example, RawContact -> Gender , HandleName MutableRawContact -> MutableGenderEntity , MutableHandleNameEntity When setting/adding a new custom data entity, MutableRawContact -> NewGender , NewHandleName NewRawContact -> NewGender , NewHandleName Setters for custom data with count restriction of AT_MOST_ONE should use setXXX for the function name. Setters for custom data with count restriction of NO_LIMIT should use addXXX and removeXXX for the function names.","title":"8. Define RawContact getters and setters"},{"location":"customdata/integrate-custom-data/#9-define-contact-getters-and-setters","text":"Defining getters and setters for RawContacts is the bare minimum. However, if you want to add some convenience functions so that you can access RawContact getters and setters from a Contact, then you are free (and recommended) to do so. Due to the length of the ContactGender.kt and ContactHandleName.kt files, I will not be copy-pasting them here. Please take a look at those files instead. A few things to note, For getters, consider returning Sequence for optimizations in Kotlin. For setters, use the first RawContact (in case there are more than one). Consider returning Sequence for the getters for optimizations in Kotlin. Define getters and setters for Contact and MutableContact . Ensure to match the type of Contact with the type of the custom data. For example, Contact -> Gender , HandleName MutableContact -> MutableGenderEntity , MutableHandleNameEntity When setting/adding a new custom data entity, MutableContact -> NewGender , NewHandleName","title":"9. Define Contact getters and setters"},{"location":"customdata/integrate-custom-data/#10-define-exceptions","text":"Whether you are building this custom data just for your own app or for others to use, it is useful to define a subclass of CustomDataException to help identify errors in certain custom data integrations. For Gender , class GenderDataException ( message : String ) : CustomDataException ( message ) For HandleName , class HandleNameDataException ( message : String ) : CustomDataException ( message )","title":"10. Define exceptions"},{"location":"customdata/integrate-custom-data/#11-implement-the-field-mapper","text":"A field mapper maps your custom data field to the corresponding property in your custom data entity. For Gender , internal class GenderFieldMapper : CustomDataFieldMapper < GenderField , GenderEntity > { override fun valueOf ( field : GenderField , customDataEntity : GenderEntity ): String? = when ( field ) { GenderFields . Type -> customDataEntity . type ?. ordinal ?. toString () GenderFields . Label -> customDataEntity . label else -> throw GenderDataException ( \"Unrecognized gender field $ field \" ) } } For HandleName , internal class HandleNameFieldMapper : CustomDataFieldMapper < HandleNameField , HandleNameEntity > { override fun valueOf ( field : HandleNameField , customDataEntity : HandleNameEntity ): String? = when ( field ) { HandleNameFields . Handle -> customDataEntity . handle else -> throw HandleNameDataException ( \"Unrecognized handle name field $ field \" ) } } A few things to note, You should throw an instance of your custom data exception in the case that there is no mapping from a field to a property. This ensures that your custom data integration will fail and fail-fast in case you forget to add a mapping to a property.","title":"11. Implement the field mapper"},{"location":"customdata/integrate-custom-data/#12-define-the-data-query-function","text":"These (extension) functions on the DataQueryFactory allows you and your consumers to use the DataQuery API to specifically query for only your custom data kind instead of Contacts. For Gender , fun DataQueryFactory . genders (): DataQuery < GenderField , GenderFields , Gender > = customData ( GenderMimeType ) For HandleName , fun DataQueryFactory . handleNames (): DataQuery < HandleNameField , HandleNameFields , HandleName > = customData ( HandleNameMimeType ) For more info on the DataQuery API, read Query specific data kinds and Query custom data .","title":"12. Define the data query function"},{"location":"customdata/integrate-custom-data/#13-define-the-custom-data-entry","text":"The entry puts everything together so that it can be handed off to the custom data registry to integrate your custom data with all of the APIs provided in the library. For Gender , internal class GenderEntry : Entry < GenderField , GenderDataCursor , GenderEntity , Gender > { override val mimeType = GenderMimeType override val fieldSet = GenderFields override val fieldMapper = GenderFieldMapper () override val countRestriction = GENDER_COUNT_RESTRICTION override val mapperFactory = GenderMapperFactory () override val operationFactory = GenderOperationFactory () } For HandleName , internal class HandleNameEntry : Entry < HandleNameField , HandleNameDataCursor , HandleNameEntity , HandleName > { override val mimeType = HandleNameMimeType override val fieldSet = HandleNameFields override val fieldMapper = HandleNameFieldMapper () override val countRestriction = HANDLE_NAME_COUNT_RESTRICTION override val mapperFactory = HandleNameMapperFactory () override val operationFactory = HandleNameOperationFactory () }","title":"13. Define the custom data entry"},{"location":"customdata/integrate-custom-data/#14-define-the-custom-data-entry-registration","text":"The entry registration provides a way for you to keep your Entry internal to your library module. \u2139\ufe0f In Java, the closest thing to this is package-private. This is not necessary to implement. Feel free to make your Entry public so that it can be handed off to the custom data registry. For Gender , class GenderRegistration : CustomDataRegistry . EntryRegistration { override fun registerTo ( customDataRegistry : CustomDataRegistry ) { customDataRegistry . register ( GenderEntry ()) } } For HandleName , class HandleNameRegistration : CustomDataRegistry . EntryRegistration { override fun registerTo ( customDataRegistry : CustomDataRegistry ) { customDataRegistry . register ( HandleNameEntry ()) } }","title":"14. Define the custom data entry registration"},{"location":"customdata/integrate-custom-data/#15-register-your-custom-data-with-the-contacts-api-instance","text":"There are two ways to register your custom data. Either using the entry registration defined in the previous step or the entry itself defined in the step prior. Using Gender and HandleName entry registration, val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( GenderRegistration (), HandleNameRegistration () ) ) Alternatively, val contactsApi = Contacts ( context ) GenderRegistration (). registerTo ( contactsApi . customDataRegistry ) HandleNameRegistration (). registerTo ( contactsApi . customDataRegistry ) Using Gender and HandleName entry, \u2139\ufe0f This is not possible with Gender and HandleName as their entries are internal. This is for demonstration purposes only. val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( GenderEntry (), HandleNameEntry () ) )","title":"15. Register your custom data with the Contacts API instance"},{"location":"customdata/integrate-custom-data/#16-use-your-custom-data-in-queries-inserts-updates-and-deletes","text":"Once you have registered your custom data with the Contacts API instance, the API instance is now able to perform read and write operations on it. Query custom data Insert custom data into new or existing contacts Update custom data Delete custom data","title":"16. Use your custom data in queries, inserts, updates, and deletes"},{"location":"customdata/integrate-custom-data/#consider-adding-your-custom-data-to-this-library","text":"Let's say that you have created your own custom data in your own app. That's great and all but your app will be the only app that will be able to perform operations on it (unless the mimetype value you are using is also used by others). This is definitely something you want to do if you don't really want others to mess with your custom data (though you can't really stop others). If you want to add your custom data to this library so that other people using this library can optionally integrate it into their own apps, please create a GitHub issue and file a pull request!","title":"Consider adding your custom data to this library"},{"location":"customdata/integrate-custom-data/#custom-data-without-sync-adapters-will-not-be-synced","text":"Custom data provided by this library such as those in those in the customdata-gender , customdata-handlename , customdata-pokemon , and customdata-rpg modules are not synced because there are no sync adapters and a remote service to store those data. Therefore, they are not synced across devices and will remain local to the device regardless of Account sync settings. It is up to you to implement your own sync adapters for your own custom data. For more info, read Sync contact data across devices .","title":"Custom data without sync adapters will not be synced"},{"location":"customdata/integrate-custom-data/#displaying-your-custom-data-in-other-contacts-apps","text":"If you want your custom data to be visible in the Android Open Source Project (AOSP) Contacts app (the default Contacts app that comes with a vanilla version of Android) and the Google Contacts app , then read this section. This is optional. If you only want your custom data to be visible in your application, then you should NOT do the things described in this part of the guide. \u2139\ufe0f The Google Contacts app keeps its \"File as\" custom data invisible to other Contacts apps such as the AOSP Contacts app. However, it exposes the \"Custom field+label\" custom data by doing the things described in this section. Important! The first criteria for being able to show your custom data in the Contacts app is to define and implement your own sync adapter. If you do not have a sync adapter implementation, your custom data will not be shown in the Contacts app! Again, this library does not provide any sync adapters. That is for you to implement based on your account services. This library provides you and users of your library an easy, uniform way to perform read and write operations on your custom data. The act of syncing is up to you. The official documentation on custom data rows is as follows, By creating and using your own custom MIME types, you can insert, edit, delete, and retrieve your own data rows in the ContactsContract.Data table. Your rows are limited to using the column defined in ContactsContract.DataColumns , although you can map your own type-specific column names to the default column names. In the device's contacts application, the data for your rows is displayed but can't be edited or deleted, and users can't add additional data. To allow users to modify your custom data rows, you must provide an editor activity in your own application. To display your custom data, provide a contacts.xml file containing a element and one or more of its child elements. This is described in more detail in the section element. Let's break down the official documentation. Contacts applications such as the Android Open Source Project (AOSP) Contacts app (the default Contacts app that comes with a vanilla version of Android) and the Google Contacts app (and other Contacts app that support this feature) shows custom data from other apps when viewing contact details. Custom data from other apps are viewable but not editable in order to preserve and respect the rules surrounding those custom data managed by other apps. This library allows you to read (query) and write (insert, update, delete) custom data from other apps. It is up to you whether you want to follow the same limitations imposed by the AOSP and Google Contacts app. In order to show your custom data in the AOSP Contacts app and Google Contacts app (and other Contacts app that support this feature), you must add an xml file in your app; res/xml/contacts.xml . The res/xml/contacts.xml template looks like this, The full official documentation for each of those tags and attributes within each tag are available by clicking this link . For example, the bare-minimum contacts.xml for showing Gender and HandleName custom data in the AOSP and Google Contacts app is the following, A few things to note, The value of android:mimeType corresponds to the String value defined in GenderMimeType and HandleNameMimeType as seen in the previous sections of this guide. The value of android:summaryColumn and android:detailColumn corresponds to the values defined in contacts.core.Fields.kt#AbstractCustomDataField.ColumnName that are used by GenderFields and HandleNameFields . These values, as raw strings, are; data1 , data2 , data3 ,... data15 Again, in order for your custom data to be shown in the Contacts app, you must also provide a sync adapter implementation. For more info, read Sync contact data across devices .","title":"Displaying your custom data in other Contacts apps"},{"location":"customdata/integrate-custom-data/#summary-of-limitations","text":"To reiterate, this library does not provide a remote server or sync adapters to interface with that server. This library provides create (insert), read (query), update, and delete (CRUD) APIs for pretty, type-safe, and well-documented read and write operations on all data kinds, including custom data. This means that if you do not implement your own sync adapter for your custom data, then your custom data... will NOT be synced across devices will NOT be shown in AOSP and Google Contacts apps, and other Contacts apps that show custom data from other apps You may still do creative things with custom data without sync adapters as long as you understand these limitations. This library provides CRUD API integration with custom data with no sync adapters; customdata-gender customdata-handlename customdata-pokemon customdata-rpg Also provided are CRUD API integration with custom data from other apps that do have sync adapters; customdata-googlecontacts \u2139\ufe0f Please update the above list whenever adding new custom data modules.","title":"Summary of limitations"},{"location":"customdata/integrate-gender-custom-data/","text":"Integrate the gender custom data \u00b6 This library provides extensions for Gender custom data that allows you to read and write gender data for all of your contacts. These (optional) extensions live in the customdata-gender module. \u2139\ufe0f If you are looking to create your own custom data or get more insight on how the Gender custom data was built, read Integrate custom data . Register the gender custom data with the Contacts API instance \u00b6 You may register the Gender custom data when creating the Contacts API instance, val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( GenderRegistration () ) ) Or, alternatively after creating the Contacts API instance, val contactsApi = Contacts ( context ) GenderRegistration (). registerTo ( contactsApi . customDataRegistry ) Get/set gender custom data \u00b6 Just like regular data kinds, gender custom data belong to a RawContact. A RawContact may only have 0 or 1 gender. To get the gender of a RawContact, val gender = rawContact . gender ( contactsApi ) To get the genders of all RawContacts belonging to a Contact, val genderSequence = contact . genders ( contactsApi ) val genderList = contact . genderList ( contactsApi ) To set the gender of a (mutable) RawContact, mutableRawContact . setGender ( contacts , mutableGender ) // or mutableRawContact . setGender ( contacts ) { type = GenderEntity . Type . MALE } To set the gender of the first RawContact in a Contact, mutableContact . setGender ( contacts , mutableGender ) // or mutableContact . setGender ( contacts ) { type = GenderEntity . Type . MALE } Use the gender custom data in queries, inserts, updates, and deletes \u00b6 Once you have registered your gender custom data with the Contacts API instance, the API instance is now able to perform read and write operations on it. Query custom data Insert custom data into new or existing contacts Update custom data Delete custom data Syncing gender custom data \u00b6 This library does not provide sync adapters for gender custom data. Unless you implement your own sync adapter, gender custom data... will NOT be synced across devices will NOT be shown in AOSP and Google Contacts apps, and other Contacts apps that show custom data from other apps For more info, read Sync contact data across devices .","title":"Integrate the Gender custom data"},{"location":"customdata/integrate-gender-custom-data/#integrate-the-gender-custom-data","text":"This library provides extensions for Gender custom data that allows you to read and write gender data for all of your contacts. These (optional) extensions live in the customdata-gender module. \u2139\ufe0f If you are looking to create your own custom data or get more insight on how the Gender custom data was built, read Integrate custom data .","title":"Integrate the gender custom data"},{"location":"customdata/integrate-gender-custom-data/#register-the-gender-custom-data-with-the-contacts-api-instance","text":"You may register the Gender custom data when creating the Contacts API instance, val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( GenderRegistration () ) ) Or, alternatively after creating the Contacts API instance, val contactsApi = Contacts ( context ) GenderRegistration (). registerTo ( contactsApi . customDataRegistry )","title":"Register the gender custom data with the Contacts API instance"},{"location":"customdata/integrate-gender-custom-data/#getset-gender-custom-data","text":"Just like regular data kinds, gender custom data belong to a RawContact. A RawContact may only have 0 or 1 gender. To get the gender of a RawContact, val gender = rawContact . gender ( contactsApi ) To get the genders of all RawContacts belonging to a Contact, val genderSequence = contact . genders ( contactsApi ) val genderList = contact . genderList ( contactsApi ) To set the gender of a (mutable) RawContact, mutableRawContact . setGender ( contacts , mutableGender ) // or mutableRawContact . setGender ( contacts ) { type = GenderEntity . Type . MALE } To set the gender of the first RawContact in a Contact, mutableContact . setGender ( contacts , mutableGender ) // or mutableContact . setGender ( contacts ) { type = GenderEntity . Type . MALE }","title":"Get/set gender custom data"},{"location":"customdata/integrate-gender-custom-data/#use-the-gender-custom-data-in-queries-inserts-updates-and-deletes","text":"Once you have registered your gender custom data with the Contacts API instance, the API instance is now able to perform read and write operations on it. Query custom data Insert custom data into new or existing contacts Update custom data Delete custom data","title":"Use the gender custom data in queries, inserts, updates, and deletes"},{"location":"customdata/integrate-gender-custom-data/#syncing-gender-custom-data","text":"This library does not provide sync adapters for gender custom data. Unless you implement your own sync adapter, gender custom data... will NOT be synced across devices will NOT be shown in AOSP and Google Contacts apps, and other Contacts apps that show custom data from other apps For more info, read Sync contact data across devices .","title":"Syncing gender custom data"},{"location":"customdata/integrate-googlecontacts-custom-data/","text":"Integrate the Google Contacts custom data \u00b6 This library provides extensions for custom data from the Google Contacts app; FileAs and UserDefined , which allows you to read and write Google Contacts data for all of your contacts. These (optional) extensions live in the customdata-googlecontacts module. \u2139\ufe0f If you are looking to create your own custom data or get more insight on how the FileAs and UserDefined custom data was built, read Integrate custom data . Register the Google Contacts custom data with the Contacts API instance \u00b6 You may register all Google Contacts custom data when creating the Contacts API instance, val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( GoogleContactsRegistration () ) ) Or, alternatively after creating the Contacts API instance, val contactsApi = Contacts ( context ) GoogleContactsRegistration (). registerTo ( contactsApi . customDataRegistry ) Read/write Google Contacts custom data \u00b6 Get/set FileAs \u00b6 Just like regular data kinds, FileAs custom data belong to a RawContact. A RawContact may only have 0 or 1 FileAs . To get the FileAs of a RawContact, val fileAs = rawContact . fileAs ( contactsApi ) To get the FileAs of all RawContacts belonging to a Contact, val fileAsSequence = contact . fileAs ( contactsApi ) val fileAsList = contact . fileAsList ( contactsApi ) To set the FileAs of a (mutable) RawContact, mutableRawContact . setFileAs ( contacts , mutableFileAs ) // or mutableRawContact . setFileAs ( contacts ) { name = \"Robot\" } To set the FileAs of the first RawContact in a Contact, mutableContact . setFileAs ( contacts , mutableFileAs ) // or mutableContact . setFileAs ( contacts ) { name = \"Robot\" } Get/add/remove UserDefined \u00b6 Just like regular data kinds, UserDefined custom data belong to a RawContact. A RawContact may have 0, 1, or more UserDefined . To get the UserDefined list/sequence of a RawContact, val userDefinedSequence = rawContact . userDefined ( contactsApi ) val userDefinedList = rawContact . userDefinedList ( contactsApi ) To get the UserDefined of all RawContacts belonging to a Contact, val userDefinedSequence = contact . userDefined ( contactsApi ) val userDefinedList = contact . userDefinedList ( contactsApi ) To add a UserDefined to a (mutable) RawContact, mutableRawContact . addUserDefined ( contacts , mutableUserDefined ) // or mutableRawContact . addUserDefined ( contacts ) { field = \"My Field\" label = \"My Label\" } To add a UserDefined to the first RawContact in a Contact, mutableContact . addUserDefined ( contacts , mutableUserDefined ) // or mutableContact . addUserDefined ( contacts ) { field = \"My Field\" label = \"My Label\" } Use the Google Contacts custom data in queries, inserts, updates, and deletes \u00b6 Once you have registered the Google Contacts custom data with the Contacts API instance, the API instance is now able to perform read and write operations on it. Query custom data Insert custom data into new or existing contacts Update custom data Delete custom data Google Contacts app data integrity \u00b6 When inserting or updating a UserDefined data kind, the Google Contacts app enforces UserDefined.field and UserDefined.label to both be non-null and non-blank. Otherwise, the insert or update operation fails. To protect the data integrity that the Google Contacts app imposes, this library is silently not performing insert or update operations for these instances. Consumers are informed via documentation. Both field and label must be non-null and non-blank strings in order for insert and update operations to be performed on them. The corresponding fields must also be included in the insert or update operation. Otherwise, the update and insert operation will silently NOT be performed. We might change the way we handle this in the future. Maybe we'll throw an exception instead or fail the entire insert/update and bubble up the reason. For now, to avoid complicating the API in these early stages, we'll go with silent but documented. We'll see what the community thinks! Google Contacts app UI \u00b6 In the Google Contacts app , the FileAs and UserDefined custom data are only shown for RawContacts that are associated with a Google Account. Local (device-only) RawContacts do not have these custom data! \u2139\ufe0f For more info on local contacts, read about Local (device-only) contacts . Syncing Google Contacts custom data \u00b6 The Google Contacts app comes with sync adapters that is responsible for syncing FileAs and UserDefined custom data. As long as you have the Google Contacts app installed, these custom data should remain synced depending on account sync settings. \u2139\ufe0f This library does not provide sync adapters for Google Contacts custom data. For more info, read Sync contact data across devices .","title":"Integrate the Google Contacts custom data"},{"location":"customdata/integrate-googlecontacts-custom-data/#integrate-the-google-contacts-custom-data","text":"This library provides extensions for custom data from the Google Contacts app; FileAs and UserDefined , which allows you to read and write Google Contacts data for all of your contacts. These (optional) extensions live in the customdata-googlecontacts module. \u2139\ufe0f If you are looking to create your own custom data or get more insight on how the FileAs and UserDefined custom data was built, read Integrate custom data .","title":"Integrate the Google Contacts custom data"},{"location":"customdata/integrate-googlecontacts-custom-data/#register-the-google-contacts-custom-data-with-the-contacts-api-instance","text":"You may register all Google Contacts custom data when creating the Contacts API instance, val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( GoogleContactsRegistration () ) ) Or, alternatively after creating the Contacts API instance, val contactsApi = Contacts ( context ) GoogleContactsRegistration (). registerTo ( contactsApi . customDataRegistry )","title":"Register the Google Contacts custom data with the Contacts API instance"},{"location":"customdata/integrate-googlecontacts-custom-data/#readwrite-google-contacts-custom-data","text":"","title":"Read/write Google Contacts custom data"},{"location":"customdata/integrate-googlecontacts-custom-data/#getset-fileas","text":"Just like regular data kinds, FileAs custom data belong to a RawContact. A RawContact may only have 0 or 1 FileAs . To get the FileAs of a RawContact, val fileAs = rawContact . fileAs ( contactsApi ) To get the FileAs of all RawContacts belonging to a Contact, val fileAsSequence = contact . fileAs ( contactsApi ) val fileAsList = contact . fileAsList ( contactsApi ) To set the FileAs of a (mutable) RawContact, mutableRawContact . setFileAs ( contacts , mutableFileAs ) // or mutableRawContact . setFileAs ( contacts ) { name = \"Robot\" } To set the FileAs of the first RawContact in a Contact, mutableContact . setFileAs ( contacts , mutableFileAs ) // or mutableContact . setFileAs ( contacts ) { name = \"Robot\" }","title":"Get/set FileAs"},{"location":"customdata/integrate-googlecontacts-custom-data/#getaddremove-userdefined","text":"Just like regular data kinds, UserDefined custom data belong to a RawContact. A RawContact may have 0, 1, or more UserDefined . To get the UserDefined list/sequence of a RawContact, val userDefinedSequence = rawContact . userDefined ( contactsApi ) val userDefinedList = rawContact . userDefinedList ( contactsApi ) To get the UserDefined of all RawContacts belonging to a Contact, val userDefinedSequence = contact . userDefined ( contactsApi ) val userDefinedList = contact . userDefinedList ( contactsApi ) To add a UserDefined to a (mutable) RawContact, mutableRawContact . addUserDefined ( contacts , mutableUserDefined ) // or mutableRawContact . addUserDefined ( contacts ) { field = \"My Field\" label = \"My Label\" } To add a UserDefined to the first RawContact in a Contact, mutableContact . addUserDefined ( contacts , mutableUserDefined ) // or mutableContact . addUserDefined ( contacts ) { field = \"My Field\" label = \"My Label\" }","title":"Get/add/remove UserDefined"},{"location":"customdata/integrate-googlecontacts-custom-data/#use-the-google-contacts-custom-data-in-queries-inserts-updates-and-deletes","text":"Once you have registered the Google Contacts custom data with the Contacts API instance, the API instance is now able to perform read and write operations on it. Query custom data Insert custom data into new or existing contacts Update custom data Delete custom data","title":"Use the Google Contacts custom data in queries, inserts, updates, and deletes"},{"location":"customdata/integrate-googlecontacts-custom-data/#google-contacts-app-data-integrity","text":"When inserting or updating a UserDefined data kind, the Google Contacts app enforces UserDefined.field and UserDefined.label to both be non-null and non-blank. Otherwise, the insert or update operation fails. To protect the data integrity that the Google Contacts app imposes, this library is silently not performing insert or update operations for these instances. Consumers are informed via documentation. Both field and label must be non-null and non-blank strings in order for insert and update operations to be performed on them. The corresponding fields must also be included in the insert or update operation. Otherwise, the update and insert operation will silently NOT be performed. We might change the way we handle this in the future. Maybe we'll throw an exception instead or fail the entire insert/update and bubble up the reason. For now, to avoid complicating the API in these early stages, we'll go with silent but documented. We'll see what the community thinks!","title":"Google Contacts app data integrity"},{"location":"customdata/integrate-googlecontacts-custom-data/#google-contacts-app-ui","text":"In the Google Contacts app , the FileAs and UserDefined custom data are only shown for RawContacts that are associated with a Google Account. Local (device-only) RawContacts do not have these custom data! \u2139\ufe0f For more info on local contacts, read about Local (device-only) contacts .","title":"Google Contacts app UI"},{"location":"customdata/integrate-googlecontacts-custom-data/#syncing-google-contacts-custom-data","text":"The Google Contacts app comes with sync adapters that is responsible for syncing FileAs and UserDefined custom data. As long as you have the Google Contacts app installed, these custom data should remain synced depending on account sync settings. \u2139\ufe0f This library does not provide sync adapters for Google Contacts custom data. For more info, read Sync contact data across devices .","title":"Syncing Google Contacts custom data"},{"location":"customdata/integrate-handlename-custom-data/","text":"Integrate the handle name custom data \u00b6 This library provides extensions for HandleName custom data that allows you to read and write handle name data for all of your contacts. These (optional) extensions live in the customdata-handlename module. \u2139\ufe0f If you are looking to create your own custom data or get more insight on how the HandleName custom data was built, read Integrate custom data . Register the handle name custom data with the Contacts API instance \u00b6 You may register the HandleName custom data when creating the Contacts API instance, val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( HandleNameRegistration () ) ) Or, alternatively after creating the Contacts API instance, val contactsApi = Contacts ( context ) HandleNameRegistration (). registerTo ( contactsApi . customDataRegistry ) Get/add/remove handle name custom data \u00b6 Just like regular data kinds, handle name custom data belong to a RawContact. A RawContact may have 0, 1, or more handle names. To get the handle names of a RawContact, val handleNameSequence = rawContact . handleNames ( contactsApi ) val handleNameList = rawContact . handleNameList ( contactsApi ) To get the handle names of all RawContacts belonging to a Contact, val handleNameSequence = contact . handleNames ( contactsApi ) val handleNameList = contact . handleNameList ( contactsApi ) To add a handle name to a (mutable) RawContact, mutableRawContact . addHandleName ( contacts , mutableHandleName ) // or mutableRawContact . addHandleName ( contacts ) { handle = \"CoolDude91\" } To add a handle name to a the first RawContact or a Contact, mutableContact . addHandleName ( contacts , mutableHandleName ) // or mutableContact . addHandleName ( contacts ) { handle = \"CoolGal89\" } Use the handle name custom data in queries, inserts, updates, and deletes \u00b6 Once you have registered your handle name custom data with the Contacts API instance, the API instance is now able to perform read and write operations on it. Query custom data Insert custom data into new or existing contacts Update custom data Delete custom data Syncing handle name custom data \u00b6 This library does not provide sync adapters for handle name custom data. Unless you implement your own sync adapter, handle name custom data... will NOT be synced across devices will NOT be shown in AOSP and Google Contacts apps, and other Contacts apps that show custom data from other apps For more info, read Sync contact data across devices .","title":"Integrate the Handle Name custom data"},{"location":"customdata/integrate-handlename-custom-data/#integrate-the-handle-name-custom-data","text":"This library provides extensions for HandleName custom data that allows you to read and write handle name data for all of your contacts. These (optional) extensions live in the customdata-handlename module. \u2139\ufe0f If you are looking to create your own custom data or get more insight on how the HandleName custom data was built, read Integrate custom data .","title":"Integrate the handle name custom data"},{"location":"customdata/integrate-handlename-custom-data/#register-the-handle-name-custom-data-with-the-contacts-api-instance","text":"You may register the HandleName custom data when creating the Contacts API instance, val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( HandleNameRegistration () ) ) Or, alternatively after creating the Contacts API instance, val contactsApi = Contacts ( context ) HandleNameRegistration (). registerTo ( contactsApi . customDataRegistry )","title":"Register the handle name custom data with the Contacts API instance"},{"location":"customdata/integrate-handlename-custom-data/#getaddremove-handle-name-custom-data","text":"Just like regular data kinds, handle name custom data belong to a RawContact. A RawContact may have 0, 1, or more handle names. To get the handle names of a RawContact, val handleNameSequence = rawContact . handleNames ( contactsApi ) val handleNameList = rawContact . handleNameList ( contactsApi ) To get the handle names of all RawContacts belonging to a Contact, val handleNameSequence = contact . handleNames ( contactsApi ) val handleNameList = contact . handleNameList ( contactsApi ) To add a handle name to a (mutable) RawContact, mutableRawContact . addHandleName ( contacts , mutableHandleName ) // or mutableRawContact . addHandleName ( contacts ) { handle = \"CoolDude91\" } To add a handle name to a the first RawContact or a Contact, mutableContact . addHandleName ( contacts , mutableHandleName ) // or mutableContact . addHandleName ( contacts ) { handle = \"CoolGal89\" }","title":"Get/add/remove handle name custom data"},{"location":"customdata/integrate-handlename-custom-data/#use-the-handle-name-custom-data-in-queries-inserts-updates-and-deletes","text":"Once you have registered your handle name custom data with the Contacts API instance, the API instance is now able to perform read and write operations on it. Query custom data Insert custom data into new or existing contacts Update custom data Delete custom data","title":"Use the handle name custom data in queries, inserts, updates, and deletes"},{"location":"customdata/integrate-handlename-custom-data/#syncing-handle-name-custom-data","text":"This library does not provide sync adapters for handle name custom data. Unless you implement your own sync adapter, handle name custom data... will NOT be synced across devices will NOT be shown in AOSP and Google Contacts apps, and other Contacts apps that show custom data from other apps For more info, read Sync contact data across devices .","title":"Syncing handle name custom data"},{"location":"customdata/integrate-pokemon-custom-data/","text":"Integrate the Pokemon custom data \u00b6 This library provides extensions for Pokemon custom data that allows you to read and write pokemon data for all of your contacts. These (optional) extensions live in the customdata-pokemon module. \u2139\ufe0f If you are looking to create your own custom data or get more insight on how the Pokemon custom data was built, read Integrate custom data . Register the pokemon custom data with the Contacts API instance \u00b6 You may register the Pokemon custom data when creating the Contacts API instance, val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( PokemonRegistration () ) ) Or, alternatively after creating the Contacts API instance, val contactsApi = Contacts ( context ) PokemonRegistration (). registerTo ( contactsApi . customDataRegistry ) Get/add/remove pokemon custom data \u00b6 Just like regular data kinds, pokemon custom data belong to a RawContact. A RawContact may have 0, 1, or more pokemons. To get the pokemons of a RawContact, val pokemonSequence = rawContact . pokemons ( contactsApi ) val pokemonList = rawContact . pokemonList ( contactsApi ) To get the pokemons of all RawContacts belonging to a Contact, val pokemonSequence = contact . pokemons ( contactsApi ) val pokemonList = contact . pokemonList ( contactsApi ) To add a pokemon to a (mutable) RawContact, mutableRawContact . addPokemon ( contacts , mutablePokemon ) // or mutableRawContact . addPokemon ( contacts ) { name = \"ditto\" nickname = \"copy-cat\" level = 24 pokeApiId = 132 } To add a pokemon to a the first RawContact or a Contact, mutableContact . addPokemon ( contacts , mutablePokemon ) // or mutableContact . addPokemon ( contacts ) { name = \"ditto\" nickname = \"copy-cat\" level = 24 pokeApiId = 132 } Use the pokemon custom data in queries, inserts, updates, and deletes \u00b6 Once you have registered your pokemon custom data with the Contacts API instance, the API instance is now able to perform read and write operations on it. Query custom data Insert custom data into new or existing contacts Update custom data Delete custom data Syncing pokemon custom data \u00b6 This library does not provide sync adapters for pokemon custom data. Unless you implement your own sync adapter, pokemon custom data... will NOT be synced across devices will NOT be shown in AOSP and Google Contacts apps, and other Contacts apps that show custom data from other apps For more info, read Sync contact data across devices .","title":"Integrate the Pokemon custom data"},{"location":"customdata/integrate-pokemon-custom-data/#integrate-the-pokemon-custom-data","text":"This library provides extensions for Pokemon custom data that allows you to read and write pokemon data for all of your contacts. These (optional) extensions live in the customdata-pokemon module. \u2139\ufe0f If you are looking to create your own custom data or get more insight on how the Pokemon custom data was built, read Integrate custom data .","title":"Integrate the Pokemon custom data"},{"location":"customdata/integrate-pokemon-custom-data/#register-the-pokemon-custom-data-with-the-contacts-api-instance","text":"You may register the Pokemon custom data when creating the Contacts API instance, val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( PokemonRegistration () ) ) Or, alternatively after creating the Contacts API instance, val contactsApi = Contacts ( context ) PokemonRegistration (). registerTo ( contactsApi . customDataRegistry )","title":"Register the pokemon custom data with the Contacts API instance"},{"location":"customdata/integrate-pokemon-custom-data/#getaddremove-pokemon-custom-data","text":"Just like regular data kinds, pokemon custom data belong to a RawContact. A RawContact may have 0, 1, or more pokemons. To get the pokemons of a RawContact, val pokemonSequence = rawContact . pokemons ( contactsApi ) val pokemonList = rawContact . pokemonList ( contactsApi ) To get the pokemons of all RawContacts belonging to a Contact, val pokemonSequence = contact . pokemons ( contactsApi ) val pokemonList = contact . pokemonList ( contactsApi ) To add a pokemon to a (mutable) RawContact, mutableRawContact . addPokemon ( contacts , mutablePokemon ) // or mutableRawContact . addPokemon ( contacts ) { name = \"ditto\" nickname = \"copy-cat\" level = 24 pokeApiId = 132 } To add a pokemon to a the first RawContact or a Contact, mutableContact . addPokemon ( contacts , mutablePokemon ) // or mutableContact . addPokemon ( contacts ) { name = \"ditto\" nickname = \"copy-cat\" level = 24 pokeApiId = 132 }","title":"Get/add/remove pokemon custom data"},{"location":"customdata/integrate-pokemon-custom-data/#use-the-pokemon-custom-data-in-queries-inserts-updates-and-deletes","text":"Once you have registered your pokemon custom data with the Contacts API instance, the API instance is now able to perform read and write operations on it. Query custom data Insert custom data into new or existing contacts Update custom data Delete custom data","title":"Use the pokemon custom data in queries, inserts, updates, and deletes"},{"location":"customdata/integrate-pokemon-custom-data/#syncing-pokemon-custom-data","text":"This library does not provide sync adapters for pokemon custom data. Unless you implement your own sync adapter, pokemon custom data... will NOT be synced across devices will NOT be shown in AOSP and Google Contacts apps, and other Contacts apps that show custom data from other apps For more info, read Sync contact data across devices .","title":"Syncing pokemon custom data"},{"location":"customdata/integrate-rpg-custom-data/","text":"Integrate the Role Playing Game (RPG) custom data \u00b6 This provides extensions for RpgStats and RpgProfession custom data that allows you to read and write rpg data for all of your contacts. These (optional) extensions live in the customdata-rpg module. \u2139\ufe0f If you are looking to create your own custom data or get more insight on how the RpgStats and RpgProfession custom data was built, read Integrate custom data . Register the RPG custom data with the Contacts API instance \u00b6 You may register all RPG custom data when creating the Contacts API instance, val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( RpgRegistration () ) ) Or, alternatively after creating the Contacts API instance, val contactsApi = Contacts ( context ) RpgRegistration (). registerTo ( contactsApi . customDataRegistry ) Read/write RPG custom data \u00b6 Get/set RpgStats \u00b6 Just like regular data kinds, RpgStats custom data belong to a RawContact. A RawContact may only have 0 or 1 RpgStats . To get the RpgStats of a RawContact, val rpgStats = rawContact . rpgStats ( contactsApi ) To get the RpgStats of all RawContacts belonging to a Contact, val rpgStatsSequence = contact . rpgStats ( contactsApi ) val rpgStatsList = contact . rpgStatsList ( contactsApi ) To set the RpgStats of a (mutable) RawContact, mutableRawContact . setRpgStats ( contacts , mutableRpgStats ) // or mutableRawContact . setRpgStats ( contacts ) { level = 78 speed = 500 strength = 789 intelligence = 123 luck = 999 } To set the RpgStats of the first RawContact in a Contact, mutableContact . setRpgStats ( contacts , mutableRpgStats ) // or mutableContact . setRpgStats ( contacts ) { level = 78 speed = 500 strength = 789 intelligence = 123 luck = 999 } Get/set RpgProfession \u00b6 Just like regular data kinds, RpgProfession custom data belong to a RawContact. A RawContact may only have 0 or 1 RpgProfession . To get the RpgProfession of a RawContact, val rpgProfession = rawContact . rpgProfession ( contactsApi ) To get the RpgProfession of all RawContacts belonging to a Contact, val rpgProfessionSequence = contact . rpgProfessions ( contactsApi ) val rpgProfessionList = contact . rpgProfessionList ( contactsApi ) To set the RpgProfession of a (mutable) RawContact, mutableRawContact . setRpgProfession ( contacts , mutableRpgProfession ) // or mutableRawContact . setRpgProfession ( contacts ) { title = \"swordsman\" } To set the RpgProfession of the first RawContact in a Contact, mutableContact . setRpgProfession ( contacts , mutableRpgProfession ) // or mutableContact . setRpgProfession ( contacts ) { title = \"swordsman\" } Use the RPG custom data in queries, inserts, updates, and deletes \u00b6 Once you have registered the RPG custom data with the Contacts API instance, the API instance is now able to perform read and write operations on it. Query custom data Insert custom data into new or existing contacts Update custom data Delete custom data Syncing RPG custom data \u00b6 This library does not provide sync adapters for RPG custom data. Unless you implement your own sync adapter, RPG custom data... will NOT be synced across devices will NOT be shown in AOSP and Google Contacts apps, and other Contacts apps that show custom data from other apps For more info, read Sync contact data across devices .","title":"Integrate the RPG custom data"},{"location":"customdata/integrate-rpg-custom-data/#integrate-the-role-playing-game-rpg-custom-data","text":"This provides extensions for RpgStats and RpgProfession custom data that allows you to read and write rpg data for all of your contacts. These (optional) extensions live in the customdata-rpg module. \u2139\ufe0f If you are looking to create your own custom data or get more insight on how the RpgStats and RpgProfession custom data was built, read Integrate custom data .","title":"Integrate the Role Playing Game (RPG) custom data"},{"location":"customdata/integrate-rpg-custom-data/#register-the-rpg-custom-data-with-the-contacts-api-instance","text":"You may register all RPG custom data when creating the Contacts API instance, val contactsApi = Contacts ( context , customDataRegistry = CustomDataRegistry (). register ( RpgRegistration () ) ) Or, alternatively after creating the Contacts API instance, val contactsApi = Contacts ( context ) RpgRegistration (). registerTo ( contactsApi . customDataRegistry )","title":"Register the RPG custom data with the Contacts API instance"},{"location":"customdata/integrate-rpg-custom-data/#readwrite-rpg-custom-data","text":"","title":"Read/write RPG custom data"},{"location":"customdata/integrate-rpg-custom-data/#getset-rpgstats","text":"Just like regular data kinds, RpgStats custom data belong to a RawContact. A RawContact may only have 0 or 1 RpgStats . To get the RpgStats of a RawContact, val rpgStats = rawContact . rpgStats ( contactsApi ) To get the RpgStats of all RawContacts belonging to a Contact, val rpgStatsSequence = contact . rpgStats ( contactsApi ) val rpgStatsList = contact . rpgStatsList ( contactsApi ) To set the RpgStats of a (mutable) RawContact, mutableRawContact . setRpgStats ( contacts , mutableRpgStats ) // or mutableRawContact . setRpgStats ( contacts ) { level = 78 speed = 500 strength = 789 intelligence = 123 luck = 999 } To set the RpgStats of the first RawContact in a Contact, mutableContact . setRpgStats ( contacts , mutableRpgStats ) // or mutableContact . setRpgStats ( contacts ) { level = 78 speed = 500 strength = 789 intelligence = 123 luck = 999 }","title":"Get/set RpgStats"},{"location":"customdata/integrate-rpg-custom-data/#getset-rpgprofession","text":"Just like regular data kinds, RpgProfession custom data belong to a RawContact. A RawContact may only have 0 or 1 RpgProfession . To get the RpgProfession of a RawContact, val rpgProfession = rawContact . rpgProfession ( contactsApi ) To get the RpgProfession of all RawContacts belonging to a Contact, val rpgProfessionSequence = contact . rpgProfessions ( contactsApi ) val rpgProfessionList = contact . rpgProfessionList ( contactsApi ) To set the RpgProfession of a (mutable) RawContact, mutableRawContact . setRpgProfession ( contacts , mutableRpgProfession ) // or mutableRawContact . setRpgProfession ( contacts ) { title = \"swordsman\" } To set the RpgProfession of the first RawContact in a Contact, mutableContact . setRpgProfession ( contacts , mutableRpgProfession ) // or mutableContact . setRpgProfession ( contacts ) { title = \"swordsman\" }","title":"Get/set RpgProfession"},{"location":"customdata/integrate-rpg-custom-data/#use-the-rpg-custom-data-in-queries-inserts-updates-and-deletes","text":"Once you have registered the RPG custom data with the Contacts API instance, the API instance is now able to perform read and write operations on it. Query custom data Insert custom data into new or existing contacts Update custom data Delete custom data","title":"Use the RPG custom data in queries, inserts, updates, and deletes"},{"location":"customdata/integrate-rpg-custom-data/#syncing-rpg-custom-data","text":"This library does not provide sync adapters for RPG custom data. Unless you implement your own sync adapter, RPG custom data... will NOT be synced across devices will NOT be shown in AOSP and Google Contacts apps, and other Contacts apps that show custom data from other apps For more info, read Sync contact data across devices .","title":"Syncing RPG custom data"},{"location":"customdata/query-custom-data/","text":"Query custom data \u00b6 This library provides several query APIs that support custom data integration. Query Query contacts (advanced) BroadQuery Query contacts ProfileQuery Query device owner Contact profile DataQuery Query specific data kinds To help illustrate how custom data integrates with these query APIs, we'll use the HandleName and Gender custom data. \u2139\ufe0f For more info, read Integrate the gender custom data and Integrate the handle name custom data . Getting custom data from a Contact or RawContact \u00b6 Custom data, just like regular data kinds, are attached to a RawContact. They follow the same rules as regular data kinds. \u2139\ufe0f For more info, read about API Entities . For example, you are able to get the handle names and gender of a RawContact, val handleNames = rawContact . handleNames ( contactsApi ) val gender = rawContact . gender ( contactsApi ) There are also extensions that allow you to get custom data from a Contact, which can be made up of one or more RawContacts, val handleNames = contact . handleNames ( contactsApi ) val genders = contact . genders ( contactsApi ) Getting specific custom data kinds directly \u00b6 Every custom data provides an extension to the DataQuery that allows you to query for only that specific custom data kind. For example, to get all available HandleName s and Gender s from all contacts, val handleNames = Contacts ( context ). data (). query (). handleNames (). find () val genders = Contacts ( context ). data (). query (). genders (). find () To get all HandleName s starting with the letter \"h\", val handleNames = Contacts ( context ) . data () . query () . handleNames () . where { Handle startsWith \"h\" } . find () For more info, read Query specific data kinds . The include function and custom data \u00b6 All of the above mentioned APIs provide an include function that allows you to include only a given set of fields (data) in each of the returned entities. Custom data entries provides fields that can be used in this function. By default, not calling the include function will include all fields, including custom data fields. For example, to explicitly include all HandleName fields, . include ( HandleNameFields . all ) For more info, read Include only certain fields for read and write operations . The where function and custom data \u00b6 The Query and DataQuery APIs provides a where function that allows you to specify a matching criteria based on specific field values. Custom data entries provides fields that can be used in this function. For example, to match HandleName s starting with the letter \"h\", . where { Handle startsWith \"h\" } The BroadQuery API provides a whereAnyContactDataPartiallyMatches function that NOT support matching custom data. Only native data are included in the matching process. The ProfileQuery API does not provide a where function as there can only be one profile Contact per device. The orderBy function and custom data \u00b6 The DataQuery API provides an orderBy function that supports custom data. For example, to order HandleName s, . orderBy ( HandleNameFields . Handle . asc ()) The Query and BroadQuery APIs provides an orderBy function that only takes in fields from the Contacts table, not data. So there is no custom data, or native data, support for this. The ProfileQuery API does not provide an orderBy function as there can only be at most one profile Contact on the device.","title":"Query custom data"},{"location":"customdata/query-custom-data/#query-custom-data","text":"This library provides several query APIs that support custom data integration. Query Query contacts (advanced) BroadQuery Query contacts ProfileQuery Query device owner Contact profile DataQuery Query specific data kinds To help illustrate how custom data integrates with these query APIs, we'll use the HandleName and Gender custom data. \u2139\ufe0f For more info, read Integrate the gender custom data and Integrate the handle name custom data .","title":"Query custom data"},{"location":"customdata/query-custom-data/#getting-custom-data-from-a-contact-or-rawcontact","text":"Custom data, just like regular data kinds, are attached to a RawContact. They follow the same rules as regular data kinds. \u2139\ufe0f For more info, read about API Entities . For example, you are able to get the handle names and gender of a RawContact, val handleNames = rawContact . handleNames ( contactsApi ) val gender = rawContact . gender ( contactsApi ) There are also extensions that allow you to get custom data from a Contact, which can be made up of one or more RawContacts, val handleNames = contact . handleNames ( contactsApi ) val genders = contact . genders ( contactsApi )","title":"Getting custom data from a Contact or RawContact"},{"location":"customdata/query-custom-data/#getting-specific-custom-data-kinds-directly","text":"Every custom data provides an extension to the DataQuery that allows you to query for only that specific custom data kind. For example, to get all available HandleName s and Gender s from all contacts, val handleNames = Contacts ( context ). data (). query (). handleNames (). find () val genders = Contacts ( context ). data (). query (). genders (). find () To get all HandleName s starting with the letter \"h\", val handleNames = Contacts ( context ) . data () . query () . handleNames () . where { Handle startsWith \"h\" } . find () For more info, read Query specific data kinds .","title":"Getting specific custom data kinds directly"},{"location":"customdata/query-custom-data/#the-include-function-and-custom-data","text":"All of the above mentioned APIs provide an include function that allows you to include only a given set of fields (data) in each of the returned entities. Custom data entries provides fields that can be used in this function. By default, not calling the include function will include all fields, including custom data fields. For example, to explicitly include all HandleName fields, . include ( HandleNameFields . all ) For more info, read Include only certain fields for read and write operations .","title":"The include function and custom data"},{"location":"customdata/query-custom-data/#the-where-function-and-custom-data","text":"The Query and DataQuery APIs provides a where function that allows you to specify a matching criteria based on specific field values. Custom data entries provides fields that can be used in this function. For example, to match HandleName s starting with the letter \"h\", . where { Handle startsWith \"h\" } The BroadQuery API provides a whereAnyContactDataPartiallyMatches function that NOT support matching custom data. Only native data are included in the matching process. The ProfileQuery API does not provide a where function as there can only be one profile Contact per device.","title":"The where function and custom data"},{"location":"customdata/query-custom-data/#the-orderby-function-and-custom-data","text":"The DataQuery API provides an orderBy function that supports custom data. For example, to order HandleName s, . orderBy ( HandleNameFields . Handle . asc ()) The Query and BroadQuery APIs provides an orderBy function that only takes in fields from the Contacts table, not data. So there is no custom data, or native data, support for this. The ProfileQuery API does not provide an orderBy function as there can only be at most one profile Contact on the device.","title":"The orderBy function and custom data"},{"location":"customdata/update-custom-data/","text":"Update custom data \u00b6 This library provides several update APIs that support custom data integration. Update Update contacts ProfileUpdate Update device owner Contact profile DataUpdate Update existing sets of data To help illustrate how custom data integrates with these update APIs, we'll use the HandleName and Gender custom data. \u2139\ufe0f For more info about custom data, read Integrate custom data . Updating custom data via Contacts/RawContacts \u00b6 Custom data, just like regular data kinds, are attached to a RawContact. They follow the same rules as regular data kinds. \u2139\ufe0f For more info, read about API Entities . For example, you are able to update existing handle names and the gender of an existing RawContact, mutableRawContact . handleNames ( contactsApi ). firstOrNull () ?. apply { handle = \"gal91\" } mutableRawContact . gender ( contactsApi ) ?. apply { type = GenderEntity . Type . FEMALE } There are also extensions that allow you to update custom data of an existing RawContact via a Contact, which can be made up of one or more RawContacts, mutableContact . handleNames ( contactsApi ). firstOrNull () ?. apply { handle = \"gal91\" } mutableContact . genders ( contactsApi ). firstOrNull () ?. apply { type = GenderEntity . Type . FEMALE } Once you have made the updates to existing custom data, you can perform the update operation on the RawContact to commit your changes into the database using Update or ProfileUpdate . Updating sets of custom data directly \u00b6 All custom data are compatible with the DataUpdate API, which allows you to update sets of existing regular and custom data kinds. For example, to update a set of HandleName s and Gender s, val handleNames : List < MutableHandleName > val genders : List < MutableGender > val updateResult = Contacts ( this ) . data () . update () . data ( handleNames + genders ) . commit () For more info, read Update existing sets of data . The include function and custom data \u00b6 All of the above mentioned APIs provide an include function that allows you to include only a given set of fields (data) to be processed in the update operation. Custom data entries provides fields that can be used in this function. By default, not calling the include function will include all fields, including custom data fields. For example, to specifically include only HandleName and Gender fields, . include ( HandleNameFields . all + GenderFields . all ) For more info, read Include only certain fields for read and write operations . Blank data are deleted \u00b6 Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are deleted by update APIs. For more info, read about Blank data .","title":"Update custom data"},{"location":"customdata/update-custom-data/#update-custom-data","text":"This library provides several update APIs that support custom data integration. Update Update contacts ProfileUpdate Update device owner Contact profile DataUpdate Update existing sets of data To help illustrate how custom data integrates with these update APIs, we'll use the HandleName and Gender custom data. \u2139\ufe0f For more info about custom data, read Integrate custom data .","title":"Update custom data"},{"location":"customdata/update-custom-data/#updating-custom-data-via-contactsrawcontacts","text":"Custom data, just like regular data kinds, are attached to a RawContact. They follow the same rules as regular data kinds. \u2139\ufe0f For more info, read about API Entities . For example, you are able to update existing handle names and the gender of an existing RawContact, mutableRawContact . handleNames ( contactsApi ). firstOrNull () ?. apply { handle = \"gal91\" } mutableRawContact . gender ( contactsApi ) ?. apply { type = GenderEntity . Type . FEMALE } There are also extensions that allow you to update custom data of an existing RawContact via a Contact, which can be made up of one or more RawContacts, mutableContact . handleNames ( contactsApi ). firstOrNull () ?. apply { handle = \"gal91\" } mutableContact . genders ( contactsApi ). firstOrNull () ?. apply { type = GenderEntity . Type . FEMALE } Once you have made the updates to existing custom data, you can perform the update operation on the RawContact to commit your changes into the database using Update or ProfileUpdate .","title":"Updating custom data via Contacts/RawContacts"},{"location":"customdata/update-custom-data/#updating-sets-of-custom-data-directly","text":"All custom data are compatible with the DataUpdate API, which allows you to update sets of existing regular and custom data kinds. For example, to update a set of HandleName s and Gender s, val handleNames : List < MutableHandleName > val genders : List < MutableGender > val updateResult = Contacts ( this ) . data () . update () . data ( handleNames + genders ) . commit () For more info, read Update existing sets of data .","title":"Updating sets of custom data directly"},{"location":"customdata/update-custom-data/#the-include-function-and-custom-data","text":"All of the above mentioned APIs provide an include function that allows you to include only a given set of fields (data) to be processed in the update operation. Custom data entries provides fields that can be used in this function. By default, not calling the include function will include all fields, including custom data fields. For example, to specifically include only HandleName and Gender fields, . include ( HandleNameFields . all + GenderFields . all ) For more info, read Include only certain fields for read and write operations .","title":"The include function and custom data"},{"location":"customdata/update-custom-data/#blank-data-are-deleted","text":"Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are deleted by update APIs. For more info, read about Blank data .","title":"Blank data are deleted"},{"location":"data/delete-data-sets/","text":"Delete existing sets of data \u00b6 This library provides the DataDelete API that allows you to delete a list of any data kinds directly without having to delete them via Contacts/RawContacts. An instance of the DataDelete API is obtained by, val delete = Contacts ( context ). data (). delete () \u2139\ufe0f To delete all kinds of data via Contacts/RawContacts, you may remove them from the Contact/RawContact and then perform an update. For more info, read Update contacts . A basic delete \u00b6 To delete a set of data, val deleteResult = Contacts ( context ) . data () . delete () . data ( data ) . commit () If you want to delete a list of emails and phones, val deleteResult = Contacts ( context ) . data () . delete () . data ( emails + phones ) . commit () Executing the delete \u00b6 To execute the delete, . commit () If you want to delete all given data in a single atomic transaction, . commitInOneTransaction () The call to commitInOneTransaction will only succeed if ALL given data are successfully deleted. If one delete fails, the entire operation will fail and everything will be reverted prior to the delete operation. In contrast, commit allows for some deletes to succeed and some to fail. Handling the delete result \u00b6 The commit and commitInOneTransaction functions returns a Result , To check if all deletes succeeded, val allDeletesSuccessful = deleteResult . isSuccessful To check if a particular delete succeeded, val firstDeleteSuccessful = deleteResult . isSuccessful ( data1 ) Performing the delete and result processing asynchronously \u00b6 Deletes are executed when the commit or commitInOneTransaction function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the delete with permission \u00b6 Deletes require the android.permission.WRITE_CONTACTS permissions. If not granted, the delete will do nothing and return a failed result. To perform the delete with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Profile data \u00b6 The DataDelete API also supports deleting the Profile (device owner) contact data. To get an instance of this API for Profile data deletes, val profileDataDelete = Contacts ( context ). profile (). data (). delete () All deletes will be limited to the Profile, whether it exists or not. Custom data support \u00b6 The DataDelete API supports custom data. For more info, read Delete custom data .","title":"Delete existing sets of data"},{"location":"data/delete-data-sets/#delete-existing-sets-of-data","text":"This library provides the DataDelete API that allows you to delete a list of any data kinds directly without having to delete them via Contacts/RawContacts. An instance of the DataDelete API is obtained by, val delete = Contacts ( context ). data (). delete () \u2139\ufe0f To delete all kinds of data via Contacts/RawContacts, you may remove them from the Contact/RawContact and then perform an update. For more info, read Update contacts .","title":"Delete existing sets of data"},{"location":"data/delete-data-sets/#a-basic-delete","text":"To delete a set of data, val deleteResult = Contacts ( context ) . data () . delete () . data ( data ) . commit () If you want to delete a list of emails and phones, val deleteResult = Contacts ( context ) . data () . delete () . data ( emails + phones ) . commit ()","title":"A basic delete"},{"location":"data/delete-data-sets/#executing-the-delete","text":"To execute the delete, . commit () If you want to delete all given data in a single atomic transaction, . commitInOneTransaction () The call to commitInOneTransaction will only succeed if ALL given data are successfully deleted. If one delete fails, the entire operation will fail and everything will be reverted prior to the delete operation. In contrast, commit allows for some deletes to succeed and some to fail.","title":"Executing the delete"},{"location":"data/delete-data-sets/#handling-the-delete-result","text":"The commit and commitInOneTransaction functions returns a Result , To check if all deletes succeeded, val allDeletesSuccessful = deleteResult . isSuccessful To check if a particular delete succeeded, val firstDeleteSuccessful = deleteResult . isSuccessful ( data1 )","title":"Handling the delete result"},{"location":"data/delete-data-sets/#performing-the-delete-and-result-processing-asynchronously","text":"Deletes are executed when the commit or commitInOneTransaction function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the delete and result processing asynchronously"},{"location":"data/delete-data-sets/#performing-the-delete-with-permission","text":"Deletes require the android.permission.WRITE_CONTACTS permissions. If not granted, the delete will do nothing and return a failed result. To perform the delete with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the delete with permission"},{"location":"data/delete-data-sets/#profile-data","text":"The DataDelete API also supports deleting the Profile (device owner) contact data. To get an instance of this API for Profile data deletes, val profileDataDelete = Contacts ( context ). profile (). data (). delete () All deletes will be limited to the Profile, whether it exists or not.","title":"Profile data"},{"location":"data/delete-data-sets/#custom-data-support","text":"The DataDelete API supports custom data. For more info, read Delete custom data .","title":"Custom data support"},{"location":"data/insert-data-sets/","text":"Insert data into new or existing contacts \u00b6 Data can only be created/inserted into the database whenever inserting or updating new or existing contacts. When using insert and update APIs such as Insert , ProfileInsert , Update , and ProfileUpdate , you are able to create/insert data into new or existing RawContacts respectively. For example, to insert an email into a new contact using the Insert API, Contacts ( context ) . insert () . rawContact { addEmail ( email ) } . commit () \u2139\ufe0f For more info, read Insert contacts . To insert an email into a new Profile contact using the ProfileInsert API, Contacts ( context ) . profile () . insert () . rawContact { addEmail ( email ) } . commit () \u2139\ufe0f For more info, read Insert device owner Contact profile . To insert an email into an existing contact using the Update API, Contacts ( context ) . update () . contacts ( existingContact . mutableCopy { addEmail ( email ) }) . commit () \u2139\ufe0f For more info, read Update contacts . To insert an email into an the existing Profile Contact using the ProfileUpdate API, Contacts ( context ) . profile () . update () . contact ( existingProfileContact . mutableCopy { addEmail ( email ) }) . commit () \u2139\ufe0f For more info, read Update device owner Contact profile . Blank data are not inserted \u00b6 Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are ignored and are not inserted by insert APIs. For more info, read about Blank data .","title":"Insert data into new or existing contacts"},{"location":"data/insert-data-sets/#insert-data-into-new-or-existing-contacts","text":"Data can only be created/inserted into the database whenever inserting or updating new or existing contacts. When using insert and update APIs such as Insert , ProfileInsert , Update , and ProfileUpdate , you are able to create/insert data into new or existing RawContacts respectively. For example, to insert an email into a new contact using the Insert API, Contacts ( context ) . insert () . rawContact { addEmail ( email ) } . commit () \u2139\ufe0f For more info, read Insert contacts . To insert an email into a new Profile contact using the ProfileInsert API, Contacts ( context ) . profile () . insert () . rawContact { addEmail ( email ) } . commit () \u2139\ufe0f For more info, read Insert device owner Contact profile . To insert an email into an existing contact using the Update API, Contacts ( context ) . update () . contacts ( existingContact . mutableCopy { addEmail ( email ) }) . commit () \u2139\ufe0f For more info, read Update contacts . To insert an email into an the existing Profile Contact using the ProfileUpdate API, Contacts ( context ) . profile () . update () . contact ( existingProfileContact . mutableCopy { addEmail ( email ) }) . commit () \u2139\ufe0f For more info, read Update device owner Contact profile .","title":"Insert data into new or existing contacts"},{"location":"data/insert-data-sets/#blank-data-are-not-inserted","text":"Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are ignored and are not inserted by insert APIs. For more info, read about Blank data .","title":"Blank data are not inserted"},{"location":"data/query-data-sets/","text":"Query specific data kinds \u00b6 This library provides the DataQueryFactory API that allows you to get a list of specific data kinds directly without having to get them from Contacts/RawContacts. An instance of the DataQueryFactory API is obtained by, val query = Contacts ( context ). data (). query () \u2139\ufe0f To retrieve all kinds of data via Contacts/RawContacts, read Query contacts and Query contacts (advanced) . Data queries \u00b6 The DataQueryFactory API provides instances of DataQuery for every data kind in the library. The full list of queries are defined in the DataQueryFactory interface. Here it is for reference, val dataQueryFactory = Contacts ( context ). data (). query () val addressesQuery = dataQueryFactory . addresses () val emailsQuery = dataQueryFactory . emails () val eventsQuery = dataQueryFactory . events () val groupMembershipsQuery = dataQueryFactory . groupMemberships () val imsQuery = dataQueryFactory . ims () val namesQuery = dataQueryFactory . names () val nicknamesQuery = dataQueryFactory . nicknames () val notesQuery = dataQueryFactory . notes () val organizationsQuery = dataQueryFactory . organizations () val phonesQuery = dataQueryFactory . phones () val relationsQuery = dataQueryFactory . relations () val sipAddressesQuery = dataQueryFactory . sipAddresses () val websitesQuery = dataQueryFactory . websites () // Photos are intentionally left out as it is internal to the library. These query instances will allow you to query only specific data kinds from all contacts. For example, to get all emails from all contacts, val emails = Contacts ( context ). data (). query (). emails (). find () To get all websites with a \".net\" extension from contacts with the given IDs, val websites = Contacts ( this ) . data () . query () . websites () . where { ( Website . Url endsWith \".net\" ) and ( Contact . Id `in` contactIds ) } . find () Specifying Accounts \u00b6 To limit the search to only those data associated with one of the given accounts, . accounts ( accounts ) For example, to limit the search to data belonging to only one account, . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) \u2139\ufe0f For more info, read Query for Accounts . If no accounts are specified (this function is not called or called with no Accounts), then all data are included in the search. A null Account may be provided here, which results in RawContacts with no associated Account to be included in the search. RawContacts without an associated account are considered local contacts or device-only contacts, which are not synced. For more info, read about Local (device-only) contacts . \u2139\ufe0f This may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it. Including only specific data \u00b6 To include only the given set of fields in each of the matching data , . include ( fields ) For example, to only include the given name and family name in a names query, . include ( Fields . Name . GivenName , Fields . Name . FamilyName ) For more info, read Include only certain fields for read and write operations . Ordering \u00b6 To order resulting data using one or more fields, . orderBy ( fieldOrder ) For example, to order emails by type first and then email address, . orderBy ( Fields . Email . Type . asc (), Fields . Email . Address . asc ()) String comparisons ignores case by default. Each orderBys provides ignoreCase as an optional parameter. Use the corresponding fields in Fields to construct the orderBys. Limiting and offsetting \u00b6 To limit the amount of data returned and/or offset (skip) a specified number of data, . limit ( limit ) . offset ( offset ) For example, to only get a maximum 20 data, skipping the first 20, . limit ( 20 ) . offset ( 20 ) This is useful for pagination =) \u2139\ufe0f It is recommended to limit the number of data when querying to increase performance and decrease memory cost. Executing the query \u00b6 To execute the query, . find () Cancelling the query \u00b6 To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val data = query . find { ! isActive } } } Performing the query asynchronously \u00b6 Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes. Performing the query with permission \u00b6 Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Profile data \u00b6 The DataQueryFactory API (and its DataQuery instances) also supports querying the Profile (device owner) contact data. To get an instance of this API for Profile data queries, val profileDataQueryFactory = Contacts ( context ). profile (). data (). query () All queries will be limited to the Profile, whether it exists or not. Custom data support \u00b6 The DataQueryFactory API (and its DataQuery instances) supports custom data. For more info, read Query custom data . Using the where function to specify matching criteria \u00b6 Use the corresponding contacts.core.Fields combined with the extensions from contacts.core.Where to form WHERE clauses. \u2139\ufe0f This docs page will not provide a tutorial on database where clauses. It assumes that you know the basics. If you don't know the basics, then search for sqlite where clause . For example, to get all nicknames from all contacts, val nicknames = Contacts ( context ). data (). query (). nicknames (). find () To get all birthday events from all contacts, val birthdayEvents = Contacts ( this ) . data () . query () . events () . where { Event . Type equalTo EventEntity . Type . BIRTHDAY } . find () Limitations \u00b6 This library only provides basic WHERE functions. It does not cover the entirety of SQLite, though the community may add more over time <3 Furthermore, this library is constrained by rules and limitations set by the Contacts Provider and the behavior of the native Contacts app. One such rule/limitation has resulted in this library not providing WHERE functions such as isNull or isNullOrEmpty to prevent making misleading queries. Removing a piece of existing data results in the deletion of the row in the Data table if that row no longer contains any meaningful data. This is the behavior of the native Contacts app. Therefore, querying for null fields is not possible. For example, there may be no Data rows that exist where the email address is null. Thus, a query to search for all emails where the address is null may always return no results.","title":"Query specific data kinds"},{"location":"data/query-data-sets/#query-specific-data-kinds","text":"This library provides the DataQueryFactory API that allows you to get a list of specific data kinds directly without having to get them from Contacts/RawContacts. An instance of the DataQueryFactory API is obtained by, val query = Contacts ( context ). data (). query () \u2139\ufe0f To retrieve all kinds of data via Contacts/RawContacts, read Query contacts and Query contacts (advanced) .","title":"Query specific data kinds"},{"location":"data/query-data-sets/#data-queries","text":"The DataQueryFactory API provides instances of DataQuery for every data kind in the library. The full list of queries are defined in the DataQueryFactory interface. Here it is for reference, val dataQueryFactory = Contacts ( context ). data (). query () val addressesQuery = dataQueryFactory . addresses () val emailsQuery = dataQueryFactory . emails () val eventsQuery = dataQueryFactory . events () val groupMembershipsQuery = dataQueryFactory . groupMemberships () val imsQuery = dataQueryFactory . ims () val namesQuery = dataQueryFactory . names () val nicknamesQuery = dataQueryFactory . nicknames () val notesQuery = dataQueryFactory . notes () val organizationsQuery = dataQueryFactory . organizations () val phonesQuery = dataQueryFactory . phones () val relationsQuery = dataQueryFactory . relations () val sipAddressesQuery = dataQueryFactory . sipAddresses () val websitesQuery = dataQueryFactory . websites () // Photos are intentionally left out as it is internal to the library. These query instances will allow you to query only specific data kinds from all contacts. For example, to get all emails from all contacts, val emails = Contacts ( context ). data (). query (). emails (). find () To get all websites with a \".net\" extension from contacts with the given IDs, val websites = Contacts ( this ) . data () . query () . websites () . where { ( Website . Url endsWith \".net\" ) and ( Contact . Id `in` contactIds ) } . find ()","title":"Data queries"},{"location":"data/query-data-sets/#specifying-accounts","text":"To limit the search to only those data associated with one of the given accounts, . accounts ( accounts ) For example, to limit the search to data belonging to only one account, . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) \u2139\ufe0f For more info, read Query for Accounts . If no accounts are specified (this function is not called or called with no Accounts), then all data are included in the search. A null Account may be provided here, which results in RawContacts with no associated Account to be included in the search. RawContacts without an associated account are considered local contacts or device-only contacts, which are not synced. For more info, read about Local (device-only) contacts . \u2139\ufe0f This may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it.","title":"Specifying Accounts"},{"location":"data/query-data-sets/#including-only-specific-data","text":"To include only the given set of fields in each of the matching data , . include ( fields ) For example, to only include the given name and family name in a names query, . include ( Fields . Name . GivenName , Fields . Name . FamilyName ) For more info, read Include only certain fields for read and write operations .","title":"Including only specific data"},{"location":"data/query-data-sets/#ordering","text":"To order resulting data using one or more fields, . orderBy ( fieldOrder ) For example, to order emails by type first and then email address, . orderBy ( Fields . Email . Type . asc (), Fields . Email . Address . asc ()) String comparisons ignores case by default. Each orderBys provides ignoreCase as an optional parameter. Use the corresponding fields in Fields to construct the orderBys.","title":"Ordering"},{"location":"data/query-data-sets/#limiting-and-offsetting","text":"To limit the amount of data returned and/or offset (skip) a specified number of data, . limit ( limit ) . offset ( offset ) For example, to only get a maximum 20 data, skipping the first 20, . limit ( 20 ) . offset ( 20 ) This is useful for pagination =) \u2139\ufe0f It is recommended to limit the number of data when querying to increase performance and decrease memory cost.","title":"Limiting and offsetting"},{"location":"data/query-data-sets/#executing-the-query","text":"To execute the query, . find ()","title":"Executing the query"},{"location":"data/query-data-sets/#cancelling-the-query","text":"To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val data = query . find { ! isActive } } }","title":"Cancelling the query"},{"location":"data/query-data-sets/#performing-the-query-asynchronously","text":"Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes.","title":"Performing the query asynchronously"},{"location":"data/query-data-sets/#performing-the-query-with-permission","text":"Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the query with permission"},{"location":"data/query-data-sets/#profile-data","text":"The DataQueryFactory API (and its DataQuery instances) also supports querying the Profile (device owner) contact data. To get an instance of this API for Profile data queries, val profileDataQueryFactory = Contacts ( context ). profile (). data (). query () All queries will be limited to the Profile, whether it exists or not.","title":"Profile data"},{"location":"data/query-data-sets/#custom-data-support","text":"The DataQueryFactory API (and its DataQuery instances) supports custom data. For more info, read Query custom data .","title":"Custom data support"},{"location":"data/query-data-sets/#using-the-where-function-to-specify-matching-criteria","text":"Use the corresponding contacts.core.Fields combined with the extensions from contacts.core.Where to form WHERE clauses. \u2139\ufe0f This docs page will not provide a tutorial on database where clauses. It assumes that you know the basics. If you don't know the basics, then search for sqlite where clause . For example, to get all nicknames from all contacts, val nicknames = Contacts ( context ). data (). query (). nicknames (). find () To get all birthday events from all contacts, val birthdayEvents = Contacts ( this ) . data () . query () . events () . where { Event . Type equalTo EventEntity . Type . BIRTHDAY } . find ()","title":"Using the where function to specify matching criteria"},{"location":"data/query-data-sets/#limitations","text":"This library only provides basic WHERE functions. It does not cover the entirety of SQLite, though the community may add more over time <3 Furthermore, this library is constrained by rules and limitations set by the Contacts Provider and the behavior of the native Contacts app. One such rule/limitation has resulted in this library not providing WHERE functions such as isNull or isNullOrEmpty to prevent making misleading queries. Removing a piece of existing data results in the deletion of the row in the Data table if that row no longer contains any meaningful data. This is the behavior of the native Contacts app. Therefore, querying for null fields is not possible. For example, there may be no Data rows that exist where the email address is null. Thus, a query to search for all emails where the address is null may always return no results.","title":"Limitations"},{"location":"data/update-data-sets/","text":"Update existing sets of data \u00b6 This library provides the DataUpdate API that allows you to update a list of any data kinds in the Contacts Provider database directly without having to update them via Contacts/RawContacts. This ensures that the Contacts Provider database contains the same data you have in memory. An instance of the DataUpdate API is obtained by, val update = Contacts ( context ). data (). update () \u2139\ufe0f To update all kinds of data via Contacts/RawContacts, read Update contacts . A basic update \u00b6 To update a set of data, val updateResult = Contacts ( context ) . data () . update () . data ( data ) . commit () If you want to update a list of mutable emails and phones, val updateResult = Contacts ( context ) . data () . update () . data ( mutableEmails + mutablePhones ) . commit () Blank data are deleted \u00b6 Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are deleted by update APIs, unless the corresponding fields are not included in the operation. For more info, read about Blank data . Including only specific data \u00b6 To perform update operations only the given set of fields (data), . include ( fields ) For example, to perform updates on only email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations . Executing the update \u00b6 To execute the update, . commit () Handling the update result \u00b6 The commit function returns a Result , val contactsApi = Contacts ( context ) val mutableEmail = email . mutableCopy { ... } val mutablePhone = phone . mutableCopy { ... } val updateResult = contactsApi . date () . update () . data ( mutableEmail , mutablePhone ) . commit () To check if all updates succeeded, val allUpdatesSuccessful = updateResult . isSuccessful To check if a particular update succeeded, val emailUpdateSuccessful = updateResult . isSuccessful ( mutableEmail ) Once you have performed the updates, you can retrieve the updated data references via the DataQuery APIs, val updatedEmail = contactsApi . data () . query () . emails () . where { Email . Id equalTo emailId } . find () \u2139\ufe0f For more info, read Query specific data kinds . Alternatively, you may use the extensions provided in DataRefresh . To get the updated phone, val updatedPhone = phone . refresh ( contactsApi ) Cancelling the update \u00b6 To cancel an update amid execution, . commit { returnTrueIfUpdateShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel update processing as soon as possible. The function is called numerous times during update processing to check if processing should stop or continue. This gives you the option to cancel the update. For example, to automatically cancel the update inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val updateResult = update . commit { ! isActive } } } Performing the update and result processing asynchronously \u00b6 Updates are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the update with permission \u00b6 Updates require the android.permission.WRITE_CONTACTS permissions. If not granted, the update will do nothing and return a failed result. To perform the update with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Profile data \u00b6 The DataUpdate API also supports updating the Profile (device owner) contact data. To get an instance of this API for Profile data updates, val profileDataUpdate = Contacts ( context ). profile (). data (). update () All updates will be limited to the Profile, whether it exists or not. Custom data support \u00b6 The DataUpdate API supports custom data. For more info, read Update custom data .","title":"Update existing sets of data"},{"location":"data/update-data-sets/#update-existing-sets-of-data","text":"This library provides the DataUpdate API that allows you to update a list of any data kinds in the Contacts Provider database directly without having to update them via Contacts/RawContacts. This ensures that the Contacts Provider database contains the same data you have in memory. An instance of the DataUpdate API is obtained by, val update = Contacts ( context ). data (). update () \u2139\ufe0f To update all kinds of data via Contacts/RawContacts, read Update contacts .","title":"Update existing sets of data"},{"location":"data/update-data-sets/#a-basic-update","text":"To update a set of data, val updateResult = Contacts ( context ) . data () . update () . data ( data ) . commit () If you want to update a list of mutable emails and phones, val updateResult = Contacts ( context ) . data () . update () . data ( mutableEmails + mutablePhones ) . commit ()","title":"A basic update"},{"location":"data/update-data-sets/#blank-data-are-deleted","text":"Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are deleted by update APIs, unless the corresponding fields are not included in the operation. For more info, read about Blank data .","title":"Blank data are deleted"},{"location":"data/update-data-sets/#including-only-specific-data","text":"To perform update operations only the given set of fields (data), . include ( fields ) For example, to perform updates on only email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations .","title":"Including only specific data"},{"location":"data/update-data-sets/#executing-the-update","text":"To execute the update, . commit ()","title":"Executing the update"},{"location":"data/update-data-sets/#handling-the-update-result","text":"The commit function returns a Result , val contactsApi = Contacts ( context ) val mutableEmail = email . mutableCopy { ... } val mutablePhone = phone . mutableCopy { ... } val updateResult = contactsApi . date () . update () . data ( mutableEmail , mutablePhone ) . commit () To check if all updates succeeded, val allUpdatesSuccessful = updateResult . isSuccessful To check if a particular update succeeded, val emailUpdateSuccessful = updateResult . isSuccessful ( mutableEmail ) Once you have performed the updates, you can retrieve the updated data references via the DataQuery APIs, val updatedEmail = contactsApi . data () . query () . emails () . where { Email . Id equalTo emailId } . find () \u2139\ufe0f For more info, read Query specific data kinds . Alternatively, you may use the extensions provided in DataRefresh . To get the updated phone, val updatedPhone = phone . refresh ( contactsApi )","title":"Handling the update result"},{"location":"data/update-data-sets/#cancelling-the-update","text":"To cancel an update amid execution, . commit { returnTrueIfUpdateShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel update processing as soon as possible. The function is called numerous times during update processing to check if processing should stop or continue. This gives you the option to cancel the update. For example, to automatically cancel the update inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val updateResult = update . commit { ! isActive } } }","title":"Cancelling the update"},{"location":"data/update-data-sets/#performing-the-update-and-result-processing-asynchronously","text":"Updates are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the update and result processing asynchronously"},{"location":"data/update-data-sets/#performing-the-update-with-permission","text":"Updates require the android.permission.WRITE_CONTACTS permissions. If not granted, the update will do nothing and return a failed result. To perform the update with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the update with permission"},{"location":"data/update-data-sets/#profile-data","text":"The DataUpdate API also supports updating the Profile (device owner) contact data. To get an instance of this API for Profile data updates, val profileDataUpdate = Contacts ( context ). profile (). data (). update () All updates will be limited to the Profile, whether it exists or not.","title":"Profile data"},{"location":"data/update-data-sets/#custom-data-support","text":"The DataUpdate API supports custom data. For more info, read Update custom data .","title":"Custom data support"},{"location":"debug/debug-blockednumber-provider-tables/","text":"Debug the Blocked Number Provider tables \u00b6 If you want to take a look at the contents of Blocked Number Provider database tables that this library uses, then use the debug module functions to print relevant columns and all rows of a particular table to the Logcat. This is useful if you are experiencing an issue and are trying to figure out if it is this library's fault or not. This is most useful for contributors of this library. It allows us to verify that the work we are doing is correct. Consumers may also use it, especially if they are building their own full-fledged contacts application. Context . logBlockedNumbersTable () This is not meant to be used in production code! \u00b6 DO NOT include usages of the debug module in your production code! It is only meant to be used as a debugging tool during development ! Debug functions do not depend on the core library \u00b6 Notice that the debug module does not depend on the core module, or any other modules in this project. This is done to ensure that whatever is being logged is independent of the core API implementation! This is important for debugging the core APIs during development. We wouldn't exactly want to debug the core APIs using the core APIs. That's just a recipe for disaster! Debug functions assume that privileges are acquired \u00b6 There are no permissions required for blocked numbers. However, there are privileges that must be acquired. For more info, read about Blocked numbers . If privileges are not acquired, the debug functions will not print any table rows.","title":"Debug the BlockedNumber Provider tables"},{"location":"debug/debug-blockednumber-provider-tables/#debug-the-blocked-number-provider-tables","text":"If you want to take a look at the contents of Blocked Number Provider database tables that this library uses, then use the debug module functions to print relevant columns and all rows of a particular table to the Logcat. This is useful if you are experiencing an issue and are trying to figure out if it is this library's fault or not. This is most useful for contributors of this library. It allows us to verify that the work we are doing is correct. Consumers may also use it, especially if they are building their own full-fledged contacts application. Context . logBlockedNumbersTable ()","title":"Debug the Blocked Number Provider tables"},{"location":"debug/debug-blockednumber-provider-tables/#this-is-not-meant-to-be-used-in-production-code","text":"DO NOT include usages of the debug module in your production code! It is only meant to be used as a debugging tool during development !","title":"This is not meant to be used in production code!"},{"location":"debug/debug-blockednumber-provider-tables/#debug-functions-do-not-depend-on-the-core-library","text":"Notice that the debug module does not depend on the core module, or any other modules in this project. This is done to ensure that whatever is being logged is independent of the core API implementation! This is important for debugging the core APIs during development. We wouldn't exactly want to debug the core APIs using the core APIs. That's just a recipe for disaster!","title":"Debug functions do not depend on the core library"},{"location":"debug/debug-blockednumber-provider-tables/#debug-functions-assume-that-privileges-are-acquired","text":"There are no permissions required for blocked numbers. However, there are privileges that must be acquired. For more info, read about Blocked numbers . If privileges are not acquired, the debug functions will not print any table rows.","title":"Debug functions assume that privileges are acquired"},{"location":"debug/debug-contacts-provider-tables/","text":"Debug the Contacts Provider tables \u00b6 If you want to take a look at the contents of Contacts Provider database tables that this library uses, then use the debug module functions to print relevant columns and all rows of a particular table to the Logcat. This is useful if you are experiencing an issue and are trying to figure out if it is this library's fault or not. This is most useful for contributors of this library. It allows us to verify that the work we are doing is correct. Consumers may also use it, especially if they are building their own full-fledged contacts application. Table Function Groups Context.logGroupsTable() AggregationExceptions Context.logAggregationExceptionsTable() Profile Context.logProfile() Contacts Context.logContactsTable() RawContacts Context.logRawContactsTable() Data Context.logDataTable() To log all of the above tables in a single call, Context . logContactsProviderTables () This is not meant to be used in production code! \u00b6 DO NOT include usages of the debug module in your production code! It is only meant to be used as a debugging tool during development ! There are several reasons why you should only use this for debugging. First, Contacts database tables may be very lengthy. Imagine trying to print thousands of contact data! It would slow down your app significantly if you log in the UI thread. Second, Contacts database tables will most likely contain sensitive, private information about your users. If you are working on a contacts app and you are logging your user's Contacts database table rows into remote tracking services for analytics or crash reporting, you could be violating GDPR depending on how you use that information. Be careful. This is why logging functions in the debug module are not customizable and are not part of the core API. Other forms of logging outside of the debug module implemented by this library allows consumers to uphold privacy laws. The debug module is a power tool that should only be used for local debugging purposes! Debug functions do not depend on the core library \u00b6 Notice that the debug module does not depend on the core module, or any other modules in this project. This is done to ensure that whatever is being logged is independent of the core API implementation! This is important for debugging the core APIs during development. We wouldn't exactly want to debug the core APIs using the core APIs. That's just a recipe for disaster! Debug functions assume that permissions have been granted \u00b6 If the read permission android.permission.READ_CONTACTS is not granted, the debug functions will not print any table rows. Debugging other tables \u00b6 To debug Blocked Number Provider tables, read Debug the Blocked Number Provider tables . To debug SIM Contacts table, read Debug the Sim Contacts table .","title":"Debug the Contacts Provider tables"},{"location":"debug/debug-contacts-provider-tables/#debug-the-contacts-provider-tables","text":"If you want to take a look at the contents of Contacts Provider database tables that this library uses, then use the debug module functions to print relevant columns and all rows of a particular table to the Logcat. This is useful if you are experiencing an issue and are trying to figure out if it is this library's fault or not. This is most useful for contributors of this library. It allows us to verify that the work we are doing is correct. Consumers may also use it, especially if they are building their own full-fledged contacts application. Table Function Groups Context.logGroupsTable() AggregationExceptions Context.logAggregationExceptionsTable() Profile Context.logProfile() Contacts Context.logContactsTable() RawContacts Context.logRawContactsTable() Data Context.logDataTable() To log all of the above tables in a single call, Context . logContactsProviderTables ()","title":"Debug the Contacts Provider tables"},{"location":"debug/debug-contacts-provider-tables/#this-is-not-meant-to-be-used-in-production-code","text":"DO NOT include usages of the debug module in your production code! It is only meant to be used as a debugging tool during development ! There are several reasons why you should only use this for debugging. First, Contacts database tables may be very lengthy. Imagine trying to print thousands of contact data! It would slow down your app significantly if you log in the UI thread. Second, Contacts database tables will most likely contain sensitive, private information about your users. If you are working on a contacts app and you are logging your user's Contacts database table rows into remote tracking services for analytics or crash reporting, you could be violating GDPR depending on how you use that information. Be careful. This is why logging functions in the debug module are not customizable and are not part of the core API. Other forms of logging outside of the debug module implemented by this library allows consumers to uphold privacy laws. The debug module is a power tool that should only be used for local debugging purposes!","title":"This is not meant to be used in production code!"},{"location":"debug/debug-contacts-provider-tables/#debug-functions-do-not-depend-on-the-core-library","text":"Notice that the debug module does not depend on the core module, or any other modules in this project. This is done to ensure that whatever is being logged is independent of the core API implementation! This is important for debugging the core APIs during development. We wouldn't exactly want to debug the core APIs using the core APIs. That's just a recipe for disaster!","title":"Debug functions do not depend on the core library"},{"location":"debug/debug-contacts-provider-tables/#debug-functions-assume-that-permissions-have-been-granted","text":"If the read permission android.permission.READ_CONTACTS is not granted, the debug functions will not print any table rows.","title":"Debug functions assume that permissions have been granted"},{"location":"debug/debug-contacts-provider-tables/#debugging-other-tables","text":"To debug Blocked Number Provider tables, read Debug the Blocked Number Provider tables . To debug SIM Contacts table, read Debug the Sim Contacts table .","title":"Debugging other tables"},{"location":"debug/debug-sim-contacts-tables/","text":"Debug the Sim Contacts table \u00b6 If you want to take a look at the contents of Sim Contact database tables that this library uses, then use the debug module functions to print relevant columns and all rows of a particular table to the Logcat. This is useful if you are experiencing an issue and are trying to figure out if it is this library's fault or not. This is most useful for contributors of this library. It allows us to verify that the work we are doing is correct. Consumers may also use it, especially if they are building their own full-fledged contacts application. Context . logSimContactsTable () This is not meant to be used in production code! \u00b6 DO NOT include usages of the debug module in your production code! It is only meant to be used as a debugging tool during development ! Debug functions do not depend on the core library \u00b6 Notice that the debug module does not depend on the core module, or any other modules in this project. This is done to ensure that whatever is being logged is independent of the core API implementation! This is important for debugging the core APIs during development. We wouldn't exactly want to debug the core APIs using the core APIs. That's just a recipe for disaster! Debug functions assume that permissions have been granted \u00b6 If the read permission android.permission.READ_CONTACTS is not granted, the debug functions will not print any table rows.","title":"Debug the Sim Contacts table"},{"location":"debug/debug-sim-contacts-tables/#debug-the-sim-contacts-table","text":"If you want to take a look at the contents of Sim Contact database tables that this library uses, then use the debug module functions to print relevant columns and all rows of a particular table to the Logcat. This is useful if you are experiencing an issue and are trying to figure out if it is this library's fault or not. This is most useful for contributors of this library. It allows us to verify that the work we are doing is correct. Consumers may also use it, especially if they are building their own full-fledged contacts application. Context . logSimContactsTable ()","title":"Debug the Sim Contacts table"},{"location":"debug/debug-sim-contacts-tables/#this-is-not-meant-to-be-used-in-production-code","text":"DO NOT include usages of the debug module in your production code! It is only meant to be used as a debugging tool during development !","title":"This is not meant to be used in production code!"},{"location":"debug/debug-sim-contacts-tables/#debug-functions-do-not-depend-on-the-core-library","text":"Notice that the debug module does not depend on the core module, or any other modules in this project. This is done to ensure that whatever is being logged is independent of the core API implementation! This is important for debugging the core APIs during development. We wouldn't exactly want to debug the core APIs using the core APIs. That's just a recipe for disaster!","title":"Debug functions do not depend on the core library"},{"location":"debug/debug-sim-contacts-tables/#debug-functions-assume-that-permissions-have-been-granted","text":"If the read permission android.permission.READ_CONTACTS is not granted, the debug functions will not print any table rows.","title":"Debug functions assume that permissions have been granted"},{"location":"entities/about-api-entities/","text":"API Entities \u00b6 First, it's important to understand the most basic concept of the Android Contacts Provider / ContactsContract . Afterwards, everything in this library should just make sense. There is only one thing you need to know outside of this library. The library handles the rest of the details so you don't have to! Contacts Provider / ContactsContract Basic Concept \u00b6 There are 3 main database tables used in dealing with contacts. These tables are all connected. Contacts Rows representing different people. E.G. John Doe RawContacts Rows that link Contacts rows to specific Accounts. E.G. John Doe from john.doe@gmail.com, John Doe from john.dow@hotmail.com Data Rows containing data (e.g. name, email) for a RawContacts row. E.G. John Doe from Gmail's name and email, John Doe from Hotmail's phone and address \u2139\ufe0f There are more tables but it won't be covered in this docs for brevity. In the example given (E.G.) above, there is one row in the Contacts table for the person John Doe there are 2 rows in the RawContacts table that make up the Contact John Doe there are 4 rows in the Data table belonging to the Contact John Doe. 2 of these rows belong to John Doe from Gmail and the other 2 belong to John Doe from Hotmail In the background, the Contacts Provider automatically performs the RawContacts linking/aggregation into a single Contact. To forcefully link or unlink sets of RawContacts, read Link unlink Contacts . In the background, the Contacts Provider syncs all data from the local database to the remote database and vice versa (depending on system contact sync settings). Read more in Sync contact data across devices . That's all you need to know! Hopefully it wasn't too much. I know it was difficult for me to grasp in the beginning =P. Once you internalize this one to many relationship between Contacts -> RawContacts -> Data , you have unlocked the full potential of this library and the world is at the palm of your hands ! Contacts API Entities \u00b6 This library provides entities that model everything in the Contacts Provider database. Contact Primarily contains a list of RawContacts that are associated with this contact. RawContact Contains contact data that belong to an account. There may be more than one RawContact per Contact. DataEntity A specific kind of data of a RawContact. These entities model the common data kinds that are provided by the Contacts Provider. Address Email Event GroupMembership Im Name Nickname Note Organization Phone Photo Relation SipAddress Website You can find all of the above in the contacts.core.entities package. Note that there are other entities that are not mentioned in this docs for brevity. All entities are Parcelable to support state retention during app/activity/fragment/view recreation. Each entity has an immutable version (typically returned by queries) and a mutable version (typically used by insert, update, and delete functions). Most immutable entities have a mutableCopy function that returns a mutable copy (typically to be used for inserts and updates and other mutating API functions). \u2139\ufe0f Custom data kinds may also be integrated into the contacts database (though not synced across devices). For more info, read Integrate custom data . \u2139\ufe0f Default native and custom data may be retrieved, set, or cleared. For more info, read Get set clear default Contact data . Contacts API Fields \u00b6 The fields defined in contacts.core.Fields.kt specify what properties of entities to include in read and write operations. For example, to include only the contact display name, organization company, and all phone number fields in a query/insert/update operation, queryInsertUpdate . include ( mutableSetOf < AbstractDataField > (). apply { add ( Fields . Contact . DisplayNamePrimary ) add ( Fields . Organization . Company ) addAll ( Fields . Phone . all ) }) The following entity properties are are used in the read/write operation, Contact { displayNamePrimary RawContact { organization { company } phones { number normalizedNumber type label } } } \u2139\ufe0f For more info, read Include only certain fields for read and write operations . Data kinds count restrictions \u00b6 A RawContact may have at most one OR no limits of certain kinds of data. A RawContact may have 0 or 1 of each of these data kinds; Name Nickname Note Organization Photo SipAddress A RawContact may have 0, 1, or more of each of these data kinds; Address Email Event GroupMembership Im Phone Relation Website The Contacts Provider may or may not enforce these count restrictions. However, the native Contacts app imposes these restrictions. Therefore, this library also imposes these restrictions and disables consumers from violating them. The core library does not explicitly expose count restrictions to consumers. However, it is exposed when integrating custom data via the CustomDataCountRestriction . Data kinds Account restrictions \u00b6 Entries of some data kinds should not be allowed to exist for local RawContacts (those that are not associated with an Account). For more info, read about Local (device-only) contacts . Data integrity \u00b6 There is a section in the official Contacts Provider documentation about \"Data Integrity\"; https://developer.android.com/guide/topics/providers/contacts-provider#DataIntegrity It enumerates four general rules to follow to retain the \"integrity of data\" :D Paraphrasing in terms of this library, the rules are as follows; Always add a Name for every RawContact . Always link new Data to their parent RawContact . Change data only for those raw contacts that you own. Always use the constants defined in ContactsContract and its subclasses for authorities, content URIs, URI paths, column names, MIME types, and TYPE values. This library follows rules 2 and 4. Rule 1 is ignored because the native Contacts app also ignores that rule. Enforcing this rule means that a name has to be provided for every RawContact , which is not practical at all. Users should be able to create contacts with just an email or phone number, without a name. This library follows the native Contacts app behavior, which also disregards this rule =P Rule 3 is intentionally ignored. There are two kinds of data; a. those that are defined in the Contacts Provider (e.g. name, email, phone number, etc) b. those that are defined by other apps (e.g. custom data from other apps) This library allows modification of native data kinds and custom data kinds. Native data kinds should obviously be modifiable as it is the entire reason why the Contacts Provider exposes these data kinds to us in the first place. The question is, should this library provide functions for modifying (insert, update, delete) custom data defined by other apps/services (e.g. Google Contacts, WhatsApp, etc)? The answer to that will be determined when the time comes to support custom data from other apps in the future... Probably, yes! For more info, read Integrate custom data from other apps . Accessing contact data \u00b6 When you have an instance of Contact , you have complete access to data stored in it. To access data of a Contact with only one RawContact, val contact : Contact val rawContact : RawContact = contact . rawContacts . first () Log . d ( \"Contact\" , \"\"\" ID: ${ contact . id } Lookup Key: ${ contact . lookupKey } Display name: ${ contact . displayNamePrimary } Display name alt: ${ contact . displayNameAlt } Photo Uri: ${ contact . photoUri } Thumbnail Uri: ${ contact . photoThumbnailUri } Last updated: ${ contact . lastUpdatedTimestamp } Starred?: ${ contact . options ?. starred } Send to voicemail?: ${ contact . options ?. sendToVoicemail } Ringtone: ${ contact . options ?. customRingtone } Addresses: ${ rawContact . addresses } Emails: ${ rawContact . emails } Events: ${ rawContact . events } Group memberships: ${ rawContact . groupMemberships } IMs: ${ rawContact . ims } Name: ${ rawContact . name } Nickname: ${ rawContact . nickname } Note: ${ rawContact . note } Organization: ${ rawContact . organization } Phones: ${ rawContact . phones } Relations: ${ rawContact . relations } SipAddress: ${ rawContact . sipAddress } Websites: ${ rawContact . websites } \"\"\" . trimIndent () // Photo require separate blocking function calls. ) To access data of a Contact with possibly more than one RawContact, we can use ContactData.kt extensions to make our life easier, val contact : Contact Log . d ( \"Contact\" , \"\"\" ID: ${ contact . id } Lookup Key: ${ contact . lookupKey } Display name: ${ contact . displayNamePrimary } Display name alt: ${ contact . displayNameAlt } Photo Uri: ${ contact . photoUri } Thumbnail Uri: ${ contact . photoThumbnailUri } Last updated: ${ contact . lastUpdatedTimestamp } Starred?: ${ contact . options ?. starred } Send to voicemail?: ${ contact . options ?. sendToVoicemail } Ringtone: ${ contact . options ?. customRingtone } Aggregate data from all RawContacts of the contact ----------------------------------- Addresses: ${ contact . addressList () } Emails: ${ contact . emailList () } Events: ${ contact . eventList () } Group memberships: ${ contact . groupMembershipList () } IMs: ${ contact . imList () } Names: ${ contact . nameList () } Nicknames: ${ contact . nicknameList () } Notes: ${ contact . noteList () } Organizations: ${ contact . organizationList () } Phones: ${ contact . phoneList () } Relations: ${ contact . relationList () } SipAddresses: ${ contact . sipAddressList () } Websites: ${ contact . websiteList () } ----------------------------------- \"\"\" . trimIndent () // There are also aggregate data functions that return a sequence instead of a list. ) Each Contact may have more than one of the following data if the Contact is made up of 2 or more RawContacts; name, nickname, note, organization, sip address. \u2139\ufe0f For more info on how to easily aggregate data from all RawContacts in a Contact, read Convenience functions . \u2139\ufe0f To learn more about the Contact lookup key, read about Contact lookup key vs ID . \u2139\ufe0f To look into the actual Contacts Provider tables, read Debug the Contacts Provider tables . Redacting entities \u00b6 All Entity in this library are Redactable , which indicates that there could be sensitive private user data that could be redacted, for legal purposes. If you are logging contact data in production to remote data centers for analytics or crash reporting, then it is important to redact certain parts of every contact's data. For more info, read Redact entities and API input and output in production . Syncing contact data \u00b6 Syncing contact data, including groups, are done automatically by the Contacts Provider depending on the account sync settings. For more info, read Sync contact data across devices . Developer notes (or for advanced users) \u00b6 Automatic data kinds creation \u00b6 An entry of each of the following data kinds are automatically created for all contacts, if not provided; GroupMembership , underlying value defaults to the account's default system group Name , underlying value defaults to null Nickname , underlying value defaults to null Note , underlying value defaults to null This automatic creation occur automatically in the background (typically after creation) only for RawContacts that are associated with an Account. If a valid account is provided, membership to the (auto add) system group is automatically created immediately by the Contacts Provider at the time of creation. The name, nickname, and note are automatically created at a later time. \u2139\ufe0f Query APIs in this library do not return blanks in results. In this case, the Name , Nickname , and Note will not be included in the RawContact because their primary values are all null. Blanks are also ignored on insert and deleted on update. For more info, read about Blank data . If a valid account is not provided, no entries of the above are automatically created.","title":"API entities"},{"location":"entities/about-api-entities/#api-entities","text":"First, it's important to understand the most basic concept of the Android Contacts Provider / ContactsContract . Afterwards, everything in this library should just make sense. There is only one thing you need to know outside of this library. The library handles the rest of the details so you don't have to!","title":"API Entities"},{"location":"entities/about-api-entities/#contacts-provider-contactscontract-basic-concept","text":"There are 3 main database tables used in dealing with contacts. These tables are all connected. Contacts Rows representing different people. E.G. John Doe RawContacts Rows that link Contacts rows to specific Accounts. E.G. John Doe from john.doe@gmail.com, John Doe from john.dow@hotmail.com Data Rows containing data (e.g. name, email) for a RawContacts row. E.G. John Doe from Gmail's name and email, John Doe from Hotmail's phone and address \u2139\ufe0f There are more tables but it won't be covered in this docs for brevity. In the example given (E.G.) above, there is one row in the Contacts table for the person John Doe there are 2 rows in the RawContacts table that make up the Contact John Doe there are 4 rows in the Data table belonging to the Contact John Doe. 2 of these rows belong to John Doe from Gmail and the other 2 belong to John Doe from Hotmail In the background, the Contacts Provider automatically performs the RawContacts linking/aggregation into a single Contact. To forcefully link or unlink sets of RawContacts, read Link unlink Contacts . In the background, the Contacts Provider syncs all data from the local database to the remote database and vice versa (depending on system contact sync settings). Read more in Sync contact data across devices . That's all you need to know! Hopefully it wasn't too much. I know it was difficult for me to grasp in the beginning =P. Once you internalize this one to many relationship between Contacts -> RawContacts -> Data , you have unlocked the full potential of this library and the world is at the palm of your hands !","title":"Contacts Provider / ContactsContract Basic Concept"},{"location":"entities/about-api-entities/#contacts-api-entities","text":"This library provides entities that model everything in the Contacts Provider database. Contact Primarily contains a list of RawContacts that are associated with this contact. RawContact Contains contact data that belong to an account. There may be more than one RawContact per Contact. DataEntity A specific kind of data of a RawContact. These entities model the common data kinds that are provided by the Contacts Provider. Address Email Event GroupMembership Im Name Nickname Note Organization Phone Photo Relation SipAddress Website You can find all of the above in the contacts.core.entities package. Note that there are other entities that are not mentioned in this docs for brevity. All entities are Parcelable to support state retention during app/activity/fragment/view recreation. Each entity has an immutable version (typically returned by queries) and a mutable version (typically used by insert, update, and delete functions). Most immutable entities have a mutableCopy function that returns a mutable copy (typically to be used for inserts and updates and other mutating API functions). \u2139\ufe0f Custom data kinds may also be integrated into the contacts database (though not synced across devices). For more info, read Integrate custom data . \u2139\ufe0f Default native and custom data may be retrieved, set, or cleared. For more info, read Get set clear default Contact data .","title":"Contacts API Entities"},{"location":"entities/about-api-entities/#contacts-api-fields","text":"The fields defined in contacts.core.Fields.kt specify what properties of entities to include in read and write operations. For example, to include only the contact display name, organization company, and all phone number fields in a query/insert/update operation, queryInsertUpdate . include ( mutableSetOf < AbstractDataField > (). apply { add ( Fields . Contact . DisplayNamePrimary ) add ( Fields . Organization . Company ) addAll ( Fields . Phone . all ) }) The following entity properties are are used in the read/write operation, Contact { displayNamePrimary RawContact { organization { company } phones { number normalizedNumber type label } } } \u2139\ufe0f For more info, read Include only certain fields for read and write operations .","title":"Contacts API Fields"},{"location":"entities/about-api-entities/#data-kinds-count-restrictions","text":"A RawContact may have at most one OR no limits of certain kinds of data. A RawContact may have 0 or 1 of each of these data kinds; Name Nickname Note Organization Photo SipAddress A RawContact may have 0, 1, or more of each of these data kinds; Address Email Event GroupMembership Im Phone Relation Website The Contacts Provider may or may not enforce these count restrictions. However, the native Contacts app imposes these restrictions. Therefore, this library also imposes these restrictions and disables consumers from violating them. The core library does not explicitly expose count restrictions to consumers. However, it is exposed when integrating custom data via the CustomDataCountRestriction .","title":"Data kinds count restrictions"},{"location":"entities/about-api-entities/#data-kinds-account-restrictions","text":"Entries of some data kinds should not be allowed to exist for local RawContacts (those that are not associated with an Account). For more info, read about Local (device-only) contacts .","title":"Data kinds Account restrictions"},{"location":"entities/about-api-entities/#data-integrity","text":"There is a section in the official Contacts Provider documentation about \"Data Integrity\"; https://developer.android.com/guide/topics/providers/contacts-provider#DataIntegrity It enumerates four general rules to follow to retain the \"integrity of data\" :D Paraphrasing in terms of this library, the rules are as follows; Always add a Name for every RawContact . Always link new Data to their parent RawContact . Change data only for those raw contacts that you own. Always use the constants defined in ContactsContract and its subclasses for authorities, content URIs, URI paths, column names, MIME types, and TYPE values. This library follows rules 2 and 4. Rule 1 is ignored because the native Contacts app also ignores that rule. Enforcing this rule means that a name has to be provided for every RawContact , which is not practical at all. Users should be able to create contacts with just an email or phone number, without a name. This library follows the native Contacts app behavior, which also disregards this rule =P Rule 3 is intentionally ignored. There are two kinds of data; a. those that are defined in the Contacts Provider (e.g. name, email, phone number, etc) b. those that are defined by other apps (e.g. custom data from other apps) This library allows modification of native data kinds and custom data kinds. Native data kinds should obviously be modifiable as it is the entire reason why the Contacts Provider exposes these data kinds to us in the first place. The question is, should this library provide functions for modifying (insert, update, delete) custom data defined by other apps/services (e.g. Google Contacts, WhatsApp, etc)? The answer to that will be determined when the time comes to support custom data from other apps in the future... Probably, yes! For more info, read Integrate custom data from other apps .","title":"Data integrity"},{"location":"entities/about-api-entities/#accessing-contact-data","text":"When you have an instance of Contact , you have complete access to data stored in it. To access data of a Contact with only one RawContact, val contact : Contact val rawContact : RawContact = contact . rawContacts . first () Log . d ( \"Contact\" , \"\"\" ID: ${ contact . id } Lookup Key: ${ contact . lookupKey } Display name: ${ contact . displayNamePrimary } Display name alt: ${ contact . displayNameAlt } Photo Uri: ${ contact . photoUri } Thumbnail Uri: ${ contact . photoThumbnailUri } Last updated: ${ contact . lastUpdatedTimestamp } Starred?: ${ contact . options ?. starred } Send to voicemail?: ${ contact . options ?. sendToVoicemail } Ringtone: ${ contact . options ?. customRingtone } Addresses: ${ rawContact . addresses } Emails: ${ rawContact . emails } Events: ${ rawContact . events } Group memberships: ${ rawContact . groupMemberships } IMs: ${ rawContact . ims } Name: ${ rawContact . name } Nickname: ${ rawContact . nickname } Note: ${ rawContact . note } Organization: ${ rawContact . organization } Phones: ${ rawContact . phones } Relations: ${ rawContact . relations } SipAddress: ${ rawContact . sipAddress } Websites: ${ rawContact . websites } \"\"\" . trimIndent () // Photo require separate blocking function calls. ) To access data of a Contact with possibly more than one RawContact, we can use ContactData.kt extensions to make our life easier, val contact : Contact Log . d ( \"Contact\" , \"\"\" ID: ${ contact . id } Lookup Key: ${ contact . lookupKey } Display name: ${ contact . displayNamePrimary } Display name alt: ${ contact . displayNameAlt } Photo Uri: ${ contact . photoUri } Thumbnail Uri: ${ contact . photoThumbnailUri } Last updated: ${ contact . lastUpdatedTimestamp } Starred?: ${ contact . options ?. starred } Send to voicemail?: ${ contact . options ?. sendToVoicemail } Ringtone: ${ contact . options ?. customRingtone } Aggregate data from all RawContacts of the contact ----------------------------------- Addresses: ${ contact . addressList () } Emails: ${ contact . emailList () } Events: ${ contact . eventList () } Group memberships: ${ contact . groupMembershipList () } IMs: ${ contact . imList () } Names: ${ contact . nameList () } Nicknames: ${ contact . nicknameList () } Notes: ${ contact . noteList () } Organizations: ${ contact . organizationList () } Phones: ${ contact . phoneList () } Relations: ${ contact . relationList () } SipAddresses: ${ contact . sipAddressList () } Websites: ${ contact . websiteList () } ----------------------------------- \"\"\" . trimIndent () // There are also aggregate data functions that return a sequence instead of a list. ) Each Contact may have more than one of the following data if the Contact is made up of 2 or more RawContacts; name, nickname, note, organization, sip address. \u2139\ufe0f For more info on how to easily aggregate data from all RawContacts in a Contact, read Convenience functions . \u2139\ufe0f To learn more about the Contact lookup key, read about Contact lookup key vs ID . \u2139\ufe0f To look into the actual Contacts Provider tables, read Debug the Contacts Provider tables .","title":"Accessing contact data"},{"location":"entities/about-api-entities/#redacting-entities","text":"All Entity in this library are Redactable , which indicates that there could be sensitive private user data that could be redacted, for legal purposes. If you are logging contact data in production to remote data centers for analytics or crash reporting, then it is important to redact certain parts of every contact's data. For more info, read Redact entities and API input and output in production .","title":"Redacting entities"},{"location":"entities/about-api-entities/#syncing-contact-data","text":"Syncing contact data, including groups, are done automatically by the Contacts Provider depending on the account sync settings. For more info, read Sync contact data across devices .","title":"Syncing contact data"},{"location":"entities/about-api-entities/#developer-notes-or-for-advanced-users","text":"","title":"Developer notes (or for advanced users)"},{"location":"entities/about-api-entities/#automatic-data-kinds-creation","text":"An entry of each of the following data kinds are automatically created for all contacts, if not provided; GroupMembership , underlying value defaults to the account's default system group Name , underlying value defaults to null Nickname , underlying value defaults to null Note , underlying value defaults to null This automatic creation occur automatically in the background (typically after creation) only for RawContacts that are associated with an Account. If a valid account is provided, membership to the (auto add) system group is automatically created immediately by the Contacts Provider at the time of creation. The name, nickname, and note are automatically created at a later time. \u2139\ufe0f Query APIs in this library do not return blanks in results. In this case, the Name , Nickname , and Note will not be included in the RawContact because their primary values are all null. Blanks are also ignored on insert and deleted on update. For more info, read about Blank data . If a valid account is not provided, no entries of the above are automatically created.","title":"Automatic data kinds creation"},{"location":"entities/about-blank-contacts/","text":"Blank contacts \u00b6 Blank RawContacts and blank Contacts do not have any rows in the Data table. These do not have any non-blank data. An entity is blank if the concrete implementation of Entity.isBlank returns true. The Contacts Providers allows for RawContacts that have no rows in the Data table (let's call them \"blanks\") to exist. The native Contacts app does not allow insertion of new RawContacts without at least one data row. It also deletes blanks on update. Despite seemingly not allowing blanks, the native Contacts app shows them. \u2139\ufe0f This library provides APIs that follows the native Contacts app behavior by default but also allows you to override the default behavior. There are two scenarios where blanks may exist. Contact with RawContact(s) with no Data row(s). In this case, the Contact is blank as well as its RawContact(s). Contact that has RawContact with Data row(s) and a RawContact with no Data row. In this case, the Contact and the RawContact with Data row(s) are not blank but the RawContact with no Data row is blank. Blanks in queries \u00b6 A where clause that uses any fields from the Data table Fields may exclude blanks in the result. There are some joined fields that can be used to match blanks as long as no other fields are in the where clause... Fields.Contact enables matching blank Contacts. The result will include all RawContact(s) belonging to the Contact(s), including blank(s). Examples; Fields.Contact.Id equalTo 5 Fields.Contact.Id in listOf(1,2,3) and Fields.Contact.DisplayNamePrimary contains \"a\" Fields.Contact.Options.Starred equalTo true Fields.RawContact enables matching blank RawContacts. The result will include all Contact(s) these belong to, including sibling RawContacts (blank and not blank). Examples; Fields.RawContact.Id equalTo 5 Fields.RawContact.Id notIn listOf(1,2,3) Blanks will not be included in the results even if they technically should if joined fields from other tables are in the where . In the below example, matching the Contact.Id to an existing blank Contact with Id of 5 will yield no results because it is joined by Fields.Email , which is not a part of Fields.Contact . It should technically return the blank Contact with Id of 5 because the OR operator is used. However, because we internally need to query the Contacts table to match the blanks, a DB exception will be thrown by the Contacts Provider because Fields.Email.Address (\"data1\" and \"mimetype\") are columns from the Data table that do not exist in the Contacts table. The same applies to the Fields.RawContact . Fields.Contact.Id equalTo 5 OR (Fields.Email.Address.isNotNull()) `Fields.RawContact.Id ... OR (Fields.Phone.Number...) Blank Contacts/RawContacts vs blank Data \u00b6 Blank RawContacts and blank Contacts do not have any rows in the Data table. These do not have any non-blank data. Blank data are data entities that have only null, empty, or blank primary value(s). \u2139\ufe0f For more info, read about Blank data .","title":"Blank contacts"},{"location":"entities/about-blank-contacts/#blank-contacts","text":"Blank RawContacts and blank Contacts do not have any rows in the Data table. These do not have any non-blank data. An entity is blank if the concrete implementation of Entity.isBlank returns true. The Contacts Providers allows for RawContacts that have no rows in the Data table (let's call them \"blanks\") to exist. The native Contacts app does not allow insertion of new RawContacts without at least one data row. It also deletes blanks on update. Despite seemingly not allowing blanks, the native Contacts app shows them. \u2139\ufe0f This library provides APIs that follows the native Contacts app behavior by default but also allows you to override the default behavior. There are two scenarios where blanks may exist. Contact with RawContact(s) with no Data row(s). In this case, the Contact is blank as well as its RawContact(s). Contact that has RawContact with Data row(s) and a RawContact with no Data row. In this case, the Contact and the RawContact with Data row(s) are not blank but the RawContact with no Data row is blank.","title":"Blank contacts"},{"location":"entities/about-blank-contacts/#blanks-in-queries","text":"A where clause that uses any fields from the Data table Fields may exclude blanks in the result. There are some joined fields that can be used to match blanks as long as no other fields are in the where clause... Fields.Contact enables matching blank Contacts. The result will include all RawContact(s) belonging to the Contact(s), including blank(s). Examples; Fields.Contact.Id equalTo 5 Fields.Contact.Id in listOf(1,2,3) and Fields.Contact.DisplayNamePrimary contains \"a\" Fields.Contact.Options.Starred equalTo true Fields.RawContact enables matching blank RawContacts. The result will include all Contact(s) these belong to, including sibling RawContacts (blank and not blank). Examples; Fields.RawContact.Id equalTo 5 Fields.RawContact.Id notIn listOf(1,2,3) Blanks will not be included in the results even if they technically should if joined fields from other tables are in the where . In the below example, matching the Contact.Id to an existing blank Contact with Id of 5 will yield no results because it is joined by Fields.Email , which is not a part of Fields.Contact . It should technically return the blank Contact with Id of 5 because the OR operator is used. However, because we internally need to query the Contacts table to match the blanks, a DB exception will be thrown by the Contacts Provider because Fields.Email.Address (\"data1\" and \"mimetype\") are columns from the Data table that do not exist in the Contacts table. The same applies to the Fields.RawContact . Fields.Contact.Id equalTo 5 OR (Fields.Email.Address.isNotNull()) `Fields.RawContact.Id ... OR (Fields.Phone.Number...)","title":"Blanks in queries"},{"location":"entities/about-blank-contacts/#blank-contactsrawcontacts-vs-blank-data","text":"Blank RawContacts and blank Contacts do not have any rows in the Data table. These do not have any non-blank data. Blank data are data entities that have only null, empty, or blank primary value(s). \u2139\ufe0f For more info, read about Blank data .","title":"Blank Contacts/RawContacts vs blank Data"},{"location":"entities/about-blank-data/","text":"Blank data \u00b6 Blank data are data entities that have only null, empty, or blank primary value(s). An entity is blank if the concrete implementation of Entity.isBlank returns true. For example, Email only has one primary value, which is the address ... val blankEmail1 = NewEmail () val blankEmail2 = NewEmail ( address = null ) val blankEmail3 = NewEmail ( address = \"\" ) val blankEmail4 = NewEmail ( address = \" \" ) val blankEmail5 = NewEmail ( type = EmailEntity . Type . HOME ) val emailThatIsNotBlank = NewEmail ( address = \"john.doe@gmail.com\" ) Query APIs in this library do not return null, empty, or blank data in results if they somehow exist in the Contacts Provider database. Insert APIs also ignore blanks and are not inserted. Update APIs deletes blanks. This is the same behavior as the native Contacts app. Blank Data vs blank Contacts/RawContacts \u00b6 Blank data are data entities that have only null, empty, or blank primary value(s). Blank RawContacts and blank Contacts do not have any rows in the Data table. These do not have any non-blank data. \u2139\ufe0f For more info, read about Blank contacts .","title":"Blank data"},{"location":"entities/about-blank-data/#blank-data","text":"Blank data are data entities that have only null, empty, or blank primary value(s). An entity is blank if the concrete implementation of Entity.isBlank returns true. For example, Email only has one primary value, which is the address ... val blankEmail1 = NewEmail () val blankEmail2 = NewEmail ( address = null ) val blankEmail3 = NewEmail ( address = \"\" ) val blankEmail4 = NewEmail ( address = \" \" ) val blankEmail5 = NewEmail ( type = EmailEntity . Type . HOME ) val emailThatIsNotBlank = NewEmail ( address = \"john.doe@gmail.com\" ) Query APIs in this library do not return null, empty, or blank data in results if they somehow exist in the Contacts Provider database. Insert APIs also ignore blanks and are not inserted. Update APIs deletes blanks. This is the same behavior as the native Contacts app.","title":"Blank data"},{"location":"entities/about-blank-data/#blank-data-vs-blank-contactsrawcontacts","text":"Blank data are data entities that have only null, empty, or blank primary value(s). Blank RawContacts and blank Contacts do not have any rows in the Data table. These do not have any non-blank data. \u2139\ufe0f For more info, read about Blank contacts .","title":"Blank Data vs blank Contacts/RawContacts"},{"location":"entities/about-contact-lookup-key/","text":"Contact lookup key vs ID \u00b6 The Contact ID is a number in the Contacts table that serves as the unique identifier for a row in the local Contacts table . These look like any number used as an ID in a database table. For example; 4 , 8 , 15 , 16 , 23 , 42 , ... The Contact lookup key is a string that serves as the unique identifier for an aggregate contact in the local and remote databases . These look like randomly generated or hashed strings. For example; 2059i4a27289d88a0a4e7 , 0r62-2A2C2E , ... The official documentation for the Contact lookup key is, \u2139\ufe0f An opaque value that contains hints on how to find the contact if its row id changed as a result of a sync or aggregation. Let's dissect the documentation, \"if its row id changed\". This means that a Person's row ID can change! \"as a result of a sync\". The Contacts Provider allows sync adapters to modify the local and remote Contacts databases to ensure that Contact data is synced per user account. \"as a result of...aggregation\". Two or more Contacts (along with their constituent RawContacts) can be linked into a single Contact. When this happens, those Contacts will be consolidated into a single (existing) Contact row. Unlinking will result in the original Contacts prior to linking to have different IDs in the Contacts table because the previously deleted row IDs cannot be reused. Unlike the Contact ID, the lookup key is the same across devices (for contacts that are associated with an Account and are synced). The lookup key points to a person entity rather than just a row in a table. It is the unique identifier used by local and remote sync adapters to identify an aggregate contact. \u2139\ufe0f Actually, it seems like the Contact lookup key is a reference to a RawContact (or all of its constituent RawContacts). RawContacts have a reference to the parent Contact via the Contact ID. Similarly, the parent Contact has a reference to all of its constituent RawContacts via the lookup key. Note that RawContacts do not have a lookup key. It is exclusive to Contacts. When to use Contact lookup key vs Contact ID? \u00b6 Use the Contact lookup key when you need to save a reference to a Contact that you want to fetch after some period of time. Saving/restoring activity/fragment instance state. Saving to an external database, preferences, or files. Creating shortcuts. Use the Contact ID for everything else. Performing read/write operations in the same function call or session in your app. Performing read/write operations that require ID (e.g. Contact photo and options). How to get the Contact lookup key? \u00b6 Lookup keys are included in queries by default but are not required. This means that if you use do not invoke the include function in query APIs, then it will be included in the returned Contacts. However, if you do specify fields to include by invoking the include function, then you must explicitly specify the lookup key, . include ( Fields . Contact . LookupKey ) Contact s instances returned by the query will contain a value in the Contact.lookupKey property. For more info, read Include only certain fields for read and write operations . How to get Contacts using lookup keys? \u00b6 Use the decomposedLookupKeys functions in contacts.core.util.ContactLookupKey.kt to get contacts by lookup key, val contacts = query . where { decomposedLookupKeys ( lookupKeys ) whereOr { Contact . LookupKey contains it } }. find () Or use the lookupKeyIn extensions in contacts.core.util.ContactLookupKey.kt to get contacts by lookup key, val contacts = query . where { Contact . lookupKeyIn ( lookupKeys ) }. find () For an explanation on why you should use those functions instead of the lookup key directly, read the function documentation. Note that if the lookup key is a reference to a linked Contact (a Contact with two or more constituent RawContacts), and the linked Contact is unlinked, then the query will return multiple Contacts. \u2139\ufe0f For more info, read Query contacts (advanced) . Moving RawContacts between accounts and the lookup key \u00b6 Associating a local (device-only) RawContact to an Account will change the Contact lookup key. In general, set a RawContact's Account to something else will change the lookup key. In these cases, the changes to the lookup key will only be applied after the Contacts Provider and sync adapters sync the changes. This means that the local changes are not immediately applied. \u2139\ufe0f For more info, read Sync contact data across devices . Changing a RawContact's Account will result in a failed lookup using lookup keys prior to the Account change. For example, using the default AOSP Contacts app or the Google Contacts app... View a contact's details. Create a shortcut to it in the home screen (launcher). This shortcut uses the Contact lookup key (not the ID) to form a lookup URI. Change the RawContact's Account. Tap the shortcut in the home screen (launcher). Both Contacts apps will say that the Contact no longer exist or has been removed. This is not a bug. It is expected behavior due to the way the Contacts Provider works. \u2139\ufe0f For more info, read Associate local RawContacts to an Account . Linking/unlinking contacts and the lookup key \u00b6 Linking and unlinking RawContacts will change the value of the lookup key. However, as discussed in prior sections, you are still able to use the lookup key to find the aggregate Contact even though the Contact ID has changed. Linking/unlinking contacts will result in a successful lookup using lookup keys prior to the Account change. For example, using the default AOSP Contacts app or the Google Contacts app... View a contact's details. Create a shortcut to it in the home screen (launcher). This shortcut uses the Contact lookup key (not the ID) to form a lookup URI. Link the contact to another contact. Tap the shortcut in the home screen (launcher). Unlink the contact. Tap the shortcut in the home screen (launcher). In both cases, the shortcut successfully opens the correct aggregate Contact. \u2139\ufe0f For more info on linking/unlinking, read Link unlink Contacts . Developer notes (or for advanced users) \u00b6 \u2139\ufe0f The following section are note from developers of this library for other developers. It is copied from the DEV_NOTES . You may still read the following as a consumer of the library in case you need deeper insight. The Contacts._ID is the unique identifier for the row in the Contacts table. The Contacts.LOOKUP_KEY is the unique identifier for an aggregate Contact (a person). The _ID may change due to aggregation and sync. The same goes for the LOOKUP_KEY but unlike the ID it may still be used to find the aggregate contact. Unlike the Contact ID, the lookup key is the same across devices (for contacts that are associated with an Account and are synced). \u2139\ufe0f The following investigation was done with a much larger data set. I has been simplified here for brevity. Let's take a look at the following Contacts and RawContacts table rows, #### Contacts table Contact id: 55, lookupKey: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50, displayNamePrimary: Contact With Local RawContact Contact id: 56, lookupKey: 2059i6f5de8460f7f227e, displayNamePrimary: Contact With Synced RawContact #### RawContacts table RawContact id: 55, contactId: 55, displayNamePrimary: Contact With Local RawContact RawContact id: 56, contactId: 56, displayNamePrimary: Contact With Synced RawContact There are two Contacts each having one RawContact. Notice that the lookup keys are a bit different. Contact With Local RawContact: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 Contact With Synced RawContact: 2059i6f5de8460f7f227e The Contact with unsynced, device-only, local RawContact has a much longer (or shorter e.g. 0r62-2A2C2E) lookup key and starts with \"0r -\" and all characters after it are in uppercase. The other thing to notice is that the \"55\" in \"0r55-\" seems to be the same as the RawContact ID (I did a bit more experiments than what is written in these notes to confirm that it is indeed the RawContact ID and not the Contact ID). We probably don't need to worry about these details though the Contacts Provider probably uses these things internally. We also should not rely on it. However, it may be safe to assume that the Contact lookup key is a reference to a RawContact (or reference to more than one constituent RawContact when multiple RawContacts are linked). Again, an internal Contacts Provider detail we should not rely on BUT is probably relevant when implementing sync adapters. When we link the two, we get... Contact id: 55, lookupKey: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50.2059i6f5de8460f7f227e, displayNamePrimary: Contact With Synced RawContact #### RawContacts table RawContact id: 55, contactId: 55, displayNamePrimary: Contact With Local RawContact RawContact id: 56, contactId: 55, displayNamePrimary: Contact With Synced RawContact Notice, Contact with ID 56 has been deleted. Contact with ID 55 still exist with the lookup keys of both Contact 55 and 56 combined separated by a \".\" . This holds true in cases where two or more local-only or non-local-only RawContacts are linked. RawContacts remain unchanged except reference to Contact 56 has been replaced with 55. The primary display name of Contact 55 came from RawContact 55 prior to the link and now comes from RawContact 56 after the link. This primary name resolution is probably irrelevant so pay no attention to it. The most important part to notice is that the lookup keys get combined. The lookup uri is required to build a Contacts.CONTENT_LOOKUP_URI ... /** * A content:// style URI for this table that should be used to create * shortcuts or otherwise create long-term links to contacts. This URI * should always be followed by a \"/\" and the contact's {@link #LOOKUP_KEY}. * It can optionally also have a \"/\" and last known contact ID appended after * that. This \"complete\" format is an important optimization and is highly recommended. *

        * As long as the contact's row ID remains the same, this URI is * equivalent to {@link #CONTENT_URI}. If the contact's row ID changes * as a result of a sync or aggregation, this URI will look up the * contact using indirect information (sync IDs or constituent raw * contacts). *

        * Lookup key should be appended unencoded - it is stored in the encoded * form, ready for use in a URI. */ public static final Uri CONTENT_LOOKUP_URI = Uri . withAppendedPath ( CONTENT_URI , \"lookup\" ); /** * Build a {@link #CONTENT_LOOKUP_URI} lookup {@link Uri} using the * given {@link ContactsContract.Contacts#_ID} and {@link #LOOKUP_KEY}. *

        * Returns null if unable to construct a valid lookup URI from the * provided parameters. */ public static Uri getLookupUri ( long contactId , String lookupKey ) { if ( TextUtils . isEmpty ( lookupKey )) { return null ; } return ContentUris . withAppendedId ( Uri . withAppendedPath ( Contacts . CONTENT_LOOKUP_URI , lookupKey ), contactId ); } From the lookup uri, we can lookup the Contact row... public static Uri lookupContact ( ContentResolver resolver , Uri lookupUri ) { ... } Or simply get the Contact ID... // code inside `public static Uri lookupContact` resolver . query ( lookupUri , new String [] { Contacts . _ID }, null , null , null ) However, given that the lookup key of the deleted Contact 56 still lives on, it is possible to get the linked Contact 55 using the lookup key of Contact 56 using our standard query APIs! . where { Contact . LookupKey contains lookupKey } The above is correct as long as these assumptions hold true; the lookup key is unique there is no lookup key that can contain a shorter lookup key the Contact ID fails this test because a smaller number is contained in a larger number synced contacts have shorter lookup keys than local contacts. However, local contacts' lookup keys are capitalized whereas synced contact are not. Also, there seems to be other differences in pattern between long and short lookup keys. It should be safe to make this assumption. Until the community finds that this assumption is flawed, we'll assume that it is true! For now, we can avoid having to create another API or extensions just for using lookup keys . When we unlink , we get... #### Contacts table Contact id: 55, lookupKey: 2059i6f5de8460f7f227e, displayNamePrimary: Contact With Synced RawContact Contact id: 58, lookupKey: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50, displayNamePrimary: Contact With Local RawContact #### RawContacts table RawContact id: 55, contactId: 58, displayNamePrimary: Contact With Local RawContact RawContact id: 56, contactId: 55, displayNamePrimary: Contact With Synced RawContact Notice, A new Contact row with ID of 58 is created. The lookup keys are separated and distributed between Contact 55 and 58. RawContact 55 Contact reference has been set to Contact 58. Let's compare the Contact-RawContact relationship before and after linking and then unlinking. Contact ID Lookup Key RawContact.Contact ID Before 55, 56 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50, 2059i6f5de8460f7f227e 55, 56 After 55, 58 2059i6f5de8460f7f227e, 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 58, 55 Notice, Contact ID 55 swapped lookup keys with the former Contact 56 (now 58). RawContact ID 55 swapped Contact reference with RawContact 56. The Contact IDs and lookup keys got shuffled BUT the Contact-RawContact relationship remains the same if using the lookup keys as point of reference! Here is another way to look at the table, using the lookup key as the constant... Lookup Key Before After 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 Contact 55, RawContact 55 Contact 58, RawContact 55 2059i6f5de8460f7f227e Contact 56, RawContact 56 Contact 55, RawContact 56 Notice that the indirect relationship between the lookup key and RawContacts remains the same before and after the link-unlink even though the Contact IDs changed. \u2139\ufe0f As mentioned earlier in this section, the \"55\" in \"0r55-\" seems to be referencing the RawContact ID. In other words, since local RawContacts are not synced or tracked in a remote database where Contacts -> RawContacts mappings exist, the Contacts Provider most likely uses this \"0r -\" pattern to make the connection. This is not really relevant for us as we are not relying on this mechanism. I'm just pointing out my observations, which could be incorrect. This means that... If users of this library saved a reference Contact ID 55, then a link-unlink (or sync adapter functions) occur. Getting Contact by ID 55 will result in the RawContact-Data of the former Contact 56 to be returned. This is a bug! Same goes if users saved a reference to Contact ID 56. If users of this library saved a reference to the lookup keys, then a link-unlink (or sync adapter functions) occur. Getting Contact by lookup key will result in the correct RawContact-Data to be returned. So when to use Contact ID vs lookup key? Lookup key: for a reference to a Contact that needs to be loaded after some period of time. Saving/restoring activity/fragment instance state. Saving to an external database, preferences, or files. Creating shortcuts. ID: for everything else. Performing read/write operations in the same function call or session in your app. Performing read/write operations that require ID (e.g. Contact photo and options). Another thing to check is what happens when associating a local RawContact to an Account (move from device to Account) and vice versa. Is the lookup key of the Contact affected? After associating the local RawContact to an Account... #### Contacts table Contact id: 58, lookupKey: 2059i4abd4a8f8ff89642 #### RawContacts table RawContact id: 55, contactId: 58 The lookup key changed but the Contact ID remained the same! In this case, loading a reference to the previously local Contact will fail! I verified that this is indeed the behavior of the native (AOSP) Contacts app. Moving the RawContact from device to Google using Google Contacts app while having Contact details activity opened in the AOSP Contacts app will result in \"error Contact does not exist\" message in the AOSP Contacts app! \u2139\ufe0f The RawContact and its Data also remained the same in this case. Removing the account from it results in... #### Contacts table Contact id: 59, lookupKey: 0r58-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 #### RawContacts table RawContact id: 58, contactId: 59 The Contact and RawContacts row have been deleted and new rows have been created to replace them! I also verified that the Data rows have also been deleted and new rows have been created to replace them! This stuff is not really relevant for lookup key but still good to know for implementing moving between accounts in the future.","title":"Contact lookup key vs ID"},{"location":"entities/about-contact-lookup-key/#contact-lookup-key-vs-id","text":"The Contact ID is a number in the Contacts table that serves as the unique identifier for a row in the local Contacts table . These look like any number used as an ID in a database table. For example; 4 , 8 , 15 , 16 , 23 , 42 , ... The Contact lookup key is a string that serves as the unique identifier for an aggregate contact in the local and remote databases . These look like randomly generated or hashed strings. For example; 2059i4a27289d88a0a4e7 , 0r62-2A2C2E , ... The official documentation for the Contact lookup key is, \u2139\ufe0f An opaque value that contains hints on how to find the contact if its row id changed as a result of a sync or aggregation. Let's dissect the documentation, \"if its row id changed\". This means that a Person's row ID can change! \"as a result of a sync\". The Contacts Provider allows sync adapters to modify the local and remote Contacts databases to ensure that Contact data is synced per user account. \"as a result of...aggregation\". Two or more Contacts (along with their constituent RawContacts) can be linked into a single Contact. When this happens, those Contacts will be consolidated into a single (existing) Contact row. Unlinking will result in the original Contacts prior to linking to have different IDs in the Contacts table because the previously deleted row IDs cannot be reused. Unlike the Contact ID, the lookup key is the same across devices (for contacts that are associated with an Account and are synced). The lookup key points to a person entity rather than just a row in a table. It is the unique identifier used by local and remote sync adapters to identify an aggregate contact. \u2139\ufe0f Actually, it seems like the Contact lookup key is a reference to a RawContact (or all of its constituent RawContacts). RawContacts have a reference to the parent Contact via the Contact ID. Similarly, the parent Contact has a reference to all of its constituent RawContacts via the lookup key. Note that RawContacts do not have a lookup key. It is exclusive to Contacts.","title":"Contact lookup key vs ID"},{"location":"entities/about-contact-lookup-key/#when-to-use-contact-lookup-key-vs-contact-id","text":"Use the Contact lookup key when you need to save a reference to a Contact that you want to fetch after some period of time. Saving/restoring activity/fragment instance state. Saving to an external database, preferences, or files. Creating shortcuts. Use the Contact ID for everything else. Performing read/write operations in the same function call or session in your app. Performing read/write operations that require ID (e.g. Contact photo and options).","title":"When to use Contact lookup key vs Contact ID?"},{"location":"entities/about-contact-lookup-key/#how-to-get-the-contact-lookup-key","text":"Lookup keys are included in queries by default but are not required. This means that if you use do not invoke the include function in query APIs, then it will be included in the returned Contacts. However, if you do specify fields to include by invoking the include function, then you must explicitly specify the lookup key, . include ( Fields . Contact . LookupKey ) Contact s instances returned by the query will contain a value in the Contact.lookupKey property. For more info, read Include only certain fields for read and write operations .","title":"How to get the Contact lookup key?"},{"location":"entities/about-contact-lookup-key/#how-to-get-contacts-using-lookup-keys","text":"Use the decomposedLookupKeys functions in contacts.core.util.ContactLookupKey.kt to get contacts by lookup key, val contacts = query . where { decomposedLookupKeys ( lookupKeys ) whereOr { Contact . LookupKey contains it } }. find () Or use the lookupKeyIn extensions in contacts.core.util.ContactLookupKey.kt to get contacts by lookup key, val contacts = query . where { Contact . lookupKeyIn ( lookupKeys ) }. find () For an explanation on why you should use those functions instead of the lookup key directly, read the function documentation. Note that if the lookup key is a reference to a linked Contact (a Contact with two or more constituent RawContacts), and the linked Contact is unlinked, then the query will return multiple Contacts. \u2139\ufe0f For more info, read Query contacts (advanced) .","title":"How to get Contacts using lookup keys?"},{"location":"entities/about-contact-lookup-key/#moving-rawcontacts-between-accounts-and-the-lookup-key","text":"Associating a local (device-only) RawContact to an Account will change the Contact lookup key. In general, set a RawContact's Account to something else will change the lookup key. In these cases, the changes to the lookup key will only be applied after the Contacts Provider and sync adapters sync the changes. This means that the local changes are not immediately applied. \u2139\ufe0f For more info, read Sync contact data across devices . Changing a RawContact's Account will result in a failed lookup using lookup keys prior to the Account change. For example, using the default AOSP Contacts app or the Google Contacts app... View a contact's details. Create a shortcut to it in the home screen (launcher). This shortcut uses the Contact lookup key (not the ID) to form a lookup URI. Change the RawContact's Account. Tap the shortcut in the home screen (launcher). Both Contacts apps will say that the Contact no longer exist or has been removed. This is not a bug. It is expected behavior due to the way the Contacts Provider works. \u2139\ufe0f For more info, read Associate local RawContacts to an Account .","title":"Moving RawContacts between accounts and the lookup key"},{"location":"entities/about-contact-lookup-key/#linkingunlinking-contacts-and-the-lookup-key","text":"Linking and unlinking RawContacts will change the value of the lookup key. However, as discussed in prior sections, you are still able to use the lookup key to find the aggregate Contact even though the Contact ID has changed. Linking/unlinking contacts will result in a successful lookup using lookup keys prior to the Account change. For example, using the default AOSP Contacts app or the Google Contacts app... View a contact's details. Create a shortcut to it in the home screen (launcher). This shortcut uses the Contact lookup key (not the ID) to form a lookup URI. Link the contact to another contact. Tap the shortcut in the home screen (launcher). Unlink the contact. Tap the shortcut in the home screen (launcher). In both cases, the shortcut successfully opens the correct aggregate Contact. \u2139\ufe0f For more info on linking/unlinking, read Link unlink Contacts .","title":"Linking/unlinking contacts and the lookup key"},{"location":"entities/about-contact-lookup-key/#developer-notes-or-for-advanced-users","text":"\u2139\ufe0f The following section are note from developers of this library for other developers. It is copied from the DEV_NOTES . You may still read the following as a consumer of the library in case you need deeper insight. The Contacts._ID is the unique identifier for the row in the Contacts table. The Contacts.LOOKUP_KEY is the unique identifier for an aggregate Contact (a person). The _ID may change due to aggregation and sync. The same goes for the LOOKUP_KEY but unlike the ID it may still be used to find the aggregate contact. Unlike the Contact ID, the lookup key is the same across devices (for contacts that are associated with an Account and are synced). \u2139\ufe0f The following investigation was done with a much larger data set. I has been simplified here for brevity. Let's take a look at the following Contacts and RawContacts table rows, #### Contacts table Contact id: 55, lookupKey: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50, displayNamePrimary: Contact With Local RawContact Contact id: 56, lookupKey: 2059i6f5de8460f7f227e, displayNamePrimary: Contact With Synced RawContact #### RawContacts table RawContact id: 55, contactId: 55, displayNamePrimary: Contact With Local RawContact RawContact id: 56, contactId: 56, displayNamePrimary: Contact With Synced RawContact There are two Contacts each having one RawContact. Notice that the lookup keys are a bit different. Contact With Local RawContact: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 Contact With Synced RawContact: 2059i6f5de8460f7f227e The Contact with unsynced, device-only, local RawContact has a much longer (or shorter e.g. 0r62-2A2C2E) lookup key and starts with \"0r -\" and all characters after it are in uppercase. The other thing to notice is that the \"55\" in \"0r55-\" seems to be the same as the RawContact ID (I did a bit more experiments than what is written in these notes to confirm that it is indeed the RawContact ID and not the Contact ID). We probably don't need to worry about these details though the Contacts Provider probably uses these things internally. We also should not rely on it. However, it may be safe to assume that the Contact lookup key is a reference to a RawContact (or reference to more than one constituent RawContact when multiple RawContacts are linked). Again, an internal Contacts Provider detail we should not rely on BUT is probably relevant when implementing sync adapters. When we link the two, we get... Contact id: 55, lookupKey: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50.2059i6f5de8460f7f227e, displayNamePrimary: Contact With Synced RawContact #### RawContacts table RawContact id: 55, contactId: 55, displayNamePrimary: Contact With Local RawContact RawContact id: 56, contactId: 55, displayNamePrimary: Contact With Synced RawContact Notice, Contact with ID 56 has been deleted. Contact with ID 55 still exist with the lookup keys of both Contact 55 and 56 combined separated by a \".\" . This holds true in cases where two or more local-only or non-local-only RawContacts are linked. RawContacts remain unchanged except reference to Contact 56 has been replaced with 55. The primary display name of Contact 55 came from RawContact 55 prior to the link and now comes from RawContact 56 after the link. This primary name resolution is probably irrelevant so pay no attention to it. The most important part to notice is that the lookup keys get combined. The lookup uri is required to build a Contacts.CONTENT_LOOKUP_URI ... /** * A content:// style URI for this table that should be used to create * shortcuts or otherwise create long-term links to contacts. This URI * should always be followed by a \"/\" and the contact's {@link #LOOKUP_KEY}. * It can optionally also have a \"/\" and last known contact ID appended after * that. This \"complete\" format is an important optimization and is highly recommended. *

        * As long as the contact's row ID remains the same, this URI is * equivalent to {@link #CONTENT_URI}. If the contact's row ID changes * as a result of a sync or aggregation, this URI will look up the * contact using indirect information (sync IDs or constituent raw * contacts). *

        * Lookup key should be appended unencoded - it is stored in the encoded * form, ready for use in a URI. */ public static final Uri CONTENT_LOOKUP_URI = Uri . withAppendedPath ( CONTENT_URI , \"lookup\" ); /** * Build a {@link #CONTENT_LOOKUP_URI} lookup {@link Uri} using the * given {@link ContactsContract.Contacts#_ID} and {@link #LOOKUP_KEY}. *

        * Returns null if unable to construct a valid lookup URI from the * provided parameters. */ public static Uri getLookupUri ( long contactId , String lookupKey ) { if ( TextUtils . isEmpty ( lookupKey )) { return null ; } return ContentUris . withAppendedId ( Uri . withAppendedPath ( Contacts . CONTENT_LOOKUP_URI , lookupKey ), contactId ); } From the lookup uri, we can lookup the Contact row... public static Uri lookupContact ( ContentResolver resolver , Uri lookupUri ) { ... } Or simply get the Contact ID... // code inside `public static Uri lookupContact` resolver . query ( lookupUri , new String [] { Contacts . _ID }, null , null , null ) However, given that the lookup key of the deleted Contact 56 still lives on, it is possible to get the linked Contact 55 using the lookup key of Contact 56 using our standard query APIs! . where { Contact . LookupKey contains lookupKey } The above is correct as long as these assumptions hold true; the lookup key is unique there is no lookup key that can contain a shorter lookup key the Contact ID fails this test because a smaller number is contained in a larger number synced contacts have shorter lookup keys than local contacts. However, local contacts' lookup keys are capitalized whereas synced contact are not. Also, there seems to be other differences in pattern between long and short lookup keys. It should be safe to make this assumption. Until the community finds that this assumption is flawed, we'll assume that it is true! For now, we can avoid having to create another API or extensions just for using lookup keys . When we unlink , we get... #### Contacts table Contact id: 55, lookupKey: 2059i6f5de8460f7f227e, displayNamePrimary: Contact With Synced RawContact Contact id: 58, lookupKey: 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50, displayNamePrimary: Contact With Local RawContact #### RawContacts table RawContact id: 55, contactId: 58, displayNamePrimary: Contact With Local RawContact RawContact id: 56, contactId: 55, displayNamePrimary: Contact With Synced RawContact Notice, A new Contact row with ID of 58 is created. The lookup keys are separated and distributed between Contact 55 and 58. RawContact 55 Contact reference has been set to Contact 58. Let's compare the Contact-RawContact relationship before and after linking and then unlinking. Contact ID Lookup Key RawContact.Contact ID Before 55, 56 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50, 2059i6f5de8460f7f227e 55, 56 After 55, 58 2059i6f5de8460f7f227e, 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 58, 55 Notice, Contact ID 55 swapped lookup keys with the former Contact 56 (now 58). RawContact ID 55 swapped Contact reference with RawContact 56. The Contact IDs and lookup keys got shuffled BUT the Contact-RawContact relationship remains the same if using the lookup keys as point of reference! Here is another way to look at the table, using the lookup key as the constant... Lookup Key Before After 0r55-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 Contact 55, RawContact 55 Contact 58, RawContact 55 2059i6f5de8460f7f227e Contact 56, RawContact 56 Contact 55, RawContact 56 Notice that the indirect relationship between the lookup key and RawContacts remains the same before and after the link-unlink even though the Contact IDs changed. \u2139\ufe0f As mentioned earlier in this section, the \"55\" in \"0r55-\" seems to be referencing the RawContact ID. In other words, since local RawContacts are not synced or tracked in a remote database where Contacts -> RawContacts mappings exist, the Contacts Provider most likely uses this \"0r -\" pattern to make the connection. This is not really relevant for us as we are not relying on this mechanism. I'm just pointing out my observations, which could be incorrect. This means that... If users of this library saved a reference Contact ID 55, then a link-unlink (or sync adapter functions) occur. Getting Contact by ID 55 will result in the RawContact-Data of the former Contact 56 to be returned. This is a bug! Same goes if users saved a reference to Contact ID 56. If users of this library saved a reference to the lookup keys, then a link-unlink (or sync adapter functions) occur. Getting Contact by lookup key will result in the correct RawContact-Data to be returned. So when to use Contact ID vs lookup key? Lookup key: for a reference to a Contact that needs to be loaded after some period of time. Saving/restoring activity/fragment instance state. Saving to an external database, preferences, or files. Creating shortcuts. ID: for everything else. Performing read/write operations in the same function call or session in your app. Performing read/write operations that require ID (e.g. Contact photo and options). Another thing to check is what happens when associating a local RawContact to an Account (move from device to Account) and vice versa. Is the lookup key of the Contact affected? After associating the local RawContact to an Account... #### Contacts table Contact id: 58, lookupKey: 2059i4abd4a8f8ff89642 #### RawContacts table RawContact id: 55, contactId: 58 The lookup key changed but the Contact ID remained the same! In this case, loading a reference to the previously local Contact will fail! I verified that this is indeed the behavior of the native (AOSP) Contacts app. Moving the RawContact from device to Google using Google Contacts app while having Contact details activity opened in the AOSP Contacts app will result in \"error Contact does not exist\" message in the AOSP Contacts app! \u2139\ufe0f The RawContact and its Data also remained the same in this case. Removing the account from it results in... #### Contacts table Contact id: 59, lookupKey: 0r58-2E4644502A2E50563A503840462E2A404C2A562E4644502A2E50 #### RawContacts table RawContact id: 58, contactId: 59 The Contact and RawContacts row have been deleted and new rows have been created to replace them! I also verified that the Data rows have also been deleted and new rows have been created to replace them! This stuff is not really relevant for lookup key but still good to know for implementing moving between accounts in the future.","title":"Developer notes (or for advanced users)"},{"location":"entities/about-local-contacts/","text":"Local (device-only) contacts \u00b6 Contacts, or more specifically RawContacts, that are not associated with an android.accounts.Account are local to each device and will not be synced across devices. This means that any RawContacts you create, update, or delete will NOT be synced on any device or remote service as it is not associated with any account. \u2139\ufe0f For more info, read Sync contact data across devices . Associating a local RawContact to an Account \u00b6 Local RawContacts can be associated to an Account to enable syncing. For more info, read Associate local RawContacts to an Account . Adding an Account to the device \u00b6 Depending on the API level, the Contacts Provider behaves differently when the user adds an account to the device. Lollipop (API 22) and below When an Account is added, from a state where no accounts have yet been added to the system, the Contacts Provider automatically sets all of the null accountName and accountType in the RawContacts table to that Account's name and type. RawContacts inserted without an associated account will automatically get assigned to an account if there are any available. This may take a few seconds, whenever the Contacts Provider decides to do it. Dissociating RawContacts from Accounts will result in the Contacts Provider associating those back to an Account. Marshmallow (API 23) and above The Contacts Provider no longer associates local RawContacts to an account when an account is or becomes available. Local contacts remain local. Account removal Removing the Account will remove all of the associated rows in the RawContact, Data, and Groups tables locally . This includes user Profile data in those tables. \u2139\ufe0f When all RawContacts of a Contact is removed, the Contact is also automatically removed by the Contacts Provider. Data kinds Account restrictions \u00b6 Entries of some data kinds should not be allowed to exist for local RawContacts. \u2139\ufe0f The native Contacts app hides the following UI fields when inserting or updating local RawContacts. To enforce this behavior, this library ignores all of the above during inserts and updates for local raw contacts. These data kinds are; GroupMembership Groups can only exist if it is associated with an Account. Therefore, memberships to groups is not possible when there is no associated Account. Event It is not clear why this requires an associated Account. Maybe because these are typically birth dates that users expect to be synced with their calendar across devices? Relation It is not clear why this requires an associated Account... The Contacts Provider may or may not enforce these Account restrictions. However, the native Contacts app imposes these restrictions. Therefore, this library also imposes these restrictions and disables consumers from violating them.","title":"Local (device-only) contacts"},{"location":"entities/about-local-contacts/#local-device-only-contacts","text":"Contacts, or more specifically RawContacts, that are not associated with an android.accounts.Account are local to each device and will not be synced across devices. This means that any RawContacts you create, update, or delete will NOT be synced on any device or remote service as it is not associated with any account. \u2139\ufe0f For more info, read Sync contact data across devices .","title":"Local (device-only) contacts"},{"location":"entities/about-local-contacts/#associating-a-local-rawcontact-to-an-account","text":"Local RawContacts can be associated to an Account to enable syncing. For more info, read Associate local RawContacts to an Account .","title":"Associating a local RawContact to an Account"},{"location":"entities/about-local-contacts/#adding-an-account-to-the-device","text":"Depending on the API level, the Contacts Provider behaves differently when the user adds an account to the device. Lollipop (API 22) and below When an Account is added, from a state where no accounts have yet been added to the system, the Contacts Provider automatically sets all of the null accountName and accountType in the RawContacts table to that Account's name and type. RawContacts inserted without an associated account will automatically get assigned to an account if there are any available. This may take a few seconds, whenever the Contacts Provider decides to do it. Dissociating RawContacts from Accounts will result in the Contacts Provider associating those back to an Account. Marshmallow (API 23) and above The Contacts Provider no longer associates local RawContacts to an account when an account is or becomes available. Local contacts remain local. Account removal Removing the Account will remove all of the associated rows in the RawContact, Data, and Groups tables locally . This includes user Profile data in those tables. \u2139\ufe0f When all RawContacts of a Contact is removed, the Contact is also automatically removed by the Contacts Provider.","title":"Adding an Account to the device"},{"location":"entities/about-local-contacts/#data-kinds-account-restrictions","text":"Entries of some data kinds should not be allowed to exist for local RawContacts. \u2139\ufe0f The native Contacts app hides the following UI fields when inserting or updating local RawContacts. To enforce this behavior, this library ignores all of the above during inserts and updates for local raw contacts. These data kinds are; GroupMembership Groups can only exist if it is associated with an Account. Therefore, memberships to groups is not possible when there is no associated Account. Event It is not clear why this requires an associated Account. Maybe because these are typically birth dates that users expect to be synced with their calendar across devices? Relation It is not clear why this requires an associated Account... The Contacts Provider may or may not enforce these Account restrictions. However, the native Contacts app imposes these restrictions. Therefore, this library also imposes these restrictions and disables consumers from violating them.","title":"Data kinds Account restrictions"},{"location":"entities/include-only-desired-data/","text":"Include only certain fields for read and write operations \u00b6 The read (query) and write (insert, update) APIs in this library provides an include function that allows you to specify all (default) or some fields to read or write in the Contacts Provider database. The fields defined in contacts.core.Fields.kt specify what properties of entities to include in read and write operations. For example, to include only the contact display name, organization company, and all phone number fields in a query/insert/update operation, queryInsertUpdate . include ( mutableSetOf < AbstractDataField > (). apply { add ( Fields . Contact . DisplayNamePrimary ) add ( Fields . Organization . Company ) addAll ( Fields . Phone . all ) }) The following entity properties are are used in the read/write operation, Contact { displayNamePrimary RawContact { organization { company } phones { number normalizedNumber type label } } } \u2139\ufe0f For more info, read about API Entities . To explicitly include everything, query . include ( Fields . all ) \u2139\ufe0f Not invoking the include function will default to including everything, including custom data. The above code will exclude custom data. Read the Custom data support section for more info. The matching contacts may have non-null data corresponding to each of the included fields. Fields that are included will not guarantee non-null data in the returned contact instances because some data may actually be null in the database. If no fields are specified, then all fields are included. Otherwise, only the specified fields will be included in addition to required API fields (e.g. IDs), which are always included. \u2139\ufe0f This may affect performance. It is recommended to only include fields that will be used to save CPU and memory. Using include in query APIs \u00b6 When using query APIs such as Query , BroadQuery , ProfileQuery , and DataQuery , you are able to specify all or only some kinds of data that you want to be included in the returned results. When all fields are included in a query operation, all properties of Contacts, RawContacts, and Data are populated with values from the database. Properties of fields that are included are not guaranteed to be non-null because the database may actually have no data for the corresponding field. When only some fields are included, only those included properties of Contacts, RawContacts, and Data are populated with values from the database. Properties of fields that are not included are guaranteed to be null. Using include in insert APIs \u00b6 When using insert APIs such as Insert and ProfileInsert , you are able to specify all or only some kinds of data that you want to be included in the insert operation. When all fields are included in an insert operation, all properties of Contacts, RawContacts, and Data are inserted into the database. When only some fields are included, only those included properties of Contacts, RawContacts, and Data are inserted into the database. Properties of fields that are not included are NOT inserted into the database. Using include in update APIs \u00b6 When using update APIs such as Update , ProfileUpdate , and DataUpdate , you are able to specify all or only some kinds of data that you want to be included in the update operation. An \"update\" operation consists of insertion, updates, and deletions \u00b6 To ensure that the database matches the data contained in the entities being passed into the update operation, a combination of insert, update, or delete operations are performed internally by the update API. The following is what constitutes an \"updated\" event; A RawContact can have 0 or 1 name. If it is null or blank, then the update operation will... delete the name row of the RawContact from the database, if it exist If it is not null, then the update operation will do one of the following... update an existing name row, if it exist or insert a new name row, if one does not exist A RawContact can have 0, 1 or more emails. If the list of emails is empty (or contains only blanks), then the update operation will... delete all email rows of the RawContact from the database, if any exist If the list of emails is not empty, then the update operation will do all of the following... update email rows for emails that already exist in the database insert new email rows for emails that do not yet exist in the database delete email rows for emails that exist in the database but not in the (in-memory) entity Blank data are deleted \u00b6 Blank data are deleted from the database, unless the the complete set of corresponding fields are not included in the update operation. \u26a0\ufe0f Prior to version 0.3.0 where include in update APIs have been overhauled , blank data are deleted from the database even if the corresponding fields are not included. \u2139\ufe0f For more info on blank data, read about Blank data . Including complete field sets for \"update\" \u00b6 When all fields are included in an update operation, all properties of Contacts, RawContacts, and Data are \"updated\" in the database. When only some fields are included, only those included properties of Contacts, RawContacts, and Data are \"updated\" in the database. Properties of fields that are not included are NOT \"updated\" . To get all contacts including all fields, then modify the emails, phones, and addresses and perform an update operation on all fields, val contacts = query . find () val contactsWithModifiedEmailPhoneAddress = modifyEmailPhoneAddressIn ( contacts ) update . contacts ( contactsWithModifiedEmailPhoneAddress ). commit () To modify all emails of all contacts without updating anything else into the database, val contactsWithOnlyEmailData = query . include ( Fields . Email . all ). find () val contactsWithModifiedEmailData = modifyEmailsIn ( contactsWithOnlyEmailData ) update . contacts ( contactsWithModifiedEmailData ). include ( Fields . Email . all ). commit () To remove all emails from all contacts without updating anything else in the database, val contactsWithAllData = query . find () val contactsWithNoEmailData = removeEmailsFrom ( contactsWithAllData ) update . contacts ( contactsWithNoEmailData ). include ( Fields . Email . all ). commit () Or alternatively, val contactsWithNoData = query . include ( Fields . Required . all ) // does not have to be Fields.Required. . find () update . contacts ( contactsWithNoData ). include ( Fields . Email . all ). commit () Including a subset of field sets for \"update\" \u00b6 Including only a subset of a set of fields results in, deletion of blanks (same as if the complete set of fields are included) update of properties corresponding to included fields no-op on properties corresponding to excluded fields For example, the following set the given name and family name to the non-null values but does not set all others (i.e. display name, middle name, prefix, suffix, phonetic given middle family name). contacts . update () . include ( Fields . Name . GivenName , Fields . Name . FamilyName , ) . rawContacts ( existingRawContact . mutableCopy { setName { displayName = \"Mr. \" prefix = \"Mr.\" givenName = \"First\" middleName = \"Middle\" familyName = \"Last\" suffix = \"Jr.\" phoneticGivenName = \"fUHRst\" phoneticMiddleName = \"mIdl\" phoneticFamilyName = \"lAHst\" } } ) . commit () If the name row for the RawContact did not exist before the update operation, then a new name row will be inserted into the database for the RawContact. The given name and family name columns will be set to the specified values. All other columns will be set to null. If the name row for the RawContact already exists before the update operation, then the name row will be updated. The given name and family name columns will be set to the specified values. All other columns will remain unchanged (the null or non-null values will remain null and non-null respectively). Custom data support \u00b6 The include function supports registered custom data fields, which my be combined with native (non-custom) data fields. By default, not calling the include function will include all fields, including custom data. However, the below code will include all native fields but exclude custom data; . include ( Fields . all ) If you want to include everything, including custom data, and for some reason you must invoke the include function, . include ( Fields . all + contactsApi . customDataRegistry . allFields ())","title":"Include only certain fields for read and write operations"},{"location":"entities/include-only-desired-data/#include-only-certain-fields-for-read-and-write-operations","text":"The read (query) and write (insert, update) APIs in this library provides an include function that allows you to specify all (default) or some fields to read or write in the Contacts Provider database. The fields defined in contacts.core.Fields.kt specify what properties of entities to include in read and write operations. For example, to include only the contact display name, organization company, and all phone number fields in a query/insert/update operation, queryInsertUpdate . include ( mutableSetOf < AbstractDataField > (). apply { add ( Fields . Contact . DisplayNamePrimary ) add ( Fields . Organization . Company ) addAll ( Fields . Phone . all ) }) The following entity properties are are used in the read/write operation, Contact { displayNamePrimary RawContact { organization { company } phones { number normalizedNumber type label } } } \u2139\ufe0f For more info, read about API Entities . To explicitly include everything, query . include ( Fields . all ) \u2139\ufe0f Not invoking the include function will default to including everything, including custom data. The above code will exclude custom data. Read the Custom data support section for more info. The matching contacts may have non-null data corresponding to each of the included fields. Fields that are included will not guarantee non-null data in the returned contact instances because some data may actually be null in the database. If no fields are specified, then all fields are included. Otherwise, only the specified fields will be included in addition to required API fields (e.g. IDs), which are always included. \u2139\ufe0f This may affect performance. It is recommended to only include fields that will be used to save CPU and memory.","title":"Include only certain fields for read and write operations"},{"location":"entities/include-only-desired-data/#using-include-in-query-apis","text":"When using query APIs such as Query , BroadQuery , ProfileQuery , and DataQuery , you are able to specify all or only some kinds of data that you want to be included in the returned results. When all fields are included in a query operation, all properties of Contacts, RawContacts, and Data are populated with values from the database. Properties of fields that are included are not guaranteed to be non-null because the database may actually have no data for the corresponding field. When only some fields are included, only those included properties of Contacts, RawContacts, and Data are populated with values from the database. Properties of fields that are not included are guaranteed to be null.","title":"Using include in query APIs"},{"location":"entities/include-only-desired-data/#using-include-in-insert-apis","text":"When using insert APIs such as Insert and ProfileInsert , you are able to specify all or only some kinds of data that you want to be included in the insert operation. When all fields are included in an insert operation, all properties of Contacts, RawContacts, and Data are inserted into the database. When only some fields are included, only those included properties of Contacts, RawContacts, and Data are inserted into the database. Properties of fields that are not included are NOT inserted into the database.","title":"Using include in insert APIs"},{"location":"entities/include-only-desired-data/#using-include-in-update-apis","text":"When using update APIs such as Update , ProfileUpdate , and DataUpdate , you are able to specify all or only some kinds of data that you want to be included in the update operation.","title":"Using include in update APIs"},{"location":"entities/include-only-desired-data/#an-update-operation-consists-of-insertion-updates-and-deletions","text":"To ensure that the database matches the data contained in the entities being passed into the update operation, a combination of insert, update, or delete operations are performed internally by the update API. The following is what constitutes an \"updated\" event; A RawContact can have 0 or 1 name. If it is null or blank, then the update operation will... delete the name row of the RawContact from the database, if it exist If it is not null, then the update operation will do one of the following... update an existing name row, if it exist or insert a new name row, if one does not exist A RawContact can have 0, 1 or more emails. If the list of emails is empty (or contains only blanks), then the update operation will... delete all email rows of the RawContact from the database, if any exist If the list of emails is not empty, then the update operation will do all of the following... update email rows for emails that already exist in the database insert new email rows for emails that do not yet exist in the database delete email rows for emails that exist in the database but not in the (in-memory) entity","title":"An \"update\" operation consists of insertion, updates, and deletions"},{"location":"entities/include-only-desired-data/#blank-data-are-deleted","text":"Blank data are deleted from the database, unless the the complete set of corresponding fields are not included in the update operation. \u26a0\ufe0f Prior to version 0.3.0 where include in update APIs have been overhauled , blank data are deleted from the database even if the corresponding fields are not included. \u2139\ufe0f For more info on blank data, read about Blank data .","title":"Blank data are deleted"},{"location":"entities/include-only-desired-data/#including-complete-field-sets-for-update","text":"When all fields are included in an update operation, all properties of Contacts, RawContacts, and Data are \"updated\" in the database. When only some fields are included, only those included properties of Contacts, RawContacts, and Data are \"updated\" in the database. Properties of fields that are not included are NOT \"updated\" . To get all contacts including all fields, then modify the emails, phones, and addresses and perform an update operation on all fields, val contacts = query . find () val contactsWithModifiedEmailPhoneAddress = modifyEmailPhoneAddressIn ( contacts ) update . contacts ( contactsWithModifiedEmailPhoneAddress ). commit () To modify all emails of all contacts without updating anything else into the database, val contactsWithOnlyEmailData = query . include ( Fields . Email . all ). find () val contactsWithModifiedEmailData = modifyEmailsIn ( contactsWithOnlyEmailData ) update . contacts ( contactsWithModifiedEmailData ). include ( Fields . Email . all ). commit () To remove all emails from all contacts without updating anything else in the database, val contactsWithAllData = query . find () val contactsWithNoEmailData = removeEmailsFrom ( contactsWithAllData ) update . contacts ( contactsWithNoEmailData ). include ( Fields . Email . all ). commit () Or alternatively, val contactsWithNoData = query . include ( Fields . Required . all ) // does not have to be Fields.Required. . find () update . contacts ( contactsWithNoData ). include ( Fields . Email . all ). commit ()","title":"Including complete field sets for \"update\""},{"location":"entities/include-only-desired-data/#including-a-subset-of-field-sets-for-update","text":"Including only a subset of a set of fields results in, deletion of blanks (same as if the complete set of fields are included) update of properties corresponding to included fields no-op on properties corresponding to excluded fields For example, the following set the given name and family name to the non-null values but does not set all others (i.e. display name, middle name, prefix, suffix, phonetic given middle family name). contacts . update () . include ( Fields . Name . GivenName , Fields . Name . FamilyName , ) . rawContacts ( existingRawContact . mutableCopy { setName { displayName = \"Mr. \" prefix = \"Mr.\" givenName = \"First\" middleName = \"Middle\" familyName = \"Last\" suffix = \"Jr.\" phoneticGivenName = \"fUHRst\" phoneticMiddleName = \"mIdl\" phoneticFamilyName = \"lAHst\" } } ) . commit () If the name row for the RawContact did not exist before the update operation, then a new name row will be inserted into the database for the RawContact. The given name and family name columns will be set to the specified values. All other columns will be set to null. If the name row for the RawContact already exists before the update operation, then the name row will be updated. The given name and family name columns will be set to the specified values. All other columns will remain unchanged (the null or non-null values will remain null and non-null respectively).","title":"Including a subset of field sets for \"update\""},{"location":"entities/include-only-desired-data/#custom-data-support","text":"The include function supports registered custom data fields, which my be combined with native (non-custom) data fields. By default, not calling the include function will include all fields, including custom data. However, the below code will include all native fields but exclude custom data; . include ( Fields . all ) If you want to include everything, including custom data, and for some reason you must invoke the include function, . include ( Fields . all + contactsApi . customDataRegistry . allFields ())","title":"Custom data support"},{"location":"entities/redact-apis-and-entities/","text":"Redact entities and API input and output in production \u00b6 All of the entities and Create (Query), Read (Query), Update, and Delete APIs (a.k.a CRUD APIs) provided in this library are Redactable . Redactables indicates that there could be sensitive private user data that could be redacted, for legal purposes such as upholding GDPR guidelines. If you are logging contact data in production to remote data centers for analytics or crash reporting, then it is important to redact certain parts of every contact's data. \u2139\ufe0f For more info on logging, read Log API input and output . DISCLAIMER: This is NOT legal advice! \u00b6 This library is written and maintained purely by software developers with no official education or certifications in any facet of law. Please review the redacted outputs of the APIs and entities within this library with your legal team! This library will not be held liable for any privacy violations! With that out of the way, let's move on to the good stuff =) Redactable entities \u00b6 All Entity in this library are Redactable . For example, Contact: id=1, email { address=\"vestrel00@gmail.com\" }, phone { number=\"(555) 555-5555\" }, etc when redacted, Contact: id=1, email { address=\"*******************\" }, phone { number=\"************\" }, etc Notice that all characters in private user data are replaced with \"*\". Redacted strings are not as useful as the non-redacted counterpart. However, we still have the following valuable information; is the string null or not? how long is the string? Database row IDs (and typically non-string properties) do not have to be redacted unless they contain sensitive information. The redactedCopy function will return an actual copy of the entity, except with sensitive data redacted. In addition to logging, this will allow consumers to do cool things like implementing a redacted contact view! Imagine a button that the user can press to redact everything in their contact form. Cool? Yes! Useful? Maybe? Redacted copies have isRedacted set to true to indicate that data has already been redacted. Redactable APIs \u00b6 All Create (Query), Read (Query), Update, and Delete APIs (a.k.a CRUD APIs) provided in this library are Redactable . For example, Query { rawContactsWhere: (account_name LIKE 'test@gmail.com' ESCAPE '\\') AND (account_type LIKE 'com.google' ESCAPE '\\') where: data1 LIKE '%@gmail.com%' ESCAPE '\\' AND mimetype = 'vnd.android.cursor.item/email_v2' isRedacted: false // the rest is omitted for brevity } Query.Result { Number of contacts found: 2 First contact: Contact( id=46, rawContacts=[ RawContact( id=45, contactId=46, addresses=[Address(id=329, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=WORK, label=null, formattedAddress=1200 Park Ave, street=1200 Park Ave, poBox=null, neighborhood=null, city=null, region=null, postcode=null, country=null, isRedacted=false)], emails=[ Email(id=318, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=WORK, label=null, address=buzz.lightyear@pixar.com, isRedacted=false), Email(id=319, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=HOME, label=null, address=buzz@lightyear.net, isRedacted=false) ], events=[ Event(id=317, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=BIRTHDAY, label=null, date=EventDate(year=1995, month=10, dayOfMonth=22, isRedacted=false), isRedacted=false), Event(id=320, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=ANNIVERSARY, label=null, date=EventDate(year=2022, month=0, dayOfMonth=1, isRedacted=false), isRedacted=false) ], // the rest is omitted for brevity ) ] ) isRedacted: false } when redacted, Query { rawContactsWhere: (account_name LIKE '*******************' ESCAPE '\\') AND (account_type LIKE '**********' ESCAPE '\\') where: data1 LIKE '%**********%' ESCAPE '\\' AND mimetype = 'vnd.android.cursor.item/email_v2' isRedacted: true // the rest is omitted for brevity } Query.Result { Number of contacts found: 2 First contact: Contact( id=46, rawContacts=[ RawContact( id=45, contactId=46, addresses=[Address(id=329, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=WORK, label=null, formattedAddress=*************, street=*************, poBox=null, neighborhood=null, city=null, region=null, postcode=null, country=null, isRedacted=true)], emails=[ Email(id=318, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=WORK, label=null, address=************************, isRedacted=true), Email(id=319, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=HOME, label=null, address=******************, isRedacted=true) ], events=[ Event(id=317, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=BIRTHDAY, label=null, date=EventDate(year=null, month=0, dayOfMonth=1, isRedacted=true), isRedacted=true), Event(id=320, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=ANNIVERSARY, label=null, date=EventDate(year=null, month=0, dayOfMonth=1, isRedacted=true), isRedacted=true)], ], // the rest is omitted for brevity ) ] ) isRedacted: true } Insert and update operations on redacted entities \u00b6 This library will not stop you from using redacted copies in insert and update APIs. You could build some cool stuff using it. I'll let your imagination take over from here =) Logging API input and output \u00b6 All terminal API functions such as find and commit can be automatically logged prior and post execution to get visibility on input and output. All log outputs are also redactable! For more info on logging, read Log API input and output . Developer notes (or for advanced users) \u00b6 We cannot prevent users of this API from violating privacy laws if they really want to. BUT, the library should provide consumers an easy way to be GDPR-compliant! This is not necessary for all libraries to implement but this library deals with sensitive, private user data. Therefore, we need to be extra careful and provide consumers a GDPR-compliant way to log everything in this library!","title":"Redact entities and API input and output in production"},{"location":"entities/redact-apis-and-entities/#redact-entities-and-api-input-and-output-in-production","text":"All of the entities and Create (Query), Read (Query), Update, and Delete APIs (a.k.a CRUD APIs) provided in this library are Redactable . Redactables indicates that there could be sensitive private user data that could be redacted, for legal purposes such as upholding GDPR guidelines. If you are logging contact data in production to remote data centers for analytics or crash reporting, then it is important to redact certain parts of every contact's data. \u2139\ufe0f For more info on logging, read Log API input and output .","title":"Redact entities and API input and output in production"},{"location":"entities/redact-apis-and-entities/#disclaimer-this-is-not-legal-advice","text":"This library is written and maintained purely by software developers with no official education or certifications in any facet of law. Please review the redacted outputs of the APIs and entities within this library with your legal team! This library will not be held liable for any privacy violations! With that out of the way, let's move on to the good stuff =)","title":"DISCLAIMER: This is NOT legal advice!"},{"location":"entities/redact-apis-and-entities/#redactable-entities","text":"All Entity in this library are Redactable . For example, Contact: id=1, email { address=\"vestrel00@gmail.com\" }, phone { number=\"(555) 555-5555\" }, etc when redacted, Contact: id=1, email { address=\"*******************\" }, phone { number=\"************\" }, etc Notice that all characters in private user data are replaced with \"*\". Redacted strings are not as useful as the non-redacted counterpart. However, we still have the following valuable information; is the string null or not? how long is the string? Database row IDs (and typically non-string properties) do not have to be redacted unless they contain sensitive information. The redactedCopy function will return an actual copy of the entity, except with sensitive data redacted. In addition to logging, this will allow consumers to do cool things like implementing a redacted contact view! Imagine a button that the user can press to redact everything in their contact form. Cool? Yes! Useful? Maybe? Redacted copies have isRedacted set to true to indicate that data has already been redacted.","title":"Redactable entities"},{"location":"entities/redact-apis-and-entities/#redactable-apis","text":"All Create (Query), Read (Query), Update, and Delete APIs (a.k.a CRUD APIs) provided in this library are Redactable . For example, Query { rawContactsWhere: (account_name LIKE 'test@gmail.com' ESCAPE '\\') AND (account_type LIKE 'com.google' ESCAPE '\\') where: data1 LIKE '%@gmail.com%' ESCAPE '\\' AND mimetype = 'vnd.android.cursor.item/email_v2' isRedacted: false // the rest is omitted for brevity } Query.Result { Number of contacts found: 2 First contact: Contact( id=46, rawContacts=[ RawContact( id=45, contactId=46, addresses=[Address(id=329, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=WORK, label=null, formattedAddress=1200 Park Ave, street=1200 Park Ave, poBox=null, neighborhood=null, city=null, region=null, postcode=null, country=null, isRedacted=false)], emails=[ Email(id=318, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=WORK, label=null, address=buzz.lightyear@pixar.com, isRedacted=false), Email(id=319, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=HOME, label=null, address=buzz@lightyear.net, isRedacted=false) ], events=[ Event(id=317, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=BIRTHDAY, label=null, date=EventDate(year=1995, month=10, dayOfMonth=22, isRedacted=false), isRedacted=false), Event(id=320, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=ANNIVERSARY, label=null, date=EventDate(year=2022, month=0, dayOfMonth=1, isRedacted=false), isRedacted=false) ], // the rest is omitted for brevity ) ] ) isRedacted: false } when redacted, Query { rawContactsWhere: (account_name LIKE '*******************' ESCAPE '\\') AND (account_type LIKE '**********' ESCAPE '\\') where: data1 LIKE '%**********%' ESCAPE '\\' AND mimetype = 'vnd.android.cursor.item/email_v2' isRedacted: true // the rest is omitted for brevity } Query.Result { Number of contacts found: 2 First contact: Contact( id=46, rawContacts=[ RawContact( id=45, contactId=46, addresses=[Address(id=329, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=WORK, label=null, formattedAddress=*************, street=*************, poBox=null, neighborhood=null, city=null, region=null, postcode=null, country=null, isRedacted=true)], emails=[ Email(id=318, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=WORK, label=null, address=************************, isRedacted=true), Email(id=319, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=HOME, label=null, address=******************, isRedacted=true) ], events=[ Event(id=317, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=BIRTHDAY, label=null, date=EventDate(year=null, month=0, dayOfMonth=1, isRedacted=true), isRedacted=true), Event(id=320, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=ANNIVERSARY, label=null, date=EventDate(year=null, month=0, dayOfMonth=1, isRedacted=true), isRedacted=true)], ], // the rest is omitted for brevity ) ] ) isRedacted: true }","title":"Redactable APIs"},{"location":"entities/redact-apis-and-entities/#insert-and-update-operations-on-redacted-entities","text":"This library will not stop you from using redacted copies in insert and update APIs. You could build some cool stuff using it. I'll let your imagination take over from here =)","title":"Insert and update operations on redacted entities"},{"location":"entities/redact-apis-and-entities/#logging-api-input-and-output","text":"All terminal API functions such as find and commit can be automatically logged prior and post execution to get visibility on input and output. All log outputs are also redactable! For more info on logging, read Log API input and output .","title":"Logging API input and output"},{"location":"entities/redact-apis-and-entities/#developer-notes-or-for-advanced-users","text":"We cannot prevent users of this API from violating privacy laws if they really want to. BUT, the library should provide consumers an easy way to be GDPR-compliant! This is not necessary for all libraries to implement but this library deals with sensitive, private user data. Therefore, we need to be extra careful and provide consumers a GDPR-compliant way to log everything in this library!","title":"Developer notes (or for advanced users)"},{"location":"entities/sync-contact-data/","text":"Sync contact data across devices \u00b6 Syncing contact data, including groups, are done automatically by the Contacts Provider depending on the account sync settings. You can typically find these account sync settings via Settings -> Accounts -> -> Account sync -> \"Contacts\" . Of course, in addition to having Contacts syncing enabled in settings, you must also have network connection to sync between the device and remote servers. When you have Contacts syncing enabled, as long as the android.accounts.Account has active sync adapters and remote services and you have network connection, data belonging to that account (e.g. \"vestrel00@gmail.com\" is a Google account) are synced across devices and online. This means that any contacts you create, update, or delete will be synced on all devices and services associated with that account. \u2139\ufe0f Besides Google Accounts, there is also Samsung, Yahoo, MSN/Hotmail, etc. Syncing contacts across devices is possible with sync adapters and Contacts' lookup key. \u2139\ufe0f For more info, read about Contact lookup key vs ID . Adding or removing Accounts \u00b6 When an Account is added to the system and Contacts syncing is enabled and there is network connection, the Contacts Provider will automatically fetch all Contacts, RawContacts, Data, and Groups that belong to that Account. Similarly, when an Account is removed from the system though regardless of Contacts syncing enabled or network availability, the Contacts Provider will automatically remove Contacts, RawContacts, Data, and Groups that belong to that Account. Only contacts that are associated with an Account are synced \u00b6 More specifically, RawContacts that are not associated with an Account (local, device-only) are not synced. Syncing is account specific, which is why you must turn on Contact syncing in the system settings. For example, data belonging to a RawContact that is associated with a Google account (e.g. Gmail) will be available anywhere the Google account is used; in any Android or iOS device, a web browser, etc... Data is synced by Google\u2019s sync adapters between devices and their remote servers. Syncing depends on the account sync settings, which can be configured in the system settings app and possibly through some remote configuration. \u2139\ufe0f For more info, read about Local (device-only) contacts . When are changes synced? \u00b6 In general, the Contacts Provider and the registered sync adapters are responsible for triggering sync events as long as Contacts sync is enabled for the Account in the system settings. You can manually trigger a sync through the system sync settings. Some events that will probably trigger a sync are; Getting network connection from a state where there was not network connection (offline -> online). Adding an Account. Removing an Account Until changes are synced, local changes will not take effect. Some examples are; RawContact rows are marked for deletion but remain until synced. Group rows are marked for deletion but remain until synced. New lookup key is not assigned after associating a local RawContact to an Account. Some custom data provided in this library are not synced \u00b6 The Gender , HandleName , Pokemon , RpgStats , and RpgProfession custom data will not be synced because they are not account specific and they have no sync adapters and no remote service to interface with. \u2139\ufe0f For more info, read Integrate custom data . Custom data from other apps may be synced \u00b6 This library does not sync contact data that belongs to other apps and services. For example, Google Contacts , WhatsApp, and other apps define their own set of custom data that their own sync adapters sync with their own remote services, which requires authentication. \u2139\ufe0f For more info, read Integrate custom data from other apps . This library does not provide sync adapters \u00b6 This library does not have any APIs related to syncing. It is considered out of scope of this library as it requires access to remote databases and account-specific data. Let's talk about it though. However, it is good to know how it works if you just want more insight. https://developer.android.com/guide/topics/providers/contacts-provider#SyncAdapters The Contacts Provider is specifically designed for handling synchronization of contacts data between a device and an online service. This allows users to download existing data to a new device and upload existing data to a new account. Synchronization also ensures that users have the latest data at hand, regardless of the source of additions and changes. Another advantage of synchronization is that it makes contacts data available even when the device is not connected to the network. Although you can implement synchronization in a variety of ways, the Android system provides a plug-in synchronization framework that automates the following tasks: Checking network availability. Scheduling and executing synchronization, based on user preferences. Restarting synchronizations that have stopped. To use this framework, you supply a sync adapter plug-in. Each sync adapter is unique to a service and content provider, but can handle multiple account names for the same service. The framework also allows multiple sync adapters for the same service and provider. This library does not provide any sync adapters. Instead, it relies on existing sync adapters to do the syncing. Sync adapters and syncing are really out of scope of this library. Syncing is its own thing that typically happens outside of an application UI. This library is focused on Create, Read, Update, and Delete (CRUD) operations on native and custom data to and from the local database. Syncing the local database to and from a remote database in the background is a totally different story altogether. \u2139\ufe0f For more info, read Integrate custom data .","title":"Sync contact data across devices"},{"location":"entities/sync-contact-data/#sync-contact-data-across-devices","text":"Syncing contact data, including groups, are done automatically by the Contacts Provider depending on the account sync settings. You can typically find these account sync settings via Settings -> Accounts -> -> Account sync -> \"Contacts\" . Of course, in addition to having Contacts syncing enabled in settings, you must also have network connection to sync between the device and remote servers. When you have Contacts syncing enabled, as long as the android.accounts.Account has active sync adapters and remote services and you have network connection, data belonging to that account (e.g. \"vestrel00@gmail.com\" is a Google account) are synced across devices and online. This means that any contacts you create, update, or delete will be synced on all devices and services associated with that account. \u2139\ufe0f Besides Google Accounts, there is also Samsung, Yahoo, MSN/Hotmail, etc. Syncing contacts across devices is possible with sync adapters and Contacts' lookup key. \u2139\ufe0f For more info, read about Contact lookup key vs ID .","title":"Sync contact data across devices"},{"location":"entities/sync-contact-data/#adding-or-removing-accounts","text":"When an Account is added to the system and Contacts syncing is enabled and there is network connection, the Contacts Provider will automatically fetch all Contacts, RawContacts, Data, and Groups that belong to that Account. Similarly, when an Account is removed from the system though regardless of Contacts syncing enabled or network availability, the Contacts Provider will automatically remove Contacts, RawContacts, Data, and Groups that belong to that Account.","title":"Adding or removing Accounts"},{"location":"entities/sync-contact-data/#only-contacts-that-are-associated-with-an-account-are-synced","text":"More specifically, RawContacts that are not associated with an Account (local, device-only) are not synced. Syncing is account specific, which is why you must turn on Contact syncing in the system settings. For example, data belonging to a RawContact that is associated with a Google account (e.g. Gmail) will be available anywhere the Google account is used; in any Android or iOS device, a web browser, etc... Data is synced by Google\u2019s sync adapters between devices and their remote servers. Syncing depends on the account sync settings, which can be configured in the system settings app and possibly through some remote configuration. \u2139\ufe0f For more info, read about Local (device-only) contacts .","title":"Only contacts that are associated with an Account are synced"},{"location":"entities/sync-contact-data/#when-are-changes-synced","text":"In general, the Contacts Provider and the registered sync adapters are responsible for triggering sync events as long as Contacts sync is enabled for the Account in the system settings. You can manually trigger a sync through the system sync settings. Some events that will probably trigger a sync are; Getting network connection from a state where there was not network connection (offline -> online). Adding an Account. Removing an Account Until changes are synced, local changes will not take effect. Some examples are; RawContact rows are marked for deletion but remain until synced. Group rows are marked for deletion but remain until synced. New lookup key is not assigned after associating a local RawContact to an Account.","title":"When are changes synced?"},{"location":"entities/sync-contact-data/#some-custom-data-provided-in-this-library-are-not-synced","text":"The Gender , HandleName , Pokemon , RpgStats , and RpgProfession custom data will not be synced because they are not account specific and they have no sync adapters and no remote service to interface with. \u2139\ufe0f For more info, read Integrate custom data .","title":"Some custom data provided in this library are not synced"},{"location":"entities/sync-contact-data/#custom-data-from-other-apps-may-be-synced","text":"This library does not sync contact data that belongs to other apps and services. For example, Google Contacts , WhatsApp, and other apps define their own set of custom data that their own sync adapters sync with their own remote services, which requires authentication. \u2139\ufe0f For more info, read Integrate custom data from other apps .","title":"Custom data from other apps may be synced"},{"location":"entities/sync-contact-data/#this-library-does-not-provide-sync-adapters","text":"This library does not have any APIs related to syncing. It is considered out of scope of this library as it requires access to remote databases and account-specific data. Let's talk about it though. However, it is good to know how it works if you just want more insight. https://developer.android.com/guide/topics/providers/contacts-provider#SyncAdapters The Contacts Provider is specifically designed for handling synchronization of contacts data between a device and an online service. This allows users to download existing data to a new device and upload existing data to a new account. Synchronization also ensures that users have the latest data at hand, regardless of the source of additions and changes. Another advantage of synchronization is that it makes contacts data available even when the device is not connected to the network. Although you can implement synchronization in a variety of ways, the Android system provides a plug-in synchronization framework that automates the following tasks: Checking network availability. Scheduling and executing synchronization, based on user preferences. Restarting synchronizations that have stopped. To use this framework, you supply a sync adapter plug-in. Each sync adapter is unique to a service and content provider, but can handle multiple account names for the same service. The framework also allows multiple sync adapters for the same service and provider. This library does not provide any sync adapters. Instead, it relies on existing sync adapters to do the syncing. Sync adapters and syncing are really out of scope of this library. Syncing is its own thing that typically happens outside of an application UI. This library is focused on Create, Read, Update, and Delete (CRUD) operations on native and custom data to and from the local database. Syncing the local database to and from a remote database in the background is a totally different story altogether. \u2139\ufe0f For more info, read Integrate custom data .","title":"This library does not provide sync adapters"},{"location":"groups/delete-groups/","text":"Delete groups \u00b6 This library provides the GroupsDelete API that allows you to delete existing Groups. An instance of the GroupsDelete API is obtained by, val delete = Contacts ( context ). groups (). delete () A basic delete \u00b6 To delete a set of existing groups, val deleteResult = Contacts ( context ) . groups () . delete () . groups ( existingGroups ) . commit () Executing the delete \u00b6 To execute the delete, . commit () If you want to delete all given groups in a single atomic transaction, . commitInOneTransaction () The call to commitInOneTransaction will only succeed if ALL given groups are successfully deleted. If one delete fails, the entire operation will fail and everything will be reverted prior to the delete operation. In contrast, commit allows for some deletes to succeed and some to fail. Handling the delete result \u00b6 The commit and commitInOneTransaction functions returns a Result , To check if all deletes succeeded, val allDeletesSuccessful = deleteResult . isSuccessful To check if a particular delete succeeded, val firstDeleteSuccessful = deleteResult . isSuccessful ( group1 ) Performing the delete and result processing asynchronously \u00b6 Deletes are executed when the commit or commitInOneTransaction function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the delete with permission \u00b6 Deletes require the android.permission.WRITE_CONTACTS permissions. If not granted, the delete will do nothing and return a failed result. To perform the delete with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Read-only Groups \u00b6 Groups created by the system are typically read-only. You cannot delete them, even if you try! The Contacts Provider typically have the following system groups (for standard Google Accounts), systemId: Contacts, title: My Contacts systemId: null, title: Starred in Android systemId: Friends, title: Friends systemId: Family, title: Family systemId: Coworkers, title: Coworkers The above list may vary per account. The GroupsDelete API will not attempt to delete a read-only group and will simply result in failure. Group memberships are automatically deleted \u00b6 When a group is deleted, any membership to that group is deleted automatically by the Contacts Provider.","title":"Delete groups"},{"location":"groups/delete-groups/#delete-groups","text":"This library provides the GroupsDelete API that allows you to delete existing Groups. An instance of the GroupsDelete API is obtained by, val delete = Contacts ( context ). groups (). delete ()","title":"Delete groups"},{"location":"groups/delete-groups/#a-basic-delete","text":"To delete a set of existing groups, val deleteResult = Contacts ( context ) . groups () . delete () . groups ( existingGroups ) . commit ()","title":"A basic delete"},{"location":"groups/delete-groups/#executing-the-delete","text":"To execute the delete, . commit () If you want to delete all given groups in a single atomic transaction, . commitInOneTransaction () The call to commitInOneTransaction will only succeed if ALL given groups are successfully deleted. If one delete fails, the entire operation will fail and everything will be reverted prior to the delete operation. In contrast, commit allows for some deletes to succeed and some to fail.","title":"Executing the delete"},{"location":"groups/delete-groups/#handling-the-delete-result","text":"The commit and commitInOneTransaction functions returns a Result , To check if all deletes succeeded, val allDeletesSuccessful = deleteResult . isSuccessful To check if a particular delete succeeded, val firstDeleteSuccessful = deleteResult . isSuccessful ( group1 )","title":"Handling the delete result"},{"location":"groups/delete-groups/#performing-the-delete-and-result-processing-asynchronously","text":"Deletes are executed when the commit or commitInOneTransaction function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the delete and result processing asynchronously"},{"location":"groups/delete-groups/#performing-the-delete-with-permission","text":"Deletes require the android.permission.WRITE_CONTACTS permissions. If not granted, the delete will do nothing and return a failed result. To perform the delete with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the delete with permission"},{"location":"groups/delete-groups/#read-only-groups","text":"Groups created by the system are typically read-only. You cannot delete them, even if you try! The Contacts Provider typically have the following system groups (for standard Google Accounts), systemId: Contacts, title: My Contacts systemId: null, title: Starred in Android systemId: Friends, title: Friends systemId: Family, title: Family systemId: Coworkers, title: Coworkers The above list may vary per account. The GroupsDelete API will not attempt to delete a read-only group and will simply result in failure.","title":"Read-only Groups"},{"location":"groups/delete-groups/#group-memberships-are-automatically-deleted","text":"When a group is deleted, any membership to that group is deleted automatically by the Contacts Provider.","title":"Group memberships are automatically deleted"},{"location":"groups/insert-groups/","text":"Insert groups \u00b6 This library provides the GroupsInsert API that allows you to create/insert groups associated to an Account . An instance of the GroupsInsert API is obtained by, val insert = Contacts ( context ). groups (). insert () A basic insert \u00b6 To create/insert a new group for an Account, val insertResult = Contacts ( context ) . groups () . insert () . group ( title = \"Besties\" , account = Account ( \"john.doe@gmail.com\" , \"com.google\" ) ) . commit () If you need to insert multiple groups, val newGroup1 = NewGroup ( \"Goodies\" , Account ( \"john.doe@gmail.com\" , \"com.google\" )) val newGroup2 = NewGroup ( \"Baddies\" , Account ( \"john.doe@gmail.com\" , \"com.google\" )) val insertResult = Contacts ( context ) . groups () . insert () . groups ( newGroup1 , newGroup2 ) . commit () Groups and Accounts \u00b6 A set of groups exist for each Account. When there are no accounts in the system, there are no groups and inserting groups will fail. The get accounts permission is required here because this API retrieves all available accounts, if any, and does the following; if the account specified is found in the list of accounts returned by the system, then the account is used if the account specified is not found in the list of accounts returned by the system, then the insertion fails for that group if there are no accounts in the system, [commit] does nothing and fails immediately \u2139\ufe0f For more info on the relationship of Groups and Accounts, read Query groups . Groups and duplicate titles \u00b6 The Contacts Provider allows multiple groups with the same title (case-sensitive comparison) belonging to the same account to exist. In older versions of Android, the native Contacts app allows the creation of new groups with existing titles. In newer versions, duplicate titles are not allowed. Therefore, this library does not allow for duplicate titles. Executing the insert \u00b6 To execute the insert, . commit () Handling the insert result \u00b6 The commit function returns a Result . To check if all inserts succeeded, val allInsertsSuccessful = insertResult . isSuccessful To check if a particular insert succeeded, val firstInsertSuccessful = insertResult . isSuccessful ( newGroup1 ) To get the Group IDs of all the newly created Groups, val allGroupIds = insertResult . groupIds To get the Group ID of a particular Group, val secondGroupId = insertResult . groupId ( newGroup2 ) Once you have the Group IDs, you can retrieve the newly created Groups via the GroupsQuery API, val groups = contactsApi . groups () . query () . where { Id `in` allGroupIds } . find () \u2139\ufe0f For more info, read Query groups . Alternatively, you may use the extensions provided in GroupsInsertResult . To get all newly created Groups, val groups = insertResult . groups ( contactsApi ) To get a particular group, val group = insertResult . group ( contactsApi , newGroup1 ) Handling insert failure \u00b6 The insert may fail for a particular group for various reasons, insertResult . failureReason ( newGroup1 ) ?. let { when ( it ) { TITLE_ALREADY_EXIST -> promptUserToPickDifferentTitle () INVALID_ACCOUNT -> promptUserToPickDifferentAccount () UNKNOWN -> showGenericErrorMessage () } } Cancelling the insert \u00b6 To cancel an insert amid execution, . commit { returnTrueIfInsertShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel insert processing as soon as possible. The function is called numerous times during insert processing to check if processing should stop or continue. This gives you the option to cancel the insert. For example, to automatically cancel the insert inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val insertResult = insert . commit { ! isActive } } } Performing the insert and result processing asynchronously \u00b6 Inserts are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the insert with permission \u00b6 Inserts require the android.permission.WRITE_CONTACTS and android.permission.GET_ACCOUNTS permissions. If not granted, the insert will do nothing and return a failed result. To perform the insert with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Insert groups"},{"location":"groups/insert-groups/#insert-groups","text":"This library provides the GroupsInsert API that allows you to create/insert groups associated to an Account . An instance of the GroupsInsert API is obtained by, val insert = Contacts ( context ). groups (). insert ()","title":"Insert groups"},{"location":"groups/insert-groups/#a-basic-insert","text":"To create/insert a new group for an Account, val insertResult = Contacts ( context ) . groups () . insert () . group ( title = \"Besties\" , account = Account ( \"john.doe@gmail.com\" , \"com.google\" ) ) . commit () If you need to insert multiple groups, val newGroup1 = NewGroup ( \"Goodies\" , Account ( \"john.doe@gmail.com\" , \"com.google\" )) val newGroup2 = NewGroup ( \"Baddies\" , Account ( \"john.doe@gmail.com\" , \"com.google\" )) val insertResult = Contacts ( context ) . groups () . insert () . groups ( newGroup1 , newGroup2 ) . commit ()","title":"A basic insert"},{"location":"groups/insert-groups/#groups-and-accounts","text":"A set of groups exist for each Account. When there are no accounts in the system, there are no groups and inserting groups will fail. The get accounts permission is required here because this API retrieves all available accounts, if any, and does the following; if the account specified is found in the list of accounts returned by the system, then the account is used if the account specified is not found in the list of accounts returned by the system, then the insertion fails for that group if there are no accounts in the system, [commit] does nothing and fails immediately \u2139\ufe0f For more info on the relationship of Groups and Accounts, read Query groups .","title":"Groups and Accounts"},{"location":"groups/insert-groups/#groups-and-duplicate-titles","text":"The Contacts Provider allows multiple groups with the same title (case-sensitive comparison) belonging to the same account to exist. In older versions of Android, the native Contacts app allows the creation of new groups with existing titles. In newer versions, duplicate titles are not allowed. Therefore, this library does not allow for duplicate titles.","title":"Groups and duplicate titles"},{"location":"groups/insert-groups/#executing-the-insert","text":"To execute the insert, . commit ()","title":"Executing the insert"},{"location":"groups/insert-groups/#handling-the-insert-result","text":"The commit function returns a Result . To check if all inserts succeeded, val allInsertsSuccessful = insertResult . isSuccessful To check if a particular insert succeeded, val firstInsertSuccessful = insertResult . isSuccessful ( newGroup1 ) To get the Group IDs of all the newly created Groups, val allGroupIds = insertResult . groupIds To get the Group ID of a particular Group, val secondGroupId = insertResult . groupId ( newGroup2 ) Once you have the Group IDs, you can retrieve the newly created Groups via the GroupsQuery API, val groups = contactsApi . groups () . query () . where { Id `in` allGroupIds } . find () \u2139\ufe0f For more info, read Query groups . Alternatively, you may use the extensions provided in GroupsInsertResult . To get all newly created Groups, val groups = insertResult . groups ( contactsApi ) To get a particular group, val group = insertResult . group ( contactsApi , newGroup1 )","title":"Handling the insert result"},{"location":"groups/insert-groups/#handling-insert-failure","text":"The insert may fail for a particular group for various reasons, insertResult . failureReason ( newGroup1 ) ?. let { when ( it ) { TITLE_ALREADY_EXIST -> promptUserToPickDifferentTitle () INVALID_ACCOUNT -> promptUserToPickDifferentAccount () UNKNOWN -> showGenericErrorMessage () } }","title":"Handling insert failure"},{"location":"groups/insert-groups/#cancelling-the-insert","text":"To cancel an insert amid execution, . commit { returnTrueIfInsertShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel insert processing as soon as possible. The function is called numerous times during insert processing to check if processing should stop or continue. This gives you the option to cancel the insert. For example, to automatically cancel the insert inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val insertResult = insert . commit { ! isActive } } }","title":"Cancelling the insert"},{"location":"groups/insert-groups/#performing-the-insert-and-result-processing-asynchronously","text":"Inserts are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the insert and result processing asynchronously"},{"location":"groups/insert-groups/#performing-the-insert-with-permission","text":"Inserts require the android.permission.WRITE_CONTACTS and android.permission.GET_ACCOUNTS permissions. If not granted, the insert will do nothing and return a failed result. To perform the insert with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the insert with permission"},{"location":"groups/query-groups/","text":"Query groups \u00b6 This library provides the GroupsQuery API that allows you to get groups associated with an Account . An instance of the GroupsQuery API is obtained by, val query = Contacts ( context ). groups (). query () A basic query \u00b6 To get all of the groups for all accounts, val groupsFromAllAccounts = Contacts ( context ) . groups () . query () . find () \u2139\ufe0f It is recommended to get sets of groups for a single account at a time to avoid confusion. Specifying Accounts \u00b6 To limit the search to only those Groups associated with one of the given accounts, . accounts ( accounts ) For example, to limit the search to groups belonging to only one account, . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) \u2139\ufe0f For more info, read Query for Accounts . If no accounts are specified (this function is not called or called with no Accounts), then all Groups of all accounts are included in the search. A null Account may NOT be provided here because no group can exist without an account. Groups are inextricably linked to an Account. Ordering \u00b6 To order resulting Groups using one or more fields, . orderBy ( fieldOrder ) For example, to order groups by account name, . orderBy ( GroupsFields . AccountName . asc ()) String comparisons ignores case by default. Each orderBys provides ignoreCase as an optional parameter. Use GroupsFields to construct the orderBys. Limiting and offsetting \u00b6 To limit the amount of groups returned and/or offset (skip) a specified number of groups, . limit ( limit ) . offset ( offset ) For example, to only get a maximum 20 groups, skipping the first 20, . limit ( 20 ) . offset ( 20 ) This is useful for pagination =) \u2139\ufe0f It is recommended to limit the number of groups when querying to increase performance and decrease memory cost. Executing the query \u00b6 To execute the query, . find () Cancelling the query \u00b6 To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val groups = query . find { ! isActive } } } Performing the query asynchronously \u00b6 Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the query with permission \u00b6 Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Using the where function to specify matching criteria \u00b6 Use the contacts.core.GroupsFields combined with the extensions from contacts.core.Where to form WHERE clauses. \u2139\ufe0f This docs page will not provide a tutorial on database where clauses. It assumes that you know the basics. If you don't know the basics, then search for sqlite where clause . For example, to find groups with a specific title, . where { Title equalToIgnoreCase \"friends\" } To get a list of groups by IDs, . where { Id `in` groupIds } Different groups with the same titles \u00b6 Each account will have its own set of system and user-created groups. This means that there may be multiple groups with the same title belonging to different accounts. This is not a bug. This is why it is recommended to only get sets of groups per account, especially if there is more than one account in the system. Groups from more than one account in the same list \u00b6 When you perform a query that returns groups from more than one account, you will get everything in the same GroupsList . This list is just like any other List except it also provides an extra function that allows you to get a sublist with groups belonging only to a particular account. val groupsFromAccount = groupsList . from ( account ) This is equivalent to, val groupsFromAccount = groupsList . filter { it . account == account } It serves more as documentation and hint that you should really not be mixing groups from different accounts in the same list as it could cause confusion. However, if you know what you are doing and you are not confused, then do what you like :D This is also nice for Java users to not have to perform the filtering themselves.","title":"Query groups"},{"location":"groups/query-groups/#query-groups","text":"This library provides the GroupsQuery API that allows you to get groups associated with an Account . An instance of the GroupsQuery API is obtained by, val query = Contacts ( context ). groups (). query ()","title":"Query groups"},{"location":"groups/query-groups/#a-basic-query","text":"To get all of the groups for all accounts, val groupsFromAllAccounts = Contacts ( context ) . groups () . query () . find () \u2139\ufe0f It is recommended to get sets of groups for a single account at a time to avoid confusion.","title":"A basic query"},{"location":"groups/query-groups/#specifying-accounts","text":"To limit the search to only those Groups associated with one of the given accounts, . accounts ( accounts ) For example, to limit the search to groups belonging to only one account, . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) \u2139\ufe0f For more info, read Query for Accounts . If no accounts are specified (this function is not called or called with no Accounts), then all Groups of all accounts are included in the search. A null Account may NOT be provided here because no group can exist without an account. Groups are inextricably linked to an Account.","title":"Specifying Accounts"},{"location":"groups/query-groups/#ordering","text":"To order resulting Groups using one or more fields, . orderBy ( fieldOrder ) For example, to order groups by account name, . orderBy ( GroupsFields . AccountName . asc ()) String comparisons ignores case by default. Each orderBys provides ignoreCase as an optional parameter. Use GroupsFields to construct the orderBys.","title":"Ordering"},{"location":"groups/query-groups/#limiting-and-offsetting","text":"To limit the amount of groups returned and/or offset (skip) a specified number of groups, . limit ( limit ) . offset ( offset ) For example, to only get a maximum 20 groups, skipping the first 20, . limit ( 20 ) . offset ( 20 ) This is useful for pagination =) \u2139\ufe0f It is recommended to limit the number of groups when querying to increase performance and decrease memory cost.","title":"Limiting and offsetting"},{"location":"groups/query-groups/#executing-the-query","text":"To execute the query, . find ()","title":"Executing the query"},{"location":"groups/query-groups/#cancelling-the-query","text":"To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val groups = query . find { ! isActive } } }","title":"Cancelling the query"},{"location":"groups/query-groups/#performing-the-query-asynchronously","text":"Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the query asynchronously"},{"location":"groups/query-groups/#performing-the-query-with-permission","text":"Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the query with permission"},{"location":"groups/query-groups/#using-the-where-function-to-specify-matching-criteria","text":"Use the contacts.core.GroupsFields combined with the extensions from contacts.core.Where to form WHERE clauses. \u2139\ufe0f This docs page will not provide a tutorial on database where clauses. It assumes that you know the basics. If you don't know the basics, then search for sqlite where clause . For example, to find groups with a specific title, . where { Title equalToIgnoreCase \"friends\" } To get a list of groups by IDs, . where { Id `in` groupIds }","title":"Using the where function to specify matching criteria"},{"location":"groups/query-groups/#different-groups-with-the-same-titles","text":"Each account will have its own set of system and user-created groups. This means that there may be multiple groups with the same title belonging to different accounts. This is not a bug. This is why it is recommended to only get sets of groups per account, especially if there is more than one account in the system.","title":"Different groups with the same titles"},{"location":"groups/query-groups/#groups-from-more-than-one-account-in-the-same-list","text":"When you perform a query that returns groups from more than one account, you will get everything in the same GroupsList . This list is just like any other List except it also provides an extra function that allows you to get a sublist with groups belonging only to a particular account. val groupsFromAccount = groupsList . from ( account ) This is equivalent to, val groupsFromAccount = groupsList . filter { it . account == account } It serves more as documentation and hint that you should really not be mixing groups from different accounts in the same list as it could cause confusion. However, if you know what you are doing and you are not confused, then do what you like :D This is also nice for Java users to not have to perform the filtering themselves.","title":"Groups from more than one account in the same list"},{"location":"groups/update-groups/","text":"Update groups \u00b6 This library provides the GroupsUpdate API that allows you to update existing Groups. An instance of the GroupsUpdate API is obtained by, val update = Contacts ( context ). groups (). update () A basic update \u00b6 To update an existing group, val updateResult = Contacts ( context ) . groups () . update () . groups ( existingGroup ?. mutableCopy { title = \"Best Friends\" }) . commit () If you need to update multiple groups, val mutableGroup1 = group1 . mutableCopy { ... } val mutableGroup2 = group2 . mutableCopy { ... } val updateResult = Contacts ( context ) . groups () . update () . groups ( mutableGroup1 , mutableGroup2 ) . commit () Read-only Groups \u00b6 Groups created by the system are typically read-only. You cannot modify them, even if you try! The Contacts Provider typically have the following system groups (for standard Google Accounts), systemId: Contacts, title: My Contacts systemId: null, title: Starred in Android systemId: Friends, title: Friends systemId: Family, title: Family systemId: Coworkers, title: Coworkers The above list may vary per account. To prevent attempting to modify/update read-only groups, the Group.mutableCopy() function will return null if the group is read-only. \u2139\ufe0f You can try and hack your way around this limitation that this library imposes but you will still not be able to change read-only groups. This library is just trying to save you the pain and suffering caused by trying and failing XD Groups and duplicate titles \u00b6 The Contacts Provider allows multiple groups with the same title (case-sensitive comparison) belonging to the same account to exist. In older versions of Android, the native Contacts app allows the creation of new groups with existing titles. In newer versions, duplicate titles are not allowed. Therefore, this library does not allow for duplicate titles. Executing the update \u00b6 To execute the update, . commit () Handling the update result \u00b6 The commit function returns a Result . To check if all updates succeeded, val allUpdatesSuccessful = updateResult . isSuccessful To check if a particular update succeeded, val firstUpdateSuccessful = updateResult . isSuccessful ( mutableGroup1 ) Handling update failure \u00b6 The update may fail for a particular group for various reasons, updateResult . failureReason ( mutableGroup1 ) ?. let { when ( it ) { TITLE_ALREADY_EXIST -> promptUserToPickDifferentTitle () UNKNOWN -> showGenericErrorMessage () } } Cancelling the update \u00b6 To cancel an update amid execution, . commit { returnTrueIfUpdateShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel update processing as soon as possible. The function is called numerous times during update processing to check if processing should stop or continue. This gives you the option to cancel the update. For example, to automatically cancel the update inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val updateResult = update . commit { ! isActive } } } Performing the update and result processing asynchronously \u00b6 Updates are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the update with permission \u00b6 Updates require the android.permission.WRITE_CONTACTS . If not granted, the update will do nothing and return a failed result. To perform the update with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Starred in Android (Favorites) \u00b6 When a Contact is starred, the Contacts Provider automatically adds a group membership to the favorites group for all RawContacts linked to the Contact. Setting the Contact starred to false removes all group memberships to the favorites group. For more info, read Get set Contact options .","title":"Update groups"},{"location":"groups/update-groups/#update-groups","text":"This library provides the GroupsUpdate API that allows you to update existing Groups. An instance of the GroupsUpdate API is obtained by, val update = Contacts ( context ). groups (). update ()","title":"Update groups"},{"location":"groups/update-groups/#a-basic-update","text":"To update an existing group, val updateResult = Contacts ( context ) . groups () . update () . groups ( existingGroup ?. mutableCopy { title = \"Best Friends\" }) . commit () If you need to update multiple groups, val mutableGroup1 = group1 . mutableCopy { ... } val mutableGroup2 = group2 . mutableCopy { ... } val updateResult = Contacts ( context ) . groups () . update () . groups ( mutableGroup1 , mutableGroup2 ) . commit ()","title":"A basic update"},{"location":"groups/update-groups/#read-only-groups","text":"Groups created by the system are typically read-only. You cannot modify them, even if you try! The Contacts Provider typically have the following system groups (for standard Google Accounts), systemId: Contacts, title: My Contacts systemId: null, title: Starred in Android systemId: Friends, title: Friends systemId: Family, title: Family systemId: Coworkers, title: Coworkers The above list may vary per account. To prevent attempting to modify/update read-only groups, the Group.mutableCopy() function will return null if the group is read-only. \u2139\ufe0f You can try and hack your way around this limitation that this library imposes but you will still not be able to change read-only groups. This library is just trying to save you the pain and suffering caused by trying and failing XD","title":"Read-only Groups"},{"location":"groups/update-groups/#groups-and-duplicate-titles","text":"The Contacts Provider allows multiple groups with the same title (case-sensitive comparison) belonging to the same account to exist. In older versions of Android, the native Contacts app allows the creation of new groups with existing titles. In newer versions, duplicate titles are not allowed. Therefore, this library does not allow for duplicate titles.","title":"Groups and duplicate titles"},{"location":"groups/update-groups/#executing-the-update","text":"To execute the update, . commit ()","title":"Executing the update"},{"location":"groups/update-groups/#handling-the-update-result","text":"The commit function returns a Result . To check if all updates succeeded, val allUpdatesSuccessful = updateResult . isSuccessful To check if a particular update succeeded, val firstUpdateSuccessful = updateResult . isSuccessful ( mutableGroup1 )","title":"Handling the update result"},{"location":"groups/update-groups/#handling-update-failure","text":"The update may fail for a particular group for various reasons, updateResult . failureReason ( mutableGroup1 ) ?. let { when ( it ) { TITLE_ALREADY_EXIST -> promptUserToPickDifferentTitle () UNKNOWN -> showGenericErrorMessage () } }","title":"Handling update failure"},{"location":"groups/update-groups/#cancelling-the-update","text":"To cancel an update amid execution, . commit { returnTrueIfUpdateShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel update processing as soon as possible. The function is called numerous times during update processing to check if processing should stop or continue. This gives you the option to cancel the update. For example, to automatically cancel the update inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val updateResult = update . commit { ! isActive } } }","title":"Cancelling the update"},{"location":"groups/update-groups/#performing-the-update-and-result-processing-asynchronously","text":"Updates are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the update and result processing asynchronously"},{"location":"groups/update-groups/#performing-the-update-with-permission","text":"Updates require the android.permission.WRITE_CONTACTS . If not granted, the update will do nothing and return a failed result. To perform the update with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the update with permission"},{"location":"groups/update-groups/#starred-in-android-favorites","text":"When a Contact is starred, the Contacts Provider automatically adds a group membership to the favorites group for all RawContacts linked to the Contact. Setting the Contact starred to false removes all group memberships to the favorites group. For more info, read Get set Contact options .","title":"Starred in Android (Favorites)"},{"location":"logging/log-api-input-output/","text":"Log API input and output \u00b6 By default the all APIs provided in this library does not log anything at all. To enable logging all API input/output using the android.util.Log , specify the Logger when constructing an instance of Contacts ; val contactsApi = Contacts ( context , logger = AndroidLogger () ) \u2139\ufe0f For more info on Contacts API setup, read Contacts API Setup . Invoking the find or commit functions in query, insert, update, and delete APIs will result in the following output in the Logcat, Query { rawContactsWhere: (account_name LIKE 'test@gmail.com' ESCAPE '\\') AND (account_type LIKE 'com.google' ESCAPE '\\') where: data1 LIKE '%@gmail.com%' ESCAPE '\\' AND mimetype = 'vnd.android.cursor.item/email_v2' isRedacted: false // the rest is omitted for brevity } Query.Result { Number of contacts found: 2 First contact: Contact( id=46, rawContacts=[ RawContact( id=45, contactId=46, addresses=[Address(id=329, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=WORK, label=null, formattedAddress=1200 Park Ave, street=1200 Park Ave, poBox=null, neighborhood=null, city=null, region=null, postcode=null, country=null, isRedacted=false)], emails=[ Email(id=318, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=WORK, label=null, address=buzz.lightyear@pixar.com, isRedacted=false), Email(id=319, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=HOME, label=null, address=buzz@lightyear.net, isRedacted=false) ], events=[ Event(id=317, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=BIRTHDAY, label=null, date=EventDate(year=1995, month=10, dayOfMonth=22, isRedacted=false), isRedacted=false), Event(id=320, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=ANNIVERSARY, label=null, date=EventDate(year=2022, month=0, dayOfMonth=1, isRedacted=false), isRedacted=false) ], // the rest is omitted for brevity ) ] ) isRedacted: false } This is very useful during development. If you have any issues with the library, maintainers will most likely ask you for these logs to help debug your issues. Custom loggers \u00b6 The library provides the AndroidLogger . However, if you want to use your own logging/tracking functions, you may create your own logger by providing an implementation of Logger . For example, to use Timber instead of android.util.Log , class TimberLogger : Logger { override val redactMessages : Boolean = true override fun log ( message : String ) { Timber . d ( message ) } } val contactsApi = Contacts ( context , logger = AndroidLogger () ) Redacting log messages \u00b6 The messages that are logged may contain private user data (contact data). Depending on how you log these messages in production, you may end up violating privacy laws such as GDPR guidelines. To ensure that you are not violating any privacy laws in your production apps when using this library, make sure to set Logger.redactMessages to true . val contactsApi = Contacts ( context , logger = AndroidLogger ( redactMessages = true ) ) Redacted messages are not as useful when debugging so you should set it to false during development. A common way to redact messages in release builds but not debug builds is to, AndroidLogger ( redactMessages = ! BuildConfig . DEBUG ) For more info on redaction, read Redact entities and API input and output in production .","title":"Log API input and output"},{"location":"logging/log-api-input-output/#log-api-input-and-output","text":"By default the all APIs provided in this library does not log anything at all. To enable logging all API input/output using the android.util.Log , specify the Logger when constructing an instance of Contacts ; val contactsApi = Contacts ( context , logger = AndroidLogger () ) \u2139\ufe0f For more info on Contacts API setup, read Contacts API Setup . Invoking the find or commit functions in query, insert, update, and delete APIs will result in the following output in the Logcat, Query { rawContactsWhere: (account_name LIKE 'test@gmail.com' ESCAPE '\\') AND (account_type LIKE 'com.google' ESCAPE '\\') where: data1 LIKE '%@gmail.com%' ESCAPE '\\' AND mimetype = 'vnd.android.cursor.item/email_v2' isRedacted: false // the rest is omitted for brevity } Query.Result { Number of contacts found: 2 First contact: Contact( id=46, rawContacts=[ RawContact( id=45, contactId=46, addresses=[Address(id=329, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=WORK, label=null, formattedAddress=1200 Park Ave, street=1200 Park Ave, poBox=null, neighborhood=null, city=null, region=null, postcode=null, country=null, isRedacted=false)], emails=[ Email(id=318, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=WORK, label=null, address=buzz.lightyear@pixar.com, isRedacted=false), Email(id=319, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=HOME, label=null, address=buzz@lightyear.net, isRedacted=false) ], events=[ Event(id=317, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=BIRTHDAY, label=null, date=EventDate(year=1995, month=10, dayOfMonth=22, isRedacted=false), isRedacted=false), Event(id=320, rawContactId=45, contactId=46, isPrimary=false, isSuperPrimary=false, type=ANNIVERSARY, label=null, date=EventDate(year=2022, month=0, dayOfMonth=1, isRedacted=false), isRedacted=false) ], // the rest is omitted for brevity ) ] ) isRedacted: false } This is very useful during development. If you have any issues with the library, maintainers will most likely ask you for these logs to help debug your issues.","title":"Log API input and output"},{"location":"logging/log-api-input-output/#custom-loggers","text":"The library provides the AndroidLogger . However, if you want to use your own logging/tracking functions, you may create your own logger by providing an implementation of Logger . For example, to use Timber instead of android.util.Log , class TimberLogger : Logger { override val redactMessages : Boolean = true override fun log ( message : String ) { Timber . d ( message ) } } val contactsApi = Contacts ( context , logger = AndroidLogger () )","title":"Custom loggers"},{"location":"logging/log-api-input-output/#redacting-log-messages","text":"The messages that are logged may contain private user data (contact data). Depending on how you log these messages in production, you may end up violating privacy laws such as GDPR guidelines. To ensure that you are not violating any privacy laws in your production apps when using this library, make sure to set Logger.redactMessages to true . val contactsApi = Contacts ( context , logger = AndroidLogger ( redactMessages = true ) ) Redacted messages are not as useful when debugging so you should set it to false during development. A common way to redact messages in release builds but not debug builds is to, AndroidLogger ( redactMessages = ! BuildConfig . DEBUG ) For more info on redaction, read Redact entities and API input and output in production .","title":"Redacting log messages"},{"location":"other/convenience-functions/","text":"Convenience functions \u00b6 This library provides some nice-to-have extensions in the contacts.core.utils package. I will be going over some of them in this page. \u2139\ufe0f Functions in the util package that are used directly by other APIs such as result APIs are not discussed here. Contact data getter and setters \u00b6 Contacts can be made up of one or more RawContacts. In the case that a Contact has two or more RawContacts, getting/setting RawContact data may be a bit of a hassle, requiring loops or iterators, // get all emails from all RawContacts belonging to the Contact val contactEmails = contact . rawContacts . flatMap { it . emails } // add an email to the first RawContact belonging to the Contact contact . mutableCopy (). rawContacts . first (). emails . add ( NewEmail ()) \u2139\ufe0f For more info, read about API Entities . To simplify things, getter/setter extensions are provided in the ContactData.kt file, // get all emails from all RawContacts belonging to the Contact val contactEmailSequence = contact . emails () val contactEmailList = contact . emailList () // add an email to the first RawContact belonging to the Contact contact . mutableCopy (). addEmail ( NewEmail ()) \u2139\ufe0f Newer versions of the Android Open Source Project Contacts app and the Google Contacts app shows data coming from all RawContacts in a Contact details screen. However, they only allow editing a single RawContact and not the aggregate Contact in a single screen to avoid confusion. With this in mind, feel free to use the getter extensions but be very careful with using the setters! Mutable and New RawContact data setters \u00b6 Getting data from RawContacts is straightforward. You have direct access to their properties. The same goes for setting data. val rawContactEmails = rawContact . emails rawContact . mutableCopy (). addEmail ( NewEmail (). apply { address = \"abc@alphabet.com\" type = EmailEntity . Type . WORK } ) Still, there are some setter extensions provided in MutableRawContactData.kt and NewRawContactData.kt that can add some sugar to your syntax. rawContact . mutableCopy (). addEmail { address = \"abc@alphabet.com\" type = EmailEntity . Type . WORK } The setter functions in this section and in the \"Contact data getter and setters\" section also uphold the redacted state of the mutable Contact/RawContact. We setting or adding a property using these extensions, the property being passed will be redacted if the Contact/RawContact it is being added to is redacted. \u2139\ufe0f For more info, read Redact entities and API input and output in production . Getting the parent Contact of a RawContact or Data \u00b6 Using the Query API, it is easy to get the parent Contact of a RawContact or Data, val contactOfRawContact = contactsApi . query (). where { Contact . Id equalTo rawContact . contactId }. find (). firstOrNull () val contactOfData = contactsApi . query (). where { Contact . Id equalTo data . contactId }. find (). firstOrNull () \u2139\ufe0f For more info, read Query contacts (advanced) . To shorten things, you can use the extensions in RawContactContact.kt and DataContact.kt , val contactOfRawContact = rawContact . contact ( contactsApi ) val contactOfData = data . contact ( contactsApi ) On a similar note, to get the parent RawContact of a Data using the extensions in DataRawContact.kt , val rawContactOfData = data . rawContact ( contactsApi ) These are blocking calls so you might want to do them outside the UI thread. \u2139\ufe0f For more info, read Execute work outside of the UI thread using coroutines . Refresh Contact, RawContact, and Data references \u00b6 In-memory references to these entities could become inaccurate due to changes in the database that could occur in your app, other apps, or by the Contacts Provider. If you need to get the most up-to-date reference of an entity from the database, you could do it using the Query and DataQuery APIs, val contactFromDb = contactsApi . query (). where { Contact . Id equalTo contactInMemory . id }. find (). firstOrNull () val rawContactFromDb = contactsApi . query (). where { RawContact . Id equalTo rawContactInMemory . id }. find () . firstOrNull () ?. rawContacts ?. find { it . id == rawContactInMemory . id } val dataFromDb = contactsApi . data (). query (). where { DataId equalTo dataInMemory . id }. find (). firstOrNull () To shorten things, you can use extensions in ContactRefresh.kt , RawContactRefresh.kt , and DataRefresh.kt , val contactFromDb = contactInMemory . refresh ( contactsApi ) val rawContactFromDb = rawContactInMemory . refresh ( contactsApi ) val dataFromDb = dataInMemory . refresh ( contactsApi ) These are blocking calls so you might want to do them outside the UI thread. \u2139\ufe0f For more info, read Execute work outside of the UI thread using coroutines . Sort Contacts by data fields \u00b6 The Query and BroadQuery APIs allows you to sort Contacts based on fields in the Contacts table such as Id and DisplayNamePrimary , val sortedContacts = query . orderBy ( ContactsFields . DisplayNamePrimary . desc ( ignoreCase = true )) If you want to sort Contacts based on data fields (e.g. email), you are unable to use the query APIs provided in this library to do so. However, if you have a list of Contacts in memory, you can use the extensions in ContactsComparator.kt to build a Comparator to use for sorting, val sortedContacts = unsortedContacts . sortedWith ( Fields . Email . Address . desc ( ignoreCase = true ). contactsComparator () ) You can also specify multiple fields for sorting, val sortedContacts = unsortedContacts . sortedWith ( setOf ( Fields . Contact . Options . Starred . desc (), Fields . Contact . DisplayNamePrimary . asc ( ignoreCase = false ), Fields . Email . Address . asc () ). contactsComparator () ) Get the Group of a GroupMembership \u00b6 The GroupsQuery allows you to get groups from a set of group Ids, val group = contactsApi . groups (). query (). where { Id equalTo groupMembership . groupId }. find (). firstOrNull () val groups = contactsApi . groups (). query (). where { Id `in` groupMemberships . map { it . groupId } }. find () To shorten things, you can use the extensions in GroupMembershipGroup.kt , val group = groupMembership . group ( contactsApi ) val groups = groupMemberships . groups ( contactsApi ) These are blocking calls so you might want to do them outside the UI thread. \u2139\ufe0f For more info, read Execute work outside of the UI thread using coroutines . Get the RawContact of a BlankRawContact \u00b6 The Query API allows you to get the RawContact version of a BlankRawContact , val rawContact = contactsApi . query (). where { RawContact . Id equalTo blankRawContact . id }. find () . firstOrNull () ?. rawContacts ?. find { it . id == blankRawContact . id } To shorten things, you can use the extensions in BlankRawContactToRawContact.kt , val rawContact = blankRawContact . toRawContact ( contactsApi ) These are blocking calls so you might want to do them outside the UI thread. \u2139\ufe0f For more info, read Execute work outside of the UI thread using coroutines .","title":"Convenience functions"},{"location":"other/convenience-functions/#convenience-functions","text":"This library provides some nice-to-have extensions in the contacts.core.utils package. I will be going over some of them in this page. \u2139\ufe0f Functions in the util package that are used directly by other APIs such as result APIs are not discussed here.","title":"Convenience functions"},{"location":"other/convenience-functions/#contact-data-getter-and-setters","text":"Contacts can be made up of one or more RawContacts. In the case that a Contact has two or more RawContacts, getting/setting RawContact data may be a bit of a hassle, requiring loops or iterators, // get all emails from all RawContacts belonging to the Contact val contactEmails = contact . rawContacts . flatMap { it . emails } // add an email to the first RawContact belonging to the Contact contact . mutableCopy (). rawContacts . first (). emails . add ( NewEmail ()) \u2139\ufe0f For more info, read about API Entities . To simplify things, getter/setter extensions are provided in the ContactData.kt file, // get all emails from all RawContacts belonging to the Contact val contactEmailSequence = contact . emails () val contactEmailList = contact . emailList () // add an email to the first RawContact belonging to the Contact contact . mutableCopy (). addEmail ( NewEmail ()) \u2139\ufe0f Newer versions of the Android Open Source Project Contacts app and the Google Contacts app shows data coming from all RawContacts in a Contact details screen. However, they only allow editing a single RawContact and not the aggregate Contact in a single screen to avoid confusion. With this in mind, feel free to use the getter extensions but be very careful with using the setters!","title":"Contact data getter and setters"},{"location":"other/convenience-functions/#mutable-and-new-rawcontact-data-setters","text":"Getting data from RawContacts is straightforward. You have direct access to their properties. The same goes for setting data. val rawContactEmails = rawContact . emails rawContact . mutableCopy (). addEmail ( NewEmail (). apply { address = \"abc@alphabet.com\" type = EmailEntity . Type . WORK } ) Still, there are some setter extensions provided in MutableRawContactData.kt and NewRawContactData.kt that can add some sugar to your syntax. rawContact . mutableCopy (). addEmail { address = \"abc@alphabet.com\" type = EmailEntity . Type . WORK } The setter functions in this section and in the \"Contact data getter and setters\" section also uphold the redacted state of the mutable Contact/RawContact. We setting or adding a property using these extensions, the property being passed will be redacted if the Contact/RawContact it is being added to is redacted. \u2139\ufe0f For more info, read Redact entities and API input and output in production .","title":"Mutable and New RawContact data setters"},{"location":"other/convenience-functions/#getting-the-parent-contact-of-a-rawcontact-or-data","text":"Using the Query API, it is easy to get the parent Contact of a RawContact or Data, val contactOfRawContact = contactsApi . query (). where { Contact . Id equalTo rawContact . contactId }. find (). firstOrNull () val contactOfData = contactsApi . query (). where { Contact . Id equalTo data . contactId }. find (). firstOrNull () \u2139\ufe0f For more info, read Query contacts (advanced) . To shorten things, you can use the extensions in RawContactContact.kt and DataContact.kt , val contactOfRawContact = rawContact . contact ( contactsApi ) val contactOfData = data . contact ( contactsApi ) On a similar note, to get the parent RawContact of a Data using the extensions in DataRawContact.kt , val rawContactOfData = data . rawContact ( contactsApi ) These are blocking calls so you might want to do them outside the UI thread. \u2139\ufe0f For more info, read Execute work outside of the UI thread using coroutines .","title":"Getting the parent Contact of a RawContact or Data"},{"location":"other/convenience-functions/#refresh-contact-rawcontact-and-data-references","text":"In-memory references to these entities could become inaccurate due to changes in the database that could occur in your app, other apps, or by the Contacts Provider. If you need to get the most up-to-date reference of an entity from the database, you could do it using the Query and DataQuery APIs, val contactFromDb = contactsApi . query (). where { Contact . Id equalTo contactInMemory . id }. find (). firstOrNull () val rawContactFromDb = contactsApi . query (). where { RawContact . Id equalTo rawContactInMemory . id }. find () . firstOrNull () ?. rawContacts ?. find { it . id == rawContactInMemory . id } val dataFromDb = contactsApi . data (). query (). where { DataId equalTo dataInMemory . id }. find (). firstOrNull () To shorten things, you can use extensions in ContactRefresh.kt , RawContactRefresh.kt , and DataRefresh.kt , val contactFromDb = contactInMemory . refresh ( contactsApi ) val rawContactFromDb = rawContactInMemory . refresh ( contactsApi ) val dataFromDb = dataInMemory . refresh ( contactsApi ) These are blocking calls so you might want to do them outside the UI thread. \u2139\ufe0f For more info, read Execute work outside of the UI thread using coroutines .","title":"Refresh Contact, RawContact, and Data references"},{"location":"other/convenience-functions/#sort-contacts-by-data-fields","text":"The Query and BroadQuery APIs allows you to sort Contacts based on fields in the Contacts table such as Id and DisplayNamePrimary , val sortedContacts = query . orderBy ( ContactsFields . DisplayNamePrimary . desc ( ignoreCase = true )) If you want to sort Contacts based on data fields (e.g. email), you are unable to use the query APIs provided in this library to do so. However, if you have a list of Contacts in memory, you can use the extensions in ContactsComparator.kt to build a Comparator to use for sorting, val sortedContacts = unsortedContacts . sortedWith ( Fields . Email . Address . desc ( ignoreCase = true ). contactsComparator () ) You can also specify multiple fields for sorting, val sortedContacts = unsortedContacts . sortedWith ( setOf ( Fields . Contact . Options . Starred . desc (), Fields . Contact . DisplayNamePrimary . asc ( ignoreCase = false ), Fields . Email . Address . asc () ). contactsComparator () )","title":"Sort Contacts by data fields"},{"location":"other/convenience-functions/#get-the-group-of-a-groupmembership","text":"The GroupsQuery allows you to get groups from a set of group Ids, val group = contactsApi . groups (). query (). where { Id equalTo groupMembership . groupId }. find (). firstOrNull () val groups = contactsApi . groups (). query (). where { Id `in` groupMemberships . map { it . groupId } }. find () To shorten things, you can use the extensions in GroupMembershipGroup.kt , val group = groupMembership . group ( contactsApi ) val groups = groupMemberships . groups ( contactsApi ) These are blocking calls so you might want to do them outside the UI thread. \u2139\ufe0f For more info, read Execute work outside of the UI thread using coroutines .","title":"Get the Group of a GroupMembership"},{"location":"other/convenience-functions/#get-the-rawcontact-of-a-blankrawcontact","text":"The Query API allows you to get the RawContact version of a BlankRawContact , val rawContact = contactsApi . query (). where { RawContact . Id equalTo blankRawContact . id }. find () . firstOrNull () ?. rawContacts ?. find { it . id == blankRawContact . id } To shorten things, you can use the extensions in BlankRawContactToRawContact.kt , val rawContact = blankRawContact . toRawContact ( contactsApi ) These are blocking calls so you might want to do them outside the UI thread. \u2139\ufe0f For more info, read Execute work outside of the UI thread using coroutines .","title":"Get the RawContact of a BlankRawContact"},{"location":"other/get-set-clear-contact-raw-contact-options/","text":"Get set Contact options \u00b6 This library provides several functions to interact with Contact and RawContact options; starred, send to voicemail, and ringtone. Contact and RawContact options affect each other \u00b6 Changes to the options of the parent Contact will be propagated to all child RawContact options. Changes to the options of a RawContact may or may not affect the options of the parent Contact. Getting contact options \u00b6 To get Contact options, val options = contact . options ( contactsApi ) To get RawContact options, val options = rawContact . options ( contactsApi ) Setting contact options \u00b6 To set Contact options, contact . setOptions ( contactsApi , mutableOptions ) To set RawContact options, rawContact . setOptions ( contactsApi , mutableOptions ) For example, to set a contact to be starred (favorited), contact . setOptions ( contactsApi , mutableOptions . apply { starred = true }) The setOption function takes in an arbitrary Options instance. If you instead want to modify the options of a Contact or RawContact retrieved from the database, contact . updateOptions ( contactsApi ) { starred = true } This is useful if you only want to set certain properties and keep other properties the same. Changes are immediate and are not applied to the receiver \u00b6 These apply to set and update functions. Changes are immediate. These functions will make the changes to the Contacts Provider database immediately. You do not need to use update APIs to commit the changes. Changes are not applied to the receiver. This function call does NOT mutate immutable or mutable receivers. Therefore, you should use query APIs or refresh extensions or process the result of this function call to get the most up-to-date reference to mutable or immutable entity that contains the changes in the Contacts Provider database. Using the ui RingtonePicker extensions \u00b6 The contacts.ui.util.RingtonePicker.kt in the ui module` provides extension functions to make selecting existing ringtones easier. It provides you the same UX as the native Contacts app. To use it, Activity { fun onSelectRingtoneClicked () { selectRingtone ( contact . options ?. customRingtone ) } override fun onActivityResult ( requestCode : Int , resultCode : Int , data : Intent?) { super . onActivityResult ( requestCode , resultCode , data ) onRingtoneSelected ( requestCode , resultCode , data ) { ringtoneUri -> contact . updateOptions ( contactsApi ) { customRingtone = ringtoneUri } } } } Starting with Android 11 (API 30), you must include the following to your manifest in order to successfully use the above functions. Performing options management asynchronously \u00b6 All of the code shown in this guide are done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing options management with permission \u00b6 Getting and setting options require the android.permission.READ_CONTACTS and android.permission.WRITE_CONTACTS permissions respectively. If not granted, getting and setting options will fail. TODO Update this section as part of issue #120 . Starred in Android (Favorites) \u00b6 When a Contact is starred, the Contacts Provider automatically adds a group membership to the favorites group for all RawContacts linked to the Contact. Setting the Contact starred to false removes all group memberships to the favorites group. The Contact's \"starred\" value is interdependent with group memberships to the favorites group. Adding a group membership to the favorites group results in starred being set to true. Removing the membership sets it to false. Raw contacts that are not associated with an account do not have any group memberships. Even though these RawContacts may not have a membership to the favorites group, they may still be \"starred\" (favorited), which is not dependent on the existence of a favorites group membership. Refresh RawContact instances after changing the starred value. Otherwise, performing an update on the RawContact with a stale set of group memberships may revert the star/unstar operation. For example, -> query returns a starred RawContact -> set starred to false -> update RawContact (still containing a group membership to the favorites group) -> starred will be set back to true. FAQs \u00b6 Can contacts be inserted with options? \u00b6 No, you cannot get/set/update options for Contacts/RawContacts that have not yet been inserted in the Contacts Provider database. In other words, only Contacts/RawContacts retrieved via query or result APIs can use the extension functions in contacts.core.util.ContactOptions.kt and contacts.core.util.RawContactOptions.kt . \u2139\ufe0f Issue #120 will change the answer to \"yes\". THe underlying mechanism will not change but the outward public facing API will change. Internally, the insert operation will insert a new row in the Contacts/RawContacts table and then attempt to set the options immediately after. To insert a new contact \"with options\", you should insert the contact first. Then, if the insert succeeds, proceed to set the options. \u2139\ufe0f For more info about insert, read Insert contacts .","title":"Get set Contact options"},{"location":"other/get-set-clear-contact-raw-contact-options/#get-set-contact-options","text":"This library provides several functions to interact with Contact and RawContact options; starred, send to voicemail, and ringtone.","title":"Get set Contact options"},{"location":"other/get-set-clear-contact-raw-contact-options/#contact-and-rawcontact-options-affect-each-other","text":"Changes to the options of the parent Contact will be propagated to all child RawContact options. Changes to the options of a RawContact may or may not affect the options of the parent Contact.","title":"Contact and RawContact options affect each other"},{"location":"other/get-set-clear-contact-raw-contact-options/#getting-contact-options","text":"To get Contact options, val options = contact . options ( contactsApi ) To get RawContact options, val options = rawContact . options ( contactsApi )","title":"Getting contact options"},{"location":"other/get-set-clear-contact-raw-contact-options/#setting-contact-options","text":"To set Contact options, contact . setOptions ( contactsApi , mutableOptions ) To set RawContact options, rawContact . setOptions ( contactsApi , mutableOptions ) For example, to set a contact to be starred (favorited), contact . setOptions ( contactsApi , mutableOptions . apply { starred = true }) The setOption function takes in an arbitrary Options instance. If you instead want to modify the options of a Contact or RawContact retrieved from the database, contact . updateOptions ( contactsApi ) { starred = true } This is useful if you only want to set certain properties and keep other properties the same.","title":"Setting contact options"},{"location":"other/get-set-clear-contact-raw-contact-options/#changes-are-immediate-and-are-not-applied-to-the-receiver","text":"These apply to set and update functions. Changes are immediate. These functions will make the changes to the Contacts Provider database immediately. You do not need to use update APIs to commit the changes. Changes are not applied to the receiver. This function call does NOT mutate immutable or mutable receivers. Therefore, you should use query APIs or refresh extensions or process the result of this function call to get the most up-to-date reference to mutable or immutable entity that contains the changes in the Contacts Provider database.","title":"Changes are immediate and are not applied to the receiver"},{"location":"other/get-set-clear-contact-raw-contact-options/#using-the-ui-ringtonepicker-extensions","text":"The contacts.ui.util.RingtonePicker.kt in the ui module` provides extension functions to make selecting existing ringtones easier. It provides you the same UX as the native Contacts app. To use it, Activity { fun onSelectRingtoneClicked () { selectRingtone ( contact . options ?. customRingtone ) } override fun onActivityResult ( requestCode : Int , resultCode : Int , data : Intent?) { super . onActivityResult ( requestCode , resultCode , data ) onRingtoneSelected ( requestCode , resultCode , data ) { ringtoneUri -> contact . updateOptions ( contactsApi ) { customRingtone = ringtoneUri } } } } Starting with Android 11 (API 30), you must include the following to your manifest in order to successfully use the above functions. ","title":"Using the ui RingtonePicker extensions"},{"location":"other/get-set-clear-contact-raw-contact-options/#performing-options-management-asynchronously","text":"All of the code shown in this guide are done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing options management asynchronously"},{"location":"other/get-set-clear-contact-raw-contact-options/#performing-options-management-with-permission","text":"Getting and setting options require the android.permission.READ_CONTACTS and android.permission.WRITE_CONTACTS permissions respectively. If not granted, getting and setting options will fail. TODO Update this section as part of issue #120 .","title":"Performing options management with permission"},{"location":"other/get-set-clear-contact-raw-contact-options/#starred-in-android-favorites","text":"When a Contact is starred, the Contacts Provider automatically adds a group membership to the favorites group for all RawContacts linked to the Contact. Setting the Contact starred to false removes all group memberships to the favorites group. The Contact's \"starred\" value is interdependent with group memberships to the favorites group. Adding a group membership to the favorites group results in starred being set to true. Removing the membership sets it to false. Raw contacts that are not associated with an account do not have any group memberships. Even though these RawContacts may not have a membership to the favorites group, they may still be \"starred\" (favorited), which is not dependent on the existence of a favorites group membership. Refresh RawContact instances after changing the starred value. Otherwise, performing an update on the RawContact with a stale set of group memberships may revert the star/unstar operation. For example, -> query returns a starred RawContact -> set starred to false -> update RawContact (still containing a group membership to the favorites group) -> starred will be set back to true.","title":"Starred in Android (Favorites)"},{"location":"other/get-set-clear-contact-raw-contact-options/#faqs","text":"","title":"FAQs"},{"location":"other/get-set-clear-contact-raw-contact-options/#can-contacts-be-inserted-with-options","text":"No, you cannot get/set/update options for Contacts/RawContacts that have not yet been inserted in the Contacts Provider database. In other words, only Contacts/RawContacts retrieved via query or result APIs can use the extension functions in contacts.core.util.ContactOptions.kt and contacts.core.util.RawContactOptions.kt . \u2139\ufe0f Issue #120 will change the answer to \"yes\". THe underlying mechanism will not change but the outward public facing API will change. Internally, the insert operation will insert a new row in the Contacts/RawContacts table and then attempt to set the options immediately after. To insert a new contact \"with options\", you should insert the contact first. Then, if the insert succeeds, proceed to set the options. \u2139\ufe0f For more info about insert, read Insert contacts .","title":"Can contacts be inserted with options?"},{"location":"other/get-set-clear-default-data/","text":"Get set clear default Contact data \u00b6 Default contact data are instances of common data kinds that are marked as the default. The two most common data kinds that use this mechanism are emails and phones. In the native Contacts app Contact details activity, long pressing an email or phone shows a popup menu with an option to set it as default. When a particular email or phone is set as default, sending an email and making a phone call to that contact will use that default email and phone respectively. \u2139\ufe0f For more info on the common data kinds, read about API Entities . Getting default data \u00b6 To get the default Contact email and phone from all RawContacts, val defaultContactEmail : Email? = contact . emails (). default () val defaultContactPhone : Phone? = contact . phones (). default () To get the default RawContact email and phone, val defaultRawContactEmail : Email? = rawContact . emails . default () val defaultRawContactPhone : Phone? = rawContact . phones . default () To get the first default data out of a generic list of data, val defaultData = dataList . default () Note that the most common use of defaults is with Contacts, not RawContacts. You typically do not need to worry about defaults at a RawContact level. Setting default data \u00b6 To set a particular data as the default for the set of data of the same type (e.g. email) for the aggregate Contact, email . setAsDefault ( contactsApi ) If a default data of the same type for the aggregate Contact already exist before this call, then it will no longer be the default. For example, these emails belong to the same aggregate Contact; x@x.com (default) y@y.com z@z.com Calling this function on a non-default data (e.g. y@y.com) will remove the default status for data that was previously set as the default. This data will then be set as the default. This results in; x@x.com y@y.com (default) z@z.com Clearing default data \u00b6 To remove the default status of any data of the same type (e.g. email), if any, for the aggregate Contact, email . clearDefault ( contactsApi ) For example, these emails belong to the same aggregate Contact; x@x.com y@y.com (default) z@z.com Calling this function on any data of the same kind for the aggregate contact (default or not) will remove the default status on all data of the same kind for the aggregate Contact. This results in; x@x.com y@y.com z@z.com Changes are immediate and are not applied to the receiver \u00b6 These apply to set and clear functions. Changes are immediate. These functions will make the changes to the Contacts Provider database immediately. You do not need to use update APIs to commit the changes. Changes are not applied to the receiver. This function call does NOT mutate immutable or mutable receivers. Therefore, you should use query APIs or refresh extensions or process the result of this function call to get the most up-to-date reference to mutable or immutable entity that contains the changes in the Contacts Provider database. Performing default data management asynchronously \u00b6 Setting or clearing default data is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing default data management with permission \u00b6 Getting and setting/clearing default data require the android.permission.READ_CONTACTS and android.permission.WRITE_CONTACTS permissions respectively. If not granted, getting and setting/clearing default data will fail. Developer notes (or for advanced users) \u00b6 As per documentation, for a set of data rows with the same mimetype (e.g. a set of emails), there should only be one primary data row (e.g. email) per RawContact and one super primary data row per Contact. Furthermore, a data row that is super primary must also be primary. Unfortunately, the Contacts Provider does not do any data set validation for the Data columns IS_PRIMARY and IS_SUPER_PRIMARY . This means that it is possible to set more than one data row of the same mimetype as primary for the same RawContact and super primary for the same aggregate Contact. It is also possible to set a data row as super primary but not primary. Upholding the the contract is left to the us (the library). For example, given this relationship; Contact RawContact X Email A Email B RawContact Y Email C Email D When Emails A, B, C, and D are inserted with the RawContacts or after the RawContacts have been created, we get the following state; Email Primary Super Primary A 0 0 B 0 0 C 0 0 D 0 0 The state does not change when RawContact X is linked with RawContact Y. After setting Email A as the \"default\" email, it becomes primary and super primary; Email Primary Super Primary A 1 1 B 0 0 C 0 0 D 0 0 Then setting Email B as the default email, it becomes primary and super primary. Email A is no longer primary or super primary. Email Primary Super Primary A 0 0 B 1 1 C 0 0 D 0 0 Then setting Email C as the default email, it becomes primary and super primary. Email B is still primary because it belongs to a different RawContact than Email C. However, Email B is no longer the super primary as there can only be one per aggregate Contact. Email Primary Super Primary A 0 0 B 1 0 C 1 1 D 0 0 Then setting Email D as the default email, it becomes primary and super primary. Email C is no longer primary or super primary. Email Primary Super Primary A 0 0 B 1 0 C 0 0 D 1 1 Then clearing the default email D, removes its primary and super primary status. However, email B remains a primary but not a super primary. Email Primary Super Primary A 0 0 B 1 0 C 0 0 D 0 0 The above behavior is observed from the native Contacts app. The \"super primary\" data of an aggregate Contact is referred to as the \"default\".","title":"Get set clear default Contact data"},{"location":"other/get-set-clear-default-data/#get-set-clear-default-contact-data","text":"Default contact data are instances of common data kinds that are marked as the default. The two most common data kinds that use this mechanism are emails and phones. In the native Contacts app Contact details activity, long pressing an email or phone shows a popup menu with an option to set it as default. When a particular email or phone is set as default, sending an email and making a phone call to that contact will use that default email and phone respectively. \u2139\ufe0f For more info on the common data kinds, read about API Entities .","title":"Get set clear default Contact data"},{"location":"other/get-set-clear-default-data/#getting-default-data","text":"To get the default Contact email and phone from all RawContacts, val defaultContactEmail : Email? = contact . emails (). default () val defaultContactPhone : Phone? = contact . phones (). default () To get the default RawContact email and phone, val defaultRawContactEmail : Email? = rawContact . emails . default () val defaultRawContactPhone : Phone? = rawContact . phones . default () To get the first default data out of a generic list of data, val defaultData = dataList . default () Note that the most common use of defaults is with Contacts, not RawContacts. You typically do not need to worry about defaults at a RawContact level.","title":"Getting default data"},{"location":"other/get-set-clear-default-data/#setting-default-data","text":"To set a particular data as the default for the set of data of the same type (e.g. email) for the aggregate Contact, email . setAsDefault ( contactsApi ) If a default data of the same type for the aggregate Contact already exist before this call, then it will no longer be the default. For example, these emails belong to the same aggregate Contact; x@x.com (default) y@y.com z@z.com Calling this function on a non-default data (e.g. y@y.com) will remove the default status for data that was previously set as the default. This data will then be set as the default. This results in; x@x.com y@y.com (default) z@z.com","title":"Setting default data"},{"location":"other/get-set-clear-default-data/#clearing-default-data","text":"To remove the default status of any data of the same type (e.g. email), if any, for the aggregate Contact, email . clearDefault ( contactsApi ) For example, these emails belong to the same aggregate Contact; x@x.com y@y.com (default) z@z.com Calling this function on any data of the same kind for the aggregate contact (default or not) will remove the default status on all data of the same kind for the aggregate Contact. This results in; x@x.com y@y.com z@z.com","title":"Clearing default data"},{"location":"other/get-set-clear-default-data/#changes-are-immediate-and-are-not-applied-to-the-receiver","text":"These apply to set and clear functions. Changes are immediate. These functions will make the changes to the Contacts Provider database immediately. You do not need to use update APIs to commit the changes. Changes are not applied to the receiver. This function call does NOT mutate immutable or mutable receivers. Therefore, you should use query APIs or refresh extensions or process the result of this function call to get the most up-to-date reference to mutable or immutable entity that contains the changes in the Contacts Provider database.","title":"Changes are immediate and are not applied to the receiver"},{"location":"other/get-set-clear-default-data/#performing-default-data-management-asynchronously","text":"Setting or clearing default data is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing default data management asynchronously"},{"location":"other/get-set-clear-default-data/#performing-default-data-management-with-permission","text":"Getting and setting/clearing default data require the android.permission.READ_CONTACTS and android.permission.WRITE_CONTACTS permissions respectively. If not granted, getting and setting/clearing default data will fail.","title":"Performing default data management with permission"},{"location":"other/get-set-clear-default-data/#developer-notes-or-for-advanced-users","text":"As per documentation, for a set of data rows with the same mimetype (e.g. a set of emails), there should only be one primary data row (e.g. email) per RawContact and one super primary data row per Contact. Furthermore, a data row that is super primary must also be primary. Unfortunately, the Contacts Provider does not do any data set validation for the Data columns IS_PRIMARY and IS_SUPER_PRIMARY . This means that it is possible to set more than one data row of the same mimetype as primary for the same RawContact and super primary for the same aggregate Contact. It is also possible to set a data row as super primary but not primary. Upholding the the contract is left to the us (the library). For example, given this relationship; Contact RawContact X Email A Email B RawContact Y Email C Email D When Emails A, B, C, and D are inserted with the RawContacts or after the RawContacts have been created, we get the following state; Email Primary Super Primary A 0 0 B 0 0 C 0 0 D 0 0 The state does not change when RawContact X is linked with RawContact Y. After setting Email A as the \"default\" email, it becomes primary and super primary; Email Primary Super Primary A 1 1 B 0 0 C 0 0 D 0 0 Then setting Email B as the default email, it becomes primary and super primary. Email A is no longer primary or super primary. Email Primary Super Primary A 0 0 B 1 1 C 0 0 D 0 0 Then setting Email C as the default email, it becomes primary and super primary. Email B is still primary because it belongs to a different RawContact than Email C. However, Email B is no longer the super primary as there can only be one per aggregate Contact. Email Primary Super Primary A 0 0 B 1 0 C 1 1 D 0 0 Then setting Email D as the default email, it becomes primary and super primary. Email C is no longer primary or super primary. Email Primary Super Primary A 0 0 B 1 0 C 0 0 D 1 1 Then clearing the default email D, removes its primary and super primary status. However, email B remains a primary but not a super primary. Email Primary Super Primary A 0 0 B 1 0 C 0 0 D 0 0 The above behavior is observed from the native Contacts app. The \"super primary\" data of an aggregate Contact is referred to as the \"default\".","title":"Developer notes (or for advanced users)"},{"location":"other/get-set-remove-contact-raw-contact-photo/","text":"Get set remove full-sized and thumbnail contact photos \u00b6 This library provides several functions to interact with Contact and RawContact full-sized and thumbnail photos. Contact and RawContact photos \u00b6 The photo assigned to a Contact is just a reference to a photo assigned to a RawContact. If a Contact consists of more than one RawContact, only the photo from one of the RawContacts will be used by the Contact. Setting/removing the (main) RawContact's photo will in turn change the Contact photo because the Contact photo is just a reference to the RawContact photo. The inverse is also true. RawContact photos are retained when linking and unlinking. \u2139\ufe0f For more info, read Link unlink Contacts . Full-sized photos and thumbnails \u00b6 Each RawContact may be assigned one photo. The thumbnail is just a downsized version of the full-sized photo. The full-sized photo is typically displayed in a large view, such as in a contact detail screen. The thumbnail is typically displayed in small views, such as in a contacts list view. Setting the full-sized photo will automatically set the thumbnail. The Contacts Provider automatically creates a downsized version of the full-sized photo. Getting contact photo \u00b6 There are several ways to do this. Using query APIs to get a list of Contact s with photo uris, val contacts = Contacts ( context ) . query () // if you only want to include photo data in the returned Contacts . include ( Fields . Contact . PhotoUri , Fields . Contact . PhotoThumbnailUri ) . find () for ( contact in contacts ) { Log . d ( \"Contact\" , \"\"\" Photo Uri: ${ contact . photoUri } Thumbnail Uri: ${ contact . photoThumbnailUri } \"\"\" . trimIndent () ) } \u2139\ufe0f For more info, read Query contacts and Query contacts (advanced) . Using one of the extension functions in contacts.core.util.ContactPhoto.kt to get photo data, val photoInputStream = contact . photoInputStream ( contactsApi ) val photoBytes = contact . photoBytes ( contactsApi ) val photoBitmap = contact . photoBitmap ( contactsApi ) val photoBitmapDrawable = contact . photoBitmapDrawable ( contactsApi ) val photoThumbnailInputStream = contact . photoThumbnailInputStream ( contactsApi ) val photoThumbnailBytes = contact . photoThumbnailBytes ( contactsApi ) val photoThumbnailBitmap = contact . photoThumbnailBitmap ( contactsApi ) val photoThumbnailBitmapDrawable = contact . photoThumbnailBitmapDrawable ( contactsApi ) To get RawContact photos directly, use one of the extension functions in contacts.core.util.RawContactPhoto.kt , val photoInputStream = rawContact . photoInputStream ( contactsApi ) val photoBytes = rawContact . photoBytes ( contactsApi ) val photoBitmap = rawContact . photoBitmap ( contactsApi ) val photoBitmapDrawable = rawContact . photoBitmapDrawable ( contactsApi ) val photoThumbnailInputStream = rawContact . photoThumbnailInputStream ( contactsApi ) val photoThumbnailBytes = rawContact . photoThumbnailBytes ( contactsApi ) val photoThumbnailBitmap = rawContact . photoThumbnailBitmap ( contactsApi ) val photoThumbnailBitmapDrawable = rawContact . photoThumbnailBitmapDrawable ( contactsApi ) \u2139\ufe0f The Contact photo is just a reference to one of its RawContact's photo. Setting contact photo \u00b6 Setting the photo can only be done after the Contact or RawContact has been inserted. In other words, photo management can only be done for existing Contacts/RawContacts. To set the Contact photo, use one of the extension functions in contacts.core.util.ContactPhoto.kt , contact . setPhoto ( contactsApi , photoInputStream ) contact . setPhoto ( contactsApi , photoBytes ) contact . setPhoto ( contactsApi , photoBitmap ) contact . setPhoto ( contactsApi , photoBitmapDrawable ) Setting the full-sized photo will automatically set the thumbnail. The Contacts Provider automatically creates a downsized version of the full-sized photo. To set a RawContact photo, use one of the extension functions in contacts.core.util.RawContactPhoto.kt , rawContact . setPhoto ( contactsApi , photoInputStream ) rawContact . setPhoto ( contactsApi , photoBytes ) rawContact . setPhoto ( contactsApi , photoBitmap ) rawContact . setPhoto ( contactsApi , photoBitmapDrawable ) \u2139\ufe0f The Contact photo is just a reference to one of its RawContact's photo. Removing contact photo \u00b6 To remove the Contact (and corresponding RawContact) photo (full-sized and thumbnail), contact . removePhoto ( contactsApi ) To remove a specific RawContact's photo (full-sized and thumbnail), rawContact . removePhoto ( contactsApi ) \u2139\ufe0f The Contact photo is just a reference to one of its RawContact's photo. Changes are immediate and are not applied to the receiver \u00b6 These apply to set and remove functions. Changes are immediate. These functions will make the changes to the Contacts Provider database immediately. You do not need to use update APIs to commit the changes. Changes are not applied to the receiver. This function call does NOT mutate immutable or mutable receivers. Therefore, you should use query APIs or refresh extensions or process the result of this function call to get the most up-to-date reference to mutable or immutable entity that contains the changes in the Contacts Provider database. Using the ui PhotoPicker extensions \u00b6 The contacts.ui.util.PhotoPicker.kt in the ui module` provides extension functions to make selecting existing photos, taking new photos, and removing photos easier. It provides you the same UX as the native Contacts app. To use it, Activity { fun onPhotoViewClicked () { showPhotoPickerDialog ( withRemovePhotoOption = true , removePhoto = { contact . removePhoto ( contactsApi ) } ) } override fun onActivityResult ( requestCode : Int , resultCode : Int , data : Intent?) { super . onActivityResult ( requestCode , resultCode , data ) onPhotoPicked ( requestCode , resultCode , data , photoBitmapPicked = { photoBitmap -> contact . setPhoto ( contactsApi , photoBitmap ) }, photoUriPicked = { uri -> // Note that bitmap decoding should be done in a non-UI thread. Threading has been // left out of this example for brevity. val photoBitmap = if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . P ) { ImageDecoder . decodeBitmap ( ImageDecoder . createSource ( context . contentResolver , uri )) } else { MediaStore . Images . Media . getBitmap ( context . contentResolver , uri ) } contact . setPhoto ( contactsApi , photoBitmap ) } ) } } Starting with Android 11 (API 30), you must include the following to your manifest in order to successfully use the above functions. Performing photo management asynchronously \u00b6 All of the code shown in this guide are done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing photo management with permission \u00b6 Getting and setting photos require the android.permission.READ_CONTACTS and android.permission.WRITE_CONTACTS permissions respectively. If not granted, getting/setting photos will fail. TODO Update this section as part of issue #119 . FAQs \u00b6 Can contacts be insert with photo? \u00b6 \u2139\ufe0f Asked in issue #116 No, you cannot get/set/remove photos for Contacts/RawContacts that have not yet been inserted in the Contacts Provider database. In other words, only Contacts/RawContacts retrieved via query or result APIs can use the extension functions in contacts.core.util.ContactPhoto.kt and contacts.core.util.RawContactPhoto.kt . \u2139\ufe0f Issue #119 will change the answer to yes\". THe underlying mechanism will not change but the outward public facing API will change. Internally, the insert operation will insert a new row in the Contacts/RawContacts table and then attempt to set the photo immediately after. To insert a new contact \"with photo\", you should insert the contact first. Then, if the insert succeeds, proceed to set the photo. \u2139\ufe0f For more info about insert, read Insert contacts . \ud83d\uddd2 Note for contributors; It is possible to include photo thumbnail data as part of the insertion of a new RawContact using ContactsContract.CommonDataKinds.Photo.PHOTO . The Contacts Provider will use the thumbnail as the full-sized photo as well. However, this is not good practice as the full-sized photo will have a really low resolution. Showing the full-sized photo in a big view will not look good. Therefore, this library does not allow this. Consumers must first insert their new RawContact so that they can set the full-sized photo. Can photo be set using a uri instead of bytes and bitmaps? \u00b6 \u2139\ufe0f Asked in discussion #195 No and yes. The core APIs provided in this library only provides functions that the Contacts Provider natively supports. This means setting Contact or RawContact photo only using bytes (and other similar types). See documentation in ContactsContract.RawContacts.DisplayPhoto . Photos are stored and managed by the Contacts Provider, which in turn provides specific URIs for RawContacts and Contacts for read/write access to those photos. We cannot simply just pass in our own URIs. The Contacts Provider will not accept it. The Contacts Provider will only accept raw photo data. It will then generate and manage URIs on its own automatically to enforce data integrity. Consumers may write their own functions to convert a URI to a byte array or bitmap using whatever imaging libraries they want. Certain URIs/URLs may require networking and heavy image processing, which this Contacts library will not cover! URI/URL to image conversion simply does not belong in this library!","title":"Get set remove full-sized and thumbnail contact photos"},{"location":"other/get-set-remove-contact-raw-contact-photo/#get-set-remove-full-sized-and-thumbnail-contact-photos","text":"This library provides several functions to interact with Contact and RawContact full-sized and thumbnail photos.","title":"Get set remove full-sized and thumbnail contact photos"},{"location":"other/get-set-remove-contact-raw-contact-photo/#contact-and-rawcontact-photos","text":"The photo assigned to a Contact is just a reference to a photo assigned to a RawContact. If a Contact consists of more than one RawContact, only the photo from one of the RawContacts will be used by the Contact. Setting/removing the (main) RawContact's photo will in turn change the Contact photo because the Contact photo is just a reference to the RawContact photo. The inverse is also true. RawContact photos are retained when linking and unlinking. \u2139\ufe0f For more info, read Link unlink Contacts .","title":"Contact and RawContact photos"},{"location":"other/get-set-remove-contact-raw-contact-photo/#full-sized-photos-and-thumbnails","text":"Each RawContact may be assigned one photo. The thumbnail is just a downsized version of the full-sized photo. The full-sized photo is typically displayed in a large view, such as in a contact detail screen. The thumbnail is typically displayed in small views, such as in a contacts list view. Setting the full-sized photo will automatically set the thumbnail. The Contacts Provider automatically creates a downsized version of the full-sized photo.","title":"Full-sized photos and thumbnails"},{"location":"other/get-set-remove-contact-raw-contact-photo/#getting-contact-photo","text":"There are several ways to do this. Using query APIs to get a list of Contact s with photo uris, val contacts = Contacts ( context ) . query () // if you only want to include photo data in the returned Contacts . include ( Fields . Contact . PhotoUri , Fields . Contact . PhotoThumbnailUri ) . find () for ( contact in contacts ) { Log . d ( \"Contact\" , \"\"\" Photo Uri: ${ contact . photoUri } Thumbnail Uri: ${ contact . photoThumbnailUri } \"\"\" . trimIndent () ) } \u2139\ufe0f For more info, read Query contacts and Query contacts (advanced) . Using one of the extension functions in contacts.core.util.ContactPhoto.kt to get photo data, val photoInputStream = contact . photoInputStream ( contactsApi ) val photoBytes = contact . photoBytes ( contactsApi ) val photoBitmap = contact . photoBitmap ( contactsApi ) val photoBitmapDrawable = contact . photoBitmapDrawable ( contactsApi ) val photoThumbnailInputStream = contact . photoThumbnailInputStream ( contactsApi ) val photoThumbnailBytes = contact . photoThumbnailBytes ( contactsApi ) val photoThumbnailBitmap = contact . photoThumbnailBitmap ( contactsApi ) val photoThumbnailBitmapDrawable = contact . photoThumbnailBitmapDrawable ( contactsApi ) To get RawContact photos directly, use one of the extension functions in contacts.core.util.RawContactPhoto.kt , val photoInputStream = rawContact . photoInputStream ( contactsApi ) val photoBytes = rawContact . photoBytes ( contactsApi ) val photoBitmap = rawContact . photoBitmap ( contactsApi ) val photoBitmapDrawable = rawContact . photoBitmapDrawable ( contactsApi ) val photoThumbnailInputStream = rawContact . photoThumbnailInputStream ( contactsApi ) val photoThumbnailBytes = rawContact . photoThumbnailBytes ( contactsApi ) val photoThumbnailBitmap = rawContact . photoThumbnailBitmap ( contactsApi ) val photoThumbnailBitmapDrawable = rawContact . photoThumbnailBitmapDrawable ( contactsApi ) \u2139\ufe0f The Contact photo is just a reference to one of its RawContact's photo.","title":"Getting contact photo"},{"location":"other/get-set-remove-contact-raw-contact-photo/#setting-contact-photo","text":"Setting the photo can only be done after the Contact or RawContact has been inserted. In other words, photo management can only be done for existing Contacts/RawContacts. To set the Contact photo, use one of the extension functions in contacts.core.util.ContactPhoto.kt , contact . setPhoto ( contactsApi , photoInputStream ) contact . setPhoto ( contactsApi , photoBytes ) contact . setPhoto ( contactsApi , photoBitmap ) contact . setPhoto ( contactsApi , photoBitmapDrawable ) Setting the full-sized photo will automatically set the thumbnail. The Contacts Provider automatically creates a downsized version of the full-sized photo. To set a RawContact photo, use one of the extension functions in contacts.core.util.RawContactPhoto.kt , rawContact . setPhoto ( contactsApi , photoInputStream ) rawContact . setPhoto ( contactsApi , photoBytes ) rawContact . setPhoto ( contactsApi , photoBitmap ) rawContact . setPhoto ( contactsApi , photoBitmapDrawable ) \u2139\ufe0f The Contact photo is just a reference to one of its RawContact's photo.","title":"Setting contact photo"},{"location":"other/get-set-remove-contact-raw-contact-photo/#removing-contact-photo","text":"To remove the Contact (and corresponding RawContact) photo (full-sized and thumbnail), contact . removePhoto ( contactsApi ) To remove a specific RawContact's photo (full-sized and thumbnail), rawContact . removePhoto ( contactsApi ) \u2139\ufe0f The Contact photo is just a reference to one of its RawContact's photo.","title":"Removing contact photo"},{"location":"other/get-set-remove-contact-raw-contact-photo/#changes-are-immediate-and-are-not-applied-to-the-receiver","text":"These apply to set and remove functions. Changes are immediate. These functions will make the changes to the Contacts Provider database immediately. You do not need to use update APIs to commit the changes. Changes are not applied to the receiver. This function call does NOT mutate immutable or mutable receivers. Therefore, you should use query APIs or refresh extensions or process the result of this function call to get the most up-to-date reference to mutable or immutable entity that contains the changes in the Contacts Provider database.","title":"Changes are immediate and are not applied to the receiver"},{"location":"other/get-set-remove-contact-raw-contact-photo/#using-the-ui-photopicker-extensions","text":"The contacts.ui.util.PhotoPicker.kt in the ui module` provides extension functions to make selecting existing photos, taking new photos, and removing photos easier. It provides you the same UX as the native Contacts app. To use it, Activity { fun onPhotoViewClicked () { showPhotoPickerDialog ( withRemovePhotoOption = true , removePhoto = { contact . removePhoto ( contactsApi ) } ) } override fun onActivityResult ( requestCode : Int , resultCode : Int , data : Intent?) { super . onActivityResult ( requestCode , resultCode , data ) onPhotoPicked ( requestCode , resultCode , data , photoBitmapPicked = { photoBitmap -> contact . setPhoto ( contactsApi , photoBitmap ) }, photoUriPicked = { uri -> // Note that bitmap decoding should be done in a non-UI thread. Threading has been // left out of this example for brevity. val photoBitmap = if ( Build . VERSION . SDK_INT >= Build . VERSION_CODES . P ) { ImageDecoder . decodeBitmap ( ImageDecoder . createSource ( context . contentResolver , uri )) } else { MediaStore . Images . Media . getBitmap ( context . contentResolver , uri ) } contact . setPhoto ( contactsApi , photoBitmap ) } ) } } Starting with Android 11 (API 30), you must include the following to your manifest in order to successfully use the above functions. ","title":"Using the ui PhotoPicker extensions"},{"location":"other/get-set-remove-contact-raw-contact-photo/#performing-photo-management-asynchronously","text":"All of the code shown in this guide are done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing photo management asynchronously"},{"location":"other/get-set-remove-contact-raw-contact-photo/#performing-photo-management-with-permission","text":"Getting and setting photos require the android.permission.READ_CONTACTS and android.permission.WRITE_CONTACTS permissions respectively. If not granted, getting/setting photos will fail. TODO Update this section as part of issue #119 .","title":"Performing photo management with permission"},{"location":"other/get-set-remove-contact-raw-contact-photo/#faqs","text":"","title":"FAQs"},{"location":"other/get-set-remove-contact-raw-contact-photo/#can-contacts-be-insert-with-photo","text":"\u2139\ufe0f Asked in issue #116 No, you cannot get/set/remove photos for Contacts/RawContacts that have not yet been inserted in the Contacts Provider database. In other words, only Contacts/RawContacts retrieved via query or result APIs can use the extension functions in contacts.core.util.ContactPhoto.kt and contacts.core.util.RawContactPhoto.kt . \u2139\ufe0f Issue #119 will change the answer to yes\". THe underlying mechanism will not change but the outward public facing API will change. Internally, the insert operation will insert a new row in the Contacts/RawContacts table and then attempt to set the photo immediately after. To insert a new contact \"with photo\", you should insert the contact first. Then, if the insert succeeds, proceed to set the photo. \u2139\ufe0f For more info about insert, read Insert contacts . \ud83d\uddd2 Note for contributors; It is possible to include photo thumbnail data as part of the insertion of a new RawContact using ContactsContract.CommonDataKinds.Photo.PHOTO . The Contacts Provider will use the thumbnail as the full-sized photo as well. However, this is not good practice as the full-sized photo will have a really low resolution. Showing the full-sized photo in a big view will not look good. Therefore, this library does not allow this. Consumers must first insert their new RawContact so that they can set the full-sized photo.","title":"Can contacts be insert with photo?"},{"location":"other/get-set-remove-contact-raw-contact-photo/#can-photo-be-set-using-a-uri-instead-of-bytes-and-bitmaps","text":"\u2139\ufe0f Asked in discussion #195 No and yes. The core APIs provided in this library only provides functions that the Contacts Provider natively supports. This means setting Contact or RawContact photo only using bytes (and other similar types). See documentation in ContactsContract.RawContacts.DisplayPhoto . Photos are stored and managed by the Contacts Provider, which in turn provides specific URIs for RawContacts and Contacts for read/write access to those photos. We cannot simply just pass in our own URIs. The Contacts Provider will not accept it. The Contacts Provider will only accept raw photo data. It will then generate and manage URIs on its own automatically to enforce data integrity. Consumers may write their own functions to convert a URI to a byte array or bitmap using whatever imaging libraries they want. Certain URIs/URLs may require networking and heavy image processing, which this Contacts library will not cover! URI/URL to image conversion simply does not belong in this library!","title":"Can photo be set using a uri instead of bytes and bitmaps?"},{"location":"other/link-unlink-contacts/","text":"Link unlink Contacts \u00b6 The Contacts Provider automatically aggregates similar RawContacts into a single Contact when it determines that they reference the same person. However, the Contacts Provider's aggregation algorithms are only as accurate as the Data belonging to these RawContacts. Sometimes, they are not enough to determine if they indeed are the same person. With this in mind, the Contacts Provider allows us to explicitly and forcefully specify whether two or more RawContacts reference the same person or not. Hence, this library provides extensions in contacts.core.util.ContactLinks.kt to allow for linking and unlinking two or more Contacts (and their constituent RawContacts). Linking \u00b6 To link three Contacts and all of their constituent RawContacts into a single Contact, val linkResult = contact1 . link ( contactsApi , contact2 , contact3 ) The above links (keep together) all RawContacts belonging to contact1 , contact2 , and contact3 into a single Contact. Aggregation is done by the Contacts Provider. For example, Contact (id: 1, display name: A) RawContact A Contact (id: 2, display name: B) RawContact B RawContact C Linking Contact 1 with Contact 2 results in; Contact (id: 1, display name: A) RawContact A RawContact B RawContact C Contact 2 no longer exists and all of the Data belonging to RawContact B and C are now associated with Contact 1. If instead Contact 2 is linked with Contact 1; Contact (id: 1, display name: B) RawContact A RawContact B RawContact C The same thing occurs except the display name has been set to the display name of RawContact B. This function only instructs the Contacts Provider which RawContacts should be aggregated to a single Contact. Details on how RawContacts are aggregated into a single Contact are left to the Contacts Provider. \u2139\ufe0f Profile Contact/RawContacts are not supported! This operation will fail if given any profile Contact/RawContacts . Handling the link result \u00b6 To check if the link succeeded, val linkSuccessful = linkResult . isSuccessful To get the ID of the parent Contact of all linked RawContacts, val contactId : Long? = linkResult . contactId \u2139\ufe0f The contactId will belong to one of the linked Contacts. Once you have the Contact ID, you can retrieve the Contact via the Query API, val contact = contactsApi . query () . where { Contact . Id equalTo contactId } . find () \u2139\ufe0f For more info, read Query contacts (advanced) . Alternatively, you may use the extensions provided in ContactLikResult . To get the parent Contact of all linked RawContacts, val contact = linkResult . contact ( contactsApi ) Unlinking \u00b6 To unlink a Contacts with more than one RawContact into a separate Contacts, val unlinkResult = contact . unlink ( contactsApi ) The above unlinks (keep separate) all RawContacts belonging to the contact into separate Contacts. The above does nothing and will fail if the Contact only has one constituent RawContact. \u2139\ufe0f Profile Contact/RawContacts are not supported! This operation will fail if given any profile Contact/RawContacts . Handling the unlink result \u00b6 To check if the unlink succeeded, val unlinkSuccessful = unlinkResult . isSuccessful To get the IDs of the constituent RawContact of of the Contact that has been unlinked, val rawContactIds = unlinkResult . rawContactIds Once you have the RawContact IDs, you can retrieve the corresponding Contacts via the Query API, val contacts = contactsApi . query () . where { RawContact . Id `in` rawContactIds } . find () \u2139\ufe0f For more info, read Query contacts (advanced) . Alternatively, you may use the extensions provided in ContactLikResult . To get the Contacts of all unlinked RawContacts, val contacts = unlinkResult . contacts ( contactsApi ) Changes are immediate and are not applied to the receiver \u00b6 These apply to set and clear functions. Changes are immediate. These functions will make the changes to the Contacts Provider database immediately. You do not need to use update APIs to commit the changes. Changes are not applied to the receiver. This function call does NOT mutate immutable or mutable receivers. Therefore, you should use query APIs or refresh extensions or process the result of this function call to get the most up-to-date reference to mutable or immutable entity that contains the changes in the Contacts Provider database. Performing linking/unlinking asynchronously \u00b6 Linking or unlinking contacts is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing linking/unlinking with permission \u00b6 Getting and setting/clearing default data require the android.permission.WRITE_CONTACTS permission. If not granted, linking/unlinking data will fail. TODO Update this section as part of issue #138 . Syncing is done at the RawContact level \u00b6 You may link Contacts with RawContacts that belong to different Accounts. Any RawContact Data modifications are synced per Account sync settings. \u2139\ufe0f For more info, read Sync contact data across devices . RawContacts that are not associated with an Account are local to the device and therefore will not be synced even if it is linked to a Contact with a RawContact that is associated with an Account. \u2139\ufe0f For more info, read about Local (device-only) contacts . Developer notes (or for advanced users) \u00b6 Behavior of linking/merging/joining contacts (AggregationExceptions) \u00b6 The native Contacts app terminology has changed over time; API 22 and below; join / separate API 23; merge / unmerge API 24 and above; link / unlink However, the internals have not changed; KEEP_TOGETHER / KEEP_SEPARATE . These operations are supported by the ContactsContract.AggregationExceptions . For example, given the following tables, ### Contacts table Contact id: 32, displayName: X, starred: 0, timesContacted: 1, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 Contact id: 33, displayName: Y, starred: 1, timesContacted: 2, lastTimeContacted: 1573071750624, customRingtone: content://media/internal/audio/media/115, sendToVoicemail: 1 ### RawContacts table RawContact id: 30, contactId: 32, displayName: X, accountName: x@x.com, accountType: com.google, starred: 0, timesContacted: 1, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 RawContact id: 31, contactId: 33, displayName: Y, accountName: y@y.com, accountType: com.google, starred: 1, timesContacted: 2, lastTimeContacted: 1573071750624, customRingtone: content://media/internal/audio/media/115, sendToVoicemail: 1 ### Data table Data id: 57, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/group_membership, data1: 18 Data id: 58, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/name, data1: X, isPrimary: 1, isSuperPrimary: 1 Data id: 59, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: x@x.com Data id: 60, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: xx@x.com, isPrimary: 1, isSuperPrimary: 1 Data id: 63, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/group_membership, data1: 6 Data id: 64, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/name, data1: Y, isPrimary: 1, isSuperPrimary: 1 Data id: 65, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/email_v2, data1: y@y.com Data id: 66, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/email_v2, data1: yy@y.com, isPrimary: 1, isSuperPrimary: 1 When Contact X links/merges/joins Contact Y , the tables becomes; ### Contacts table Contact id: 32, displayName: X, starred: 1, timesContacted: 2, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 ### RawContacts table RawContact id: 30, contactId: 32, displayName: X, accountName: x@x.com, accountType: com.google, starred: 0, timesContacted: 1, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 RawContact id: 31, contactId: 32, displayName: Y, accountName: y@y.com, accountType: com.google, starred: 1, timesContacted: 2, lastTimeContacted: 1573071750624, customRingtone: content://media/internal/audio/media/115, sendToVoicemail: 1 ### Data table Data id: 57, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/group_membership, data1: 18 Data id: 58, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/name, data1: X, isPrimary: 1, isSuperPrimary: 1 Data id: 59, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: x@x.com Data id: 60, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: xx@x.com, isPrimary: 1, isSuperPrimary: 0 Data id: 63, rawContactId: 31, contactId: 32, mimeType: vnd.android.cursor.item/group_membership, data1: 6 Data id: 64, rawContactId: 31, contactId: 32, mimeType: vnd.android.cursor.item/name, data1: Y, isPrimary: 1, isSuperPrimary: 0 Data id: 65, rawContactId: 31, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: y@y.com Data id: 66, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/email_v2, data1: yy@y.com, isPrimary: 1, isSuperPrimary: 0 What changed? Contact Y's row has been deleted and its column values have been merged into Contact X row. If the reverse occurred (Contact Y merged with Contact X), Contact Y's row would still be deleted. The difference is that Contact X's display name will be set to Contact Y's display name, which is done by the native Contacts app manually by setting Contact Y's Data name row to be the \"default\" (isPrimary and isSuperPrimary both set to 1). \u2139\ufe0f The AggregationExceptions table records the linked RawContacts IDs in ascending order regardless of the order used in RAW_CONTACT_ID1 and RAW_CONTACT_ID2 at the time of merging. The RawContacts and Data table remains the same except the joined contactId column values have now been changed to the id of Contact X. All Data rows' isSuperPrimary value has been set to 0 though the isPrimary columns remain the same. In other words, this clears any \"default\" set before the link. These are done automatically by the Contacts Provider during the link operation. What is not done automatically by the Contacts Provider is that the name row of former Contact X is set as the default. The native Contacts app does this manually. The Contacts Providers automatically sets the Contact display name to whatever the default name row is for the Contact, if available. For more info on Contact display name resolution, read the Contact Display Name and Default Name Rows section. \u2139\ufe0f Display name resolution is different for APIs below 21 (pre-lollipop). The display name of the RawContacts remain the same. The Groups table remains unmodified. Options updates Changes to the options (starred, timesContacted, lastTimeContacted, customRingtone, and sendToVoicemail) of a RawContact may affect the options of the parent Contact. On the other hand, changes to the options of the parent Contact will be propagated to all child RawContact options. Photo updates A RawContact may have a full-sized photo saved as a file and a thumbnail version of that saved in the Data table in a photo mimetype row. A Contact's full-sized photo and thumbnail are simply references to the \"chosen\" RawContact's full-sized photo and thumbnail (though the URIs may differ). \u2139\ufe0f When removing the photo in the native contacts app, the photo data row is not immediately deleted, though the PHOTO_FILE_ID is immediately set to null. This may result in the PHOTO_URI and PHOTO_THUMBNAIL_URI to still have a valid image uri even though the photo has been \"removed\". This library immediately deletes the photo data row, which seems to work perfectly. Data inserts In the native Contacts app, Data inserted in combined (raw) contacts mode will be associated to the first RawContact in the list sorted by the RawContact ID. \u2139\ufe0f This may not be the same as the RawContact referenced by ContactsColumns.NAME_RAW_CONTACT_ID . UI changes? The native Contacts App does not display the groups field when displaying / editing Contacts that have multiple RawContacts (linked/merged/joined) in combined mode. However, it does allow editing individual RawContact Data rows in which case the groups field is displayed and editable. In the native Contacts app, the name attribute used comes from the name row with IS_SUPER_PRIMARY set to true. This and all other \"unique\" mimetypes (organization) and non-unique mimetypes (email) per RawContact are shown only if they are not blank. Showing multiple RawContact's data in the same edit screen (combined mode) In older version of the native, Android Open Source Project (AOSP) Contacts app, data from multiple RawContacts was being shown in the same edit screen. This caused a lot of confusion about which data belonged to which RawContact. Newer versions of AOSP Contacts only allow editing one RawContact at a time to avoid confusion. Though, several RawContacts' data are still shown (not-editable) in the same screen. AggregationExceptions table \u00b6 Given the following Contacts and their RawContacts; Contact A RawContact 1 Contact B RawContact 2 Contact C RawContact 3 Contact D RawContact 4 Linking one by one in this order; Contact B link Contact A Contact C link Contact D Contact C link Contact B Results in the following AggregationExceptions rows respectively; Aggregation exception id: 430, type: 1, rawContactId1: 1, rawContactId2: 2 Aggregation exception id: 430, type: 1, rawContactId1: 1, rawContactId2: 2 Aggregation exception id: 432, type: 1, rawContactId1: 3, rawContactId2: 4 Aggregation exception id: 436, type: 1, rawContactId1: 1, rawContactId2: 2 Aggregation exception id: 439, type: 1, rawContactId1: 1, rawContactId2: 3 Aggregation exception id: 442, type: 1, rawContactId1: 1, rawContactId2: 4 Aggregation exception id: 440, type: 1, rawContactId1: 2, rawContactId2: 3 Aggregation exception id: 443, type: 1, rawContactId1: 2, rawContactId2: 4 Aggregation exception id: 444, type: 1, rawContactId1: 3, rawContactId2: 4 There is a pattern here. RawContact ids are sorted in ascending order and linked from least to greatest exhaustively but no double links (1-2 is the same as 2-1). RawContact 1 has a row with RawContact 2, 3, and 4. RawContact 2 has a row with RawContact 3 and 4. RawContact 3 has a row with RawContact 4. Linking all in one go; Contact C link Contact A, B, D Results in the same AggregationExceptions rows. Unlinking results in the same AggregationExceptions rows except the type is 2 ( TYPE_KEEP_SEPARATE ). Contact Display Name and Default Name Rows \u00b6 If available, the \"default\" (isPrimary and isSuperPrimary set to 1) name row for a Contact is automatically set as the Contact display name by the Contacts Provider. Otherwise, the Contacts Provider chooses from any of the other suitable data from the aggregate Contact. \u2139\ufe0f The ContactsColumns.NAME_RAW_CONTACT_ID is automatically updated by the Contacts Provider along with the display name. The default status of other sources (e.g. email) does not affect the Contact display name. The native Contacts app also sets the most recently updated name as the default at every update. This results in the Contact display name changing to the most recently updated name from one of the associated RawContacts. The \"most recently updated name\" is the name field that was last updated by the user when editing in the Contacts app, which is irrelevant to its value. It does not matter if the user deleted the last character of the name, added the same character back, and then saved. It still counts as the most recently updated. All of the above only applies to API 21 and above. Display name resolution is different for APIs below 21 (pre-Lollipop)! The ContactsColumns.NAME_RAW_CONTACT_ID was added in API 21. It changed the way display names are resolved for Contacts with more than one constituent RawContacts, which is what has been described so far. Before this change (APIs 20 and below), the native Contacts app is still able to set the Contact display name somehow. I'm not sure how. If someone figures it out, please let me know. I tried updating the Contact DISPLAY_NAME directly but it does not work. Setting a name row as default also does not affect the Contact DISPLAY_NAME . Effects of linking/unlinking contacts \u00b6 When two or more Contacts (along with their constituent RawContacts) are linked into a single Contact those Contacts will be merged into one of the existing Contact row. The Contacts that have been merged into the single Contact will have their entries/rows in the Contacts table deleted. Unlinking will result in the original Contacts prior to linking to have new rows in the Contacts table with different IDs because the previously deleted row IDs cannot be reused. Getting Contacts that have been linked into a single Contact or Contacts whose row IDs have change after unlinking is still possible using the Contact lookup key. For more info, read about Contact lookup key vs ID .","title":"Link unlink Contacts"},{"location":"other/link-unlink-contacts/#link-unlink-contacts","text":"The Contacts Provider automatically aggregates similar RawContacts into a single Contact when it determines that they reference the same person. However, the Contacts Provider's aggregation algorithms are only as accurate as the Data belonging to these RawContacts. Sometimes, they are not enough to determine if they indeed are the same person. With this in mind, the Contacts Provider allows us to explicitly and forcefully specify whether two or more RawContacts reference the same person or not. Hence, this library provides extensions in contacts.core.util.ContactLinks.kt to allow for linking and unlinking two or more Contacts (and their constituent RawContacts).","title":"Link unlink Contacts"},{"location":"other/link-unlink-contacts/#linking","text":"To link three Contacts and all of their constituent RawContacts into a single Contact, val linkResult = contact1 . link ( contactsApi , contact2 , contact3 ) The above links (keep together) all RawContacts belonging to contact1 , contact2 , and contact3 into a single Contact. Aggregation is done by the Contacts Provider. For example, Contact (id: 1, display name: A) RawContact A Contact (id: 2, display name: B) RawContact B RawContact C Linking Contact 1 with Contact 2 results in; Contact (id: 1, display name: A) RawContact A RawContact B RawContact C Contact 2 no longer exists and all of the Data belonging to RawContact B and C are now associated with Contact 1. If instead Contact 2 is linked with Contact 1; Contact (id: 1, display name: B) RawContact A RawContact B RawContact C The same thing occurs except the display name has been set to the display name of RawContact B. This function only instructs the Contacts Provider which RawContacts should be aggregated to a single Contact. Details on how RawContacts are aggregated into a single Contact are left to the Contacts Provider. \u2139\ufe0f Profile Contact/RawContacts are not supported! This operation will fail if given any profile Contact/RawContacts .","title":"Linking"},{"location":"other/link-unlink-contacts/#handling-the-link-result","text":"To check if the link succeeded, val linkSuccessful = linkResult . isSuccessful To get the ID of the parent Contact of all linked RawContacts, val contactId : Long? = linkResult . contactId \u2139\ufe0f The contactId will belong to one of the linked Contacts. Once you have the Contact ID, you can retrieve the Contact via the Query API, val contact = contactsApi . query () . where { Contact . Id equalTo contactId } . find () \u2139\ufe0f For more info, read Query contacts (advanced) . Alternatively, you may use the extensions provided in ContactLikResult . To get the parent Contact of all linked RawContacts, val contact = linkResult . contact ( contactsApi )","title":"Handling the link result"},{"location":"other/link-unlink-contacts/#unlinking","text":"To unlink a Contacts with more than one RawContact into a separate Contacts, val unlinkResult = contact . unlink ( contactsApi ) The above unlinks (keep separate) all RawContacts belonging to the contact into separate Contacts. The above does nothing and will fail if the Contact only has one constituent RawContact. \u2139\ufe0f Profile Contact/RawContacts are not supported! This operation will fail if given any profile Contact/RawContacts .","title":"Unlinking"},{"location":"other/link-unlink-contacts/#handling-the-unlink-result","text":"To check if the unlink succeeded, val unlinkSuccessful = unlinkResult . isSuccessful To get the IDs of the constituent RawContact of of the Contact that has been unlinked, val rawContactIds = unlinkResult . rawContactIds Once you have the RawContact IDs, you can retrieve the corresponding Contacts via the Query API, val contacts = contactsApi . query () . where { RawContact . Id `in` rawContactIds } . find () \u2139\ufe0f For more info, read Query contacts (advanced) . Alternatively, you may use the extensions provided in ContactLikResult . To get the Contacts of all unlinked RawContacts, val contacts = unlinkResult . contacts ( contactsApi )","title":"Handling the unlink result"},{"location":"other/link-unlink-contacts/#changes-are-immediate-and-are-not-applied-to-the-receiver","text":"These apply to set and clear functions. Changes are immediate. These functions will make the changes to the Contacts Provider database immediately. You do not need to use update APIs to commit the changes. Changes are not applied to the receiver. This function call does NOT mutate immutable or mutable receivers. Therefore, you should use query APIs or refresh extensions or process the result of this function call to get the most up-to-date reference to mutable or immutable entity that contains the changes in the Contacts Provider database.","title":"Changes are immediate and are not applied to the receiver"},{"location":"other/link-unlink-contacts/#performing-linkingunlinking-asynchronously","text":"Linking or unlinking contacts is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing linking/unlinking asynchronously"},{"location":"other/link-unlink-contacts/#performing-linkingunlinking-with-permission","text":"Getting and setting/clearing default data require the android.permission.WRITE_CONTACTS permission. If not granted, linking/unlinking data will fail. TODO Update this section as part of issue #138 .","title":"Performing linking/unlinking with permission"},{"location":"other/link-unlink-contacts/#syncing-is-done-at-the-rawcontact-level","text":"You may link Contacts with RawContacts that belong to different Accounts. Any RawContact Data modifications are synced per Account sync settings. \u2139\ufe0f For more info, read Sync contact data across devices . RawContacts that are not associated with an Account are local to the device and therefore will not be synced even if it is linked to a Contact with a RawContact that is associated with an Account. \u2139\ufe0f For more info, read about Local (device-only) contacts .","title":"Syncing is done at the RawContact level"},{"location":"other/link-unlink-contacts/#developer-notes-or-for-advanced-users","text":"","title":"Developer notes (or for advanced users)"},{"location":"other/link-unlink-contacts/#behavior-of-linkingmergingjoining-contacts-aggregationexceptions","text":"The native Contacts app terminology has changed over time; API 22 and below; join / separate API 23; merge / unmerge API 24 and above; link / unlink However, the internals have not changed; KEEP_TOGETHER / KEEP_SEPARATE . These operations are supported by the ContactsContract.AggregationExceptions . For example, given the following tables, ### Contacts table Contact id: 32, displayName: X, starred: 0, timesContacted: 1, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 Contact id: 33, displayName: Y, starred: 1, timesContacted: 2, lastTimeContacted: 1573071750624, customRingtone: content://media/internal/audio/media/115, sendToVoicemail: 1 ### RawContacts table RawContact id: 30, contactId: 32, displayName: X, accountName: x@x.com, accountType: com.google, starred: 0, timesContacted: 1, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 RawContact id: 31, contactId: 33, displayName: Y, accountName: y@y.com, accountType: com.google, starred: 1, timesContacted: 2, lastTimeContacted: 1573071750624, customRingtone: content://media/internal/audio/media/115, sendToVoicemail: 1 ### Data table Data id: 57, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/group_membership, data1: 18 Data id: 58, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/name, data1: X, isPrimary: 1, isSuperPrimary: 1 Data id: 59, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: x@x.com Data id: 60, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: xx@x.com, isPrimary: 1, isSuperPrimary: 1 Data id: 63, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/group_membership, data1: 6 Data id: 64, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/name, data1: Y, isPrimary: 1, isSuperPrimary: 1 Data id: 65, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/email_v2, data1: y@y.com Data id: 66, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/email_v2, data1: yy@y.com, isPrimary: 1, isSuperPrimary: 1 When Contact X links/merges/joins Contact Y , the tables becomes; ### Contacts table Contact id: 32, displayName: X, starred: 1, timesContacted: 2, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 ### RawContacts table RawContact id: 30, contactId: 32, displayName: X, accountName: x@x.com, accountType: com.google, starred: 0, timesContacted: 1, lastTimeContacted: 1573071785456, customRingtone: content://media/internal/audio/media/109, sendToVoicemail: 0 RawContact id: 31, contactId: 32, displayName: Y, accountName: y@y.com, accountType: com.google, starred: 1, timesContacted: 2, lastTimeContacted: 1573071750624, customRingtone: content://media/internal/audio/media/115, sendToVoicemail: 1 ### Data table Data id: 57, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/group_membership, data1: 18 Data id: 58, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/name, data1: X, isPrimary: 1, isSuperPrimary: 1 Data id: 59, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: x@x.com Data id: 60, rawContactId: 30, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: xx@x.com, isPrimary: 1, isSuperPrimary: 0 Data id: 63, rawContactId: 31, contactId: 32, mimeType: vnd.android.cursor.item/group_membership, data1: 6 Data id: 64, rawContactId: 31, contactId: 32, mimeType: vnd.android.cursor.item/name, data1: Y, isPrimary: 1, isSuperPrimary: 0 Data id: 65, rawContactId: 31, contactId: 32, mimeType: vnd.android.cursor.item/email_v2, data1: y@y.com Data id: 66, rawContactId: 31, contactId: 33, mimeType: vnd.android.cursor.item/email_v2, data1: yy@y.com, isPrimary: 1, isSuperPrimary: 0 What changed? Contact Y's row has been deleted and its column values have been merged into Contact X row. If the reverse occurred (Contact Y merged with Contact X), Contact Y's row would still be deleted. The difference is that Contact X's display name will be set to Contact Y's display name, which is done by the native Contacts app manually by setting Contact Y's Data name row to be the \"default\" (isPrimary and isSuperPrimary both set to 1). \u2139\ufe0f The AggregationExceptions table records the linked RawContacts IDs in ascending order regardless of the order used in RAW_CONTACT_ID1 and RAW_CONTACT_ID2 at the time of merging. The RawContacts and Data table remains the same except the joined contactId column values have now been changed to the id of Contact X. All Data rows' isSuperPrimary value has been set to 0 though the isPrimary columns remain the same. In other words, this clears any \"default\" set before the link. These are done automatically by the Contacts Provider during the link operation. What is not done automatically by the Contacts Provider is that the name row of former Contact X is set as the default. The native Contacts app does this manually. The Contacts Providers automatically sets the Contact display name to whatever the default name row is for the Contact, if available. For more info on Contact display name resolution, read the Contact Display Name and Default Name Rows section. \u2139\ufe0f Display name resolution is different for APIs below 21 (pre-lollipop). The display name of the RawContacts remain the same. The Groups table remains unmodified. Options updates Changes to the options (starred, timesContacted, lastTimeContacted, customRingtone, and sendToVoicemail) of a RawContact may affect the options of the parent Contact. On the other hand, changes to the options of the parent Contact will be propagated to all child RawContact options. Photo updates A RawContact may have a full-sized photo saved as a file and a thumbnail version of that saved in the Data table in a photo mimetype row. A Contact's full-sized photo and thumbnail are simply references to the \"chosen\" RawContact's full-sized photo and thumbnail (though the URIs may differ). \u2139\ufe0f When removing the photo in the native contacts app, the photo data row is not immediately deleted, though the PHOTO_FILE_ID is immediately set to null. This may result in the PHOTO_URI and PHOTO_THUMBNAIL_URI to still have a valid image uri even though the photo has been \"removed\". This library immediately deletes the photo data row, which seems to work perfectly. Data inserts In the native Contacts app, Data inserted in combined (raw) contacts mode will be associated to the first RawContact in the list sorted by the RawContact ID. \u2139\ufe0f This may not be the same as the RawContact referenced by ContactsColumns.NAME_RAW_CONTACT_ID . UI changes? The native Contacts App does not display the groups field when displaying / editing Contacts that have multiple RawContacts (linked/merged/joined) in combined mode. However, it does allow editing individual RawContact Data rows in which case the groups field is displayed and editable. In the native Contacts app, the name attribute used comes from the name row with IS_SUPER_PRIMARY set to true. This and all other \"unique\" mimetypes (organization) and non-unique mimetypes (email) per RawContact are shown only if they are not blank. Showing multiple RawContact's data in the same edit screen (combined mode) In older version of the native, Android Open Source Project (AOSP) Contacts app, data from multiple RawContacts was being shown in the same edit screen. This caused a lot of confusion about which data belonged to which RawContact. Newer versions of AOSP Contacts only allow editing one RawContact at a time to avoid confusion. Though, several RawContacts' data are still shown (not-editable) in the same screen.","title":"Behavior of linking/merging/joining contacts (AggregationExceptions)"},{"location":"other/link-unlink-contacts/#aggregationexceptions-table","text":"Given the following Contacts and their RawContacts; Contact A RawContact 1 Contact B RawContact 2 Contact C RawContact 3 Contact D RawContact 4 Linking one by one in this order; Contact B link Contact A Contact C link Contact D Contact C link Contact B Results in the following AggregationExceptions rows respectively; Aggregation exception id: 430, type: 1, rawContactId1: 1, rawContactId2: 2 Aggregation exception id: 430, type: 1, rawContactId1: 1, rawContactId2: 2 Aggregation exception id: 432, type: 1, rawContactId1: 3, rawContactId2: 4 Aggregation exception id: 436, type: 1, rawContactId1: 1, rawContactId2: 2 Aggregation exception id: 439, type: 1, rawContactId1: 1, rawContactId2: 3 Aggregation exception id: 442, type: 1, rawContactId1: 1, rawContactId2: 4 Aggregation exception id: 440, type: 1, rawContactId1: 2, rawContactId2: 3 Aggregation exception id: 443, type: 1, rawContactId1: 2, rawContactId2: 4 Aggregation exception id: 444, type: 1, rawContactId1: 3, rawContactId2: 4 There is a pattern here. RawContact ids are sorted in ascending order and linked from least to greatest exhaustively but no double links (1-2 is the same as 2-1). RawContact 1 has a row with RawContact 2, 3, and 4. RawContact 2 has a row with RawContact 3 and 4. RawContact 3 has a row with RawContact 4. Linking all in one go; Contact C link Contact A, B, D Results in the same AggregationExceptions rows. Unlinking results in the same AggregationExceptions rows except the type is 2 ( TYPE_KEEP_SEPARATE ).","title":"AggregationExceptions table"},{"location":"other/link-unlink-contacts/#contact-display-name-and-default-name-rows","text":"If available, the \"default\" (isPrimary and isSuperPrimary set to 1) name row for a Contact is automatically set as the Contact display name by the Contacts Provider. Otherwise, the Contacts Provider chooses from any of the other suitable data from the aggregate Contact. \u2139\ufe0f The ContactsColumns.NAME_RAW_CONTACT_ID is automatically updated by the Contacts Provider along with the display name. The default status of other sources (e.g. email) does not affect the Contact display name. The native Contacts app also sets the most recently updated name as the default at every update. This results in the Contact display name changing to the most recently updated name from one of the associated RawContacts. The \"most recently updated name\" is the name field that was last updated by the user when editing in the Contacts app, which is irrelevant to its value. It does not matter if the user deleted the last character of the name, added the same character back, and then saved. It still counts as the most recently updated. All of the above only applies to API 21 and above. Display name resolution is different for APIs below 21 (pre-Lollipop)! The ContactsColumns.NAME_RAW_CONTACT_ID was added in API 21. It changed the way display names are resolved for Contacts with more than one constituent RawContacts, which is what has been described so far. Before this change (APIs 20 and below), the native Contacts app is still able to set the Contact display name somehow. I'm not sure how. If someone figures it out, please let me know. I tried updating the Contact DISPLAY_NAME directly but it does not work. Setting a name row as default also does not affect the Contact DISPLAY_NAME .","title":"Contact Display Name and Default Name Rows"},{"location":"other/link-unlink-contacts/#effects-of-linkingunlinking-contacts","text":"When two or more Contacts (along with their constituent RawContacts) are linked into a single Contact those Contacts will be merged into one of the existing Contact row. The Contacts that have been merged into the single Contact will have their entries/rows in the Contacts table deleted. Unlinking will result in the original Contacts prior to linking to have new rows in the Contacts table with different IDs because the previously deleted row IDs cannot be reused. Getting Contacts that have been linked into a single Contact or Contacts whose row IDs have change after unlinking is still possible using the Contact lookup key. For more info, read about Contact lookup key vs ID .","title":"Effects of linking/unlinking contacts"},{"location":"permissions/permissions-handling-coroutines/","text":"Permissions handling using coroutines \u00b6 This library provides extensions in the permissions module that allow you to prompt users for required permissions before executing a core API function. These extensions use Kotlin Coroutines . For all core API functions that requires certain permissions to be granted (e.g. query, insert, update, and deletes), there is a corresponding xxxWithPermission extension function. Using withPermission extensions \u00b6 To perform an query, insert, update, and delete with permission , launch { val contactsApi = Contacts ( context ) val query = contactsApi . queryWithPermission () val insert = contactsApi . insertWithPermission () val update = contactsApi . updateWithPermission () val delete = contactsApi . deleteWithPermission () } For each invocation of xxxWithPermission , if the required permission(s) are not yet granted, the current coroutine is suspended, user is prompted to grant permissions, and then an operation instance is returned (which may then be executed to get a result). If permission(s) are already granted, then an operation instance is returned immediately without suspending the coroutine and prompting the user for permission. If permission(s) are not granted, then the operation will immediately fail and the result you get is incorrect (usually null or empty when it should not be). \u2139\ufe0f Prior to Android 6.0 Marshmallow (API level 23), users are NOT prompted for permission at runtime because users must already grant all permissions prior to app install. Not compatible with Java \u00b6 Unlike the core module, the permissions module is not compatible with Java because it requires Kotlin Coroutines. These extensions are optional \u00b6 You are free to use the core APIs however you want with whatever libraries or frameworks you want that works with Java or use your own DIY solution.","title":"Permissions handling using coroutines"},{"location":"permissions/permissions-handling-coroutines/#permissions-handling-using-coroutines","text":"This library provides extensions in the permissions module that allow you to prompt users for required permissions before executing a core API function. These extensions use Kotlin Coroutines . For all core API functions that requires certain permissions to be granted (e.g. query, insert, update, and deletes), there is a corresponding xxxWithPermission extension function.","title":"Permissions handling using coroutines"},{"location":"permissions/permissions-handling-coroutines/#using-withpermission-extensions","text":"To perform an query, insert, update, and delete with permission , launch { val contactsApi = Contacts ( context ) val query = contactsApi . queryWithPermission () val insert = contactsApi . insertWithPermission () val update = contactsApi . updateWithPermission () val delete = contactsApi . deleteWithPermission () } For each invocation of xxxWithPermission , if the required permission(s) are not yet granted, the current coroutine is suspended, user is prompted to grant permissions, and then an operation instance is returned (which may then be executed to get a result). If permission(s) are already granted, then an operation instance is returned immediately without suspending the coroutine and prompting the user for permission. If permission(s) are not granted, then the operation will immediately fail and the result you get is incorrect (usually null or empty when it should not be). \u2139\ufe0f Prior to Android 6.0 Marshmallow (API level 23), users are NOT prompted for permission at runtime because users must already grant all permissions prior to app install.","title":"Using withPermission extensions"},{"location":"permissions/permissions-handling-coroutines/#not-compatible-with-java","text":"Unlike the core module, the permissions module is not compatible with Java because it requires Kotlin Coroutines.","title":"Not compatible with Java"},{"location":"permissions/permissions-handling-coroutines/#these-extensions-are-optional","text":"You are free to use the core APIs however you want with whatever libraries or frameworks you want that works with Java or use your own DIY solution.","title":"These extensions are optional"},{"location":"profile/delete-profile/","text":"Delete device owner Contact profile \u00b6 This library provides the ProfileDelete API, which allows you to delete the device owner Profile Contact or only some of its constituent RawContacts. \u2139\ufe0f There can be only one device owner Contact, which is either set (not null) or not yet set (null). However, like other regular Contacts, the Profile Contact may have one or more RawContacts. An instance of the ProfileDelete API is obtained by, val delete = Contacts ( context ). profile (). delete () \u2139\ufe0f If you want to delete non-Profile Contacts, read Delete Contacts A basic delete \u00b6 To delete a the profile Contact (if it exist) and all of its RawContacts, val deleteResult = delete . contact () . commit () If you want to delete a set of RawContacts belonging to the profile Contact, val deleteResult = delete . rawContacts ( contactToDelete ) . commit () Note that the profile Contact is deleted automatically when all constituent RawContacts are deleted. Executing the delete \u00b6 To execute the delete, . commit () If you want to delete all given RawContacts in a single atomic transaction, . commitInOneTransaction () The call to commitInOneTransaction will only succeed if ALL given RawContacts are successfully deleted. If one delete fails, the entire operation will fail and everything will be reverted prior to the delete operation. In contrast, commit allows for some deletes to succeed and some to fail. This really only applies to when only rawContacts are specified. Performing the delete and result processing asynchronously \u00b6 Deletes are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the delete with permission \u00b6 Deletes require the android.permission.WRITE_CONTACTS permissions. If not granted, the delete will do nothing and return a failed result. \u2139\ufe0f For API 22 and below, the permission \"android.permission.WRITE_PROFILE\" is also required but only at the manifest level. Prior to API 23 (Marshmallow), permissions needed to be granted prior to installation instead of at runtime. To perform the delete with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Custom data support \u00b6 The ProfileDelete API supports custom data. For more info, read Delete custom data .","title":"Delete device owner Contact profile"},{"location":"profile/delete-profile/#delete-device-owner-contact-profile","text":"This library provides the ProfileDelete API, which allows you to delete the device owner Profile Contact or only some of its constituent RawContacts. \u2139\ufe0f There can be only one device owner Contact, which is either set (not null) or not yet set (null). However, like other regular Contacts, the Profile Contact may have one or more RawContacts. An instance of the ProfileDelete API is obtained by, val delete = Contacts ( context ). profile (). delete () \u2139\ufe0f If you want to delete non-Profile Contacts, read Delete Contacts","title":"Delete device owner Contact profile"},{"location":"profile/delete-profile/#a-basic-delete","text":"To delete a the profile Contact (if it exist) and all of its RawContacts, val deleteResult = delete . contact () . commit () If you want to delete a set of RawContacts belonging to the profile Contact, val deleteResult = delete . rawContacts ( contactToDelete ) . commit () Note that the profile Contact is deleted automatically when all constituent RawContacts are deleted.","title":"A basic delete"},{"location":"profile/delete-profile/#executing-the-delete","text":"To execute the delete, . commit () If you want to delete all given RawContacts in a single atomic transaction, . commitInOneTransaction () The call to commitInOneTransaction will only succeed if ALL given RawContacts are successfully deleted. If one delete fails, the entire operation will fail and everything will be reverted prior to the delete operation. In contrast, commit allows for some deletes to succeed and some to fail. This really only applies to when only rawContacts are specified.","title":"Executing the delete"},{"location":"profile/delete-profile/#performing-the-delete-and-result-processing-asynchronously","text":"Deletes are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the delete and result processing asynchronously"},{"location":"profile/delete-profile/#performing-the-delete-with-permission","text":"Deletes require the android.permission.WRITE_CONTACTS permissions. If not granted, the delete will do nothing and return a failed result. \u2139\ufe0f For API 22 and below, the permission \"android.permission.WRITE_PROFILE\" is also required but only at the manifest level. Prior to API 23 (Marshmallow), permissions needed to be granted prior to installation instead of at runtime. To perform the delete with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the delete with permission"},{"location":"profile/delete-profile/#custom-data-support","text":"The ProfileDelete API supports custom data. For more info, read Delete custom data .","title":"Custom data support"},{"location":"profile/insert-profile/","text":"Insert the device owner Contact profile \u00b6 This library provides the ProfileInsert API that allows you to insert one or more RawContacts and Data. \u2139\ufe0f There can be only one device owner Contact, which is either set (not null) or not yet set (null). However, like other regular Contacts, the Profile Contact may have one or more RawContacts. An instance of the ProfileInsert API is obtained by, val insert = Contacts ( context ). profile (). insert () \u2139\ufe0f If you want to create/insert non-Profile Contacts, read Insert contacts . A basic insert \u00b6 To create/insert a raw contact with a name of \"John Doe\" who works at Amazon with a work email of \"john.doe@amazon.com\" (in Kotlin), val insertResult = Contacts ( context ) . profile () . insert () . rawContacts ( NewRawContact (). apply { name = NewName (). apply { givenName = \"John\" familyName = \"Doe\" } organization = NewOrganization (). apply { company = \"Amazon\" title = \"Superstar\" } emails . add ( NewEmail (). apply { address = \"john.doe@amazon.com\" type = EmailEntity . Type . WORK }) }) . commit () Or alternatively, in a more Kotlinized style using named arguments, val insertResult = Contacts ( context ) . profile () . insert () . rawContacts ( NewRawContact ( name = NewName ( givenName = \"John\" , familyName = \"Doe\" ), organization = NewOrganization ( company = \"Amazon\" , title = \"Superstar\" ), emails = mutableListOf ( NewEmail ( address = \"john.doe@amazon.com\" , type = EmailEntity . Type . WORK )) )) . commit () Or alternatively, using extension functions, val insertResult = Contacts ( context ) . profile () . insert () . rawContact { setName { givenName = \"John\" familyName = \"Doe\" } setOrganization { company = \"Amazon\" title = \"Superstar\" } addEmail { address = \"john.doe@amazon.com\" type = EmailEntity . Type . WORK } } . commit () Allowing blanks \u00b6 The API allows you to specify if you want to be able to insert blank contacts or not, . allowBlanks ( true | false ) For more info, read about Blank contacts . Blank data are not inserted \u00b6 Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are ignored and are not inserted by insert APIs. For more info, read about Blank data . Allowing multiple RawContacts per Account \u00b6 The API allows you to insert a profile RawContact with an Account that already has a profile RawContact, . allowMultipleRawContactsPerAccount ( true | false ) According to the ContactsContract.Profile documentation; ... each account (including data set, if applicable) on the device may contribute a single raw contact representing the user's personal profile data from that source. In other words, one account can have one profile RawContact. However, despite the documentation of \"one profile RawContact per one Account\", the Contacts Provider allows for multiple RawContacts per Account, including multiple local RawContacts (no Account). Associating an Account \u00b6 New RawContacts can be associated with an Account in order to enable syncing, . forAccount ( account ) For example, to associated the new RawContact to an account, . forAccount ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) \u2139\ufe0f For more info, read Query for Accounts . Local RawContacts \u00b6 If no Account is provided, or null is provided, or if an incorrect account is provided, the RawContacts inserted will not be associated with an Account. RawContacts inserted without an associated account are considered local or device-only contacts, which are not synced. \u2139\ufe0f For more info, read Sync contact data across devices . There are also certain data kinds that are ignored on insert or update if the RawContact is local. \u2139\ufe0f For more info, read about Local (device-only) contacts . Including only specific data \u00b6 To include only the given set of fields (data) in each of the insert operation, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations . Executing the insert \u00b6 To execute the insert, . commit () Handling the insert result \u00b6 The commit function returns a Result , val contactsApi = Contacts ( context ) val newRawContact = NewRawContact (...) val insertResult = contactsApi . profile () . insert () . rawContact ( newRawContact ) . commit () To check if the insert succeeded, val insertSucess = insertResult . isSuccessful To get the RawContact IDs of the newly created RawContact, val rawContactId = insertResult . rawContactId Once you have the RawContact ID, you can retrieve the newly created Contact via the Query API, val contacts = contactsApi . query () . where { RawContact . Id equalTo rawContactId } . find () \u2139\ufe0f For more info, read Query contacts (advanced) . Alternatively, you may use the extensions provided in ProfileInsertResult . To get the newly created Contact, val contact = insertResult . contact ( contactsApi ) To instead get the RawContact directly, val rawContacts = insertResult . rawContact ( contactsApi ) Cancelling the insert \u00b6 To cancel an insert amid execution, . commit { returnTrueIfInsertShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel insert processing as soon as possible. The function is called numerous times during insert processing to check if processing should stop or continue. This gives you the option to cancel the insert. For example, to automatically cancel the insert inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val insertResult = insert . commit { ! isActive } } } Performing the insert and result processing asynchronously \u00b6 Inserts are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the insert with permission \u00b6 Inserts require the android.permission.WRITE_CONTACTS and android.permission.GET_ACCOUNTS permissions. If not granted, the insert will do nothing and return a failed result. \u2139\ufe0f For API 22 and below, the permission \"android.permission.WRITE_PROFILE\" is also required but only at the manifest level. Prior to API 23 (Marshmallow), permissions needed to be granted prior to installation instead of at runtime. To perform the insert with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Custom data support \u00b6 The ProfileInsert API supports custom data. For more info, read Insert custom data into new or existing contacts . RawContact and Contact aggregation \u00b6 As per documentation in android.provider.ContactsContract.Profile , The user's profile entry cannot be created explicitly (attempting to do so will throw an exception). When a raw contact is inserted into the profile, the provider will check for the existence of a profile on the device. If one is found, the raw contact's RawContacts.CONTACT_ID column gets the _ID of the profile Contact. If no match is found, the profile Contact is created and its _ID is put into the RawContacts.CONTACT_ID column of the newly inserted raw contact.","title":"Insert device owner Contact profile"},{"location":"profile/insert-profile/#insert-the-device-owner-contact-profile","text":"This library provides the ProfileInsert API that allows you to insert one or more RawContacts and Data. \u2139\ufe0f There can be only one device owner Contact, which is either set (not null) or not yet set (null). However, like other regular Contacts, the Profile Contact may have one or more RawContacts. An instance of the ProfileInsert API is obtained by, val insert = Contacts ( context ). profile (). insert () \u2139\ufe0f If you want to create/insert non-Profile Contacts, read Insert contacts .","title":"Insert the device owner Contact profile"},{"location":"profile/insert-profile/#a-basic-insert","text":"To create/insert a raw contact with a name of \"John Doe\" who works at Amazon with a work email of \"john.doe@amazon.com\" (in Kotlin), val insertResult = Contacts ( context ) . profile () . insert () . rawContacts ( NewRawContact (). apply { name = NewName (). apply { givenName = \"John\" familyName = \"Doe\" } organization = NewOrganization (). apply { company = \"Amazon\" title = \"Superstar\" } emails . add ( NewEmail (). apply { address = \"john.doe@amazon.com\" type = EmailEntity . Type . WORK }) }) . commit () Or alternatively, in a more Kotlinized style using named arguments, val insertResult = Contacts ( context ) . profile () . insert () . rawContacts ( NewRawContact ( name = NewName ( givenName = \"John\" , familyName = \"Doe\" ), organization = NewOrganization ( company = \"Amazon\" , title = \"Superstar\" ), emails = mutableListOf ( NewEmail ( address = \"john.doe@amazon.com\" , type = EmailEntity . Type . WORK )) )) . commit () Or alternatively, using extension functions, val insertResult = Contacts ( context ) . profile () . insert () . rawContact { setName { givenName = \"John\" familyName = \"Doe\" } setOrganization { company = \"Amazon\" title = \"Superstar\" } addEmail { address = \"john.doe@amazon.com\" type = EmailEntity . Type . WORK } } . commit ()","title":"A basic insert"},{"location":"profile/insert-profile/#allowing-blanks","text":"The API allows you to specify if you want to be able to insert blank contacts or not, . allowBlanks ( true | false ) For more info, read about Blank contacts .","title":"Allowing blanks"},{"location":"profile/insert-profile/#blank-data-are-not-inserted","text":"Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are ignored and are not inserted by insert APIs. For more info, read about Blank data .","title":"Blank data are not inserted"},{"location":"profile/insert-profile/#allowing-multiple-rawcontacts-per-account","text":"The API allows you to insert a profile RawContact with an Account that already has a profile RawContact, . allowMultipleRawContactsPerAccount ( true | false ) According to the ContactsContract.Profile documentation; ... each account (including data set, if applicable) on the device may contribute a single raw contact representing the user's personal profile data from that source. In other words, one account can have one profile RawContact. However, despite the documentation of \"one profile RawContact per one Account\", the Contacts Provider allows for multiple RawContacts per Account, including multiple local RawContacts (no Account).","title":"Allowing multiple RawContacts per Account"},{"location":"profile/insert-profile/#associating-an-account","text":"New RawContacts can be associated with an Account in order to enable syncing, . forAccount ( account ) For example, to associated the new RawContact to an account, . forAccount ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) \u2139\ufe0f For more info, read Query for Accounts .","title":"Associating an Account"},{"location":"profile/insert-profile/#local-rawcontacts","text":"If no Account is provided, or null is provided, or if an incorrect account is provided, the RawContacts inserted will not be associated with an Account. RawContacts inserted without an associated account are considered local or device-only contacts, which are not synced. \u2139\ufe0f For more info, read Sync contact data across devices . There are also certain data kinds that are ignored on insert or update if the RawContact is local. \u2139\ufe0f For more info, read about Local (device-only) contacts .","title":"Local RawContacts"},{"location":"profile/insert-profile/#including-only-specific-data","text":"To include only the given set of fields (data) in each of the insert operation, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations .","title":"Including only specific data"},{"location":"profile/insert-profile/#executing-the-insert","text":"To execute the insert, . commit ()","title":"Executing the insert"},{"location":"profile/insert-profile/#handling-the-insert-result","text":"The commit function returns a Result , val contactsApi = Contacts ( context ) val newRawContact = NewRawContact (...) val insertResult = contactsApi . profile () . insert () . rawContact ( newRawContact ) . commit () To check if the insert succeeded, val insertSucess = insertResult . isSuccessful To get the RawContact IDs of the newly created RawContact, val rawContactId = insertResult . rawContactId Once you have the RawContact ID, you can retrieve the newly created Contact via the Query API, val contacts = contactsApi . query () . where { RawContact . Id equalTo rawContactId } . find () \u2139\ufe0f For more info, read Query contacts (advanced) . Alternatively, you may use the extensions provided in ProfileInsertResult . To get the newly created Contact, val contact = insertResult . contact ( contactsApi ) To instead get the RawContact directly, val rawContacts = insertResult . rawContact ( contactsApi )","title":"Handling the insert result"},{"location":"profile/insert-profile/#cancelling-the-insert","text":"To cancel an insert amid execution, . commit { returnTrueIfInsertShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel insert processing as soon as possible. The function is called numerous times during insert processing to check if processing should stop or continue. This gives you the option to cancel the insert. For example, to automatically cancel the insert inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val insertResult = insert . commit { ! isActive } } }","title":"Cancelling the insert"},{"location":"profile/insert-profile/#performing-the-insert-and-result-processing-asynchronously","text":"Inserts are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the insert and result processing asynchronously"},{"location":"profile/insert-profile/#performing-the-insert-with-permission","text":"Inserts require the android.permission.WRITE_CONTACTS and android.permission.GET_ACCOUNTS permissions. If not granted, the insert will do nothing and return a failed result. \u2139\ufe0f For API 22 and below, the permission \"android.permission.WRITE_PROFILE\" is also required but only at the manifest level. Prior to API 23 (Marshmallow), permissions needed to be granted prior to installation instead of at runtime. To perform the insert with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the insert with permission"},{"location":"profile/insert-profile/#custom-data-support","text":"The ProfileInsert API supports custom data. For more info, read Insert custom data into new or existing contacts .","title":"Custom data support"},{"location":"profile/insert-profile/#rawcontact-and-contact-aggregation","text":"As per documentation in android.provider.ContactsContract.Profile , The user's profile entry cannot be created explicitly (attempting to do so will throw an exception). When a raw contact is inserted into the profile, the provider will check for the existence of a profile on the device. If one is found, the raw contact's RawContacts.CONTACT_ID column gets the _ID of the profile Contact. If no match is found, the profile Contact is created and its _ID is put into the RawContacts.CONTACT_ID column of the newly inserted raw contact.","title":"RawContact and Contact aggregation"},{"location":"profile/query-profile/","text":"Query device owner Contact profile \u00b6 This library provides the ProfileQuery API that allows you to get the device owner Profile Contact. \u2139\ufe0f There can be only one device owner Contact, which is either set (not null) or not yet set (null). However, like other regular Contacts, the Profile Contact may have one or more RawContacts. An instance of the ProfileQuery API is obtained by, val query = Contacts ( context ). profile (). query () \u2139\ufe0f If you want to get non-Profile Contacts, read Query contacts and Query contacts (advanced) . A basic query \u00b6 To get the profile Contact, val profileContact = Contacts ( context ). profile (). query (). find (). contact Including blank (raw) contacts \u00b6 The API allows you to specify if you want to include blank (raw) contacts or not, . includeBlanks ( true | false ) For more info, read about Blank contacts . Specifying Accounts \u00b6 To only include RawContacts associated with one of the given accounts, . accounts ( accounts ) For example, to include only RawContacts belonging to only one account, . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) \u2139\ufe0f For more info, read Query for Accounts . The RawContacts returned will only belong to the specified accounts. If no accounts are specified (this function is not called or called with no Accounts), then all RawContacts are included in the returned Contact. A null Account may be provided here, which results in RawContacts with no associated Account to be included. RawContacts without an associated account are considered local contacts or device-only contacts, which are not synced. For more info, read about Local (device-only) contacts . \u2139\ufe0f This may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it. Including only specific data \u00b6 To include only the given set of fields (data) in each of the Profile Contact, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations . Cancelling the query \u00b6 To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val profile = query . find { ! isActive } } } Performing the query asynchronously \u00b6 Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes. Performing the query with permission \u00b6 Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return null. \u2139\ufe0f For API 22 and below, the permission \"android.permission.READ_PROFILE\" is also required but only at the manifest level. Prior to API 23 (Marshmallow), permissions needed to be granted prior to installation instead of at runtime. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Custom data support \u00b6 The ProfilQuery API supports custom data. For more info, read Query custom data .","title":"Query device owner Contact profile"},{"location":"profile/query-profile/#query-device-owner-contact-profile","text":"This library provides the ProfileQuery API that allows you to get the device owner Profile Contact. \u2139\ufe0f There can be only one device owner Contact, which is either set (not null) or not yet set (null). However, like other regular Contacts, the Profile Contact may have one or more RawContacts. An instance of the ProfileQuery API is obtained by, val query = Contacts ( context ). profile (). query () \u2139\ufe0f If you want to get non-Profile Contacts, read Query contacts and Query contacts (advanced) .","title":"Query device owner Contact profile"},{"location":"profile/query-profile/#a-basic-query","text":"To get the profile Contact, val profileContact = Contacts ( context ). profile (). query (). find (). contact","title":"A basic query"},{"location":"profile/query-profile/#including-blank-raw-contacts","text":"The API allows you to specify if you want to include blank (raw) contacts or not, . includeBlanks ( true | false ) For more info, read about Blank contacts .","title":"Including blank (raw) contacts"},{"location":"profile/query-profile/#specifying-accounts","text":"To only include RawContacts associated with one of the given accounts, . accounts ( accounts ) For example, to include only RawContacts belonging to only one account, . accounts ( Account ( \"john.doe@gmail.com\" , \"com.google\" )) \u2139\ufe0f For more info, read Query for Accounts . The RawContacts returned will only belong to the specified accounts. If no accounts are specified (this function is not called or called with no Accounts), then all RawContacts are included in the returned Contact. A null Account may be provided here, which results in RawContacts with no associated Account to be included. RawContacts without an associated account are considered local contacts or device-only contacts, which are not synced. For more info, read about Local (device-only) contacts . \u2139\ufe0f This may affect performance. This may require one or more additional queries, internally performed in this function, which increases the time required for the search. Therefore, you should only specify this if you actually need it.","title":"Specifying Accounts"},{"location":"profile/query-profile/#including-only-specific-data","text":"To include only the given set of fields (data) in each of the Profile Contact, . include ( fields ) For example, to only include email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations .","title":"Including only specific data"},{"location":"profile/query-profile/#cancelling-the-query","text":"To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val profile = query . find { ! isActive } } }","title":"Cancelling the query"},{"location":"profile/query-profile/#performing-the-query-asynchronously","text":"Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap, which includes APIs for listening to Contacts database changes.","title":"Performing the query asynchronously"},{"location":"profile/query-profile/#performing-the-query-with-permission","text":"Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return null. \u2139\ufe0f For API 22 and below, the permission \"android.permission.READ_PROFILE\" is also required but only at the manifest level. Prior to API 23 (Marshmallow), permissions needed to be granted prior to installation instead of at runtime. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the query with permission"},{"location":"profile/query-profile/#custom-data-support","text":"The ProfilQuery API supports custom data. For more info, read Query custom data .","title":"Custom data support"},{"location":"profile/update-profile/","text":"Update device owner Contact profile \u00b6 This library provides the ProfileUpdate API that allows you to update the Profile contact in the Contacts Provider database to ensure that it contains the same data as the contact and raw contacts you have in memory. \u2139\ufe0f There can be only one device owner Contact, which is either set (not null) or not yet set (null). However, like other regular Contacts, the Profile Contact may have one or more RawContacts. An instance of the ProfileUpdate API is obtained by, val update = Contacts ( context ). profile (). update () If you want to update non-Profile Contacts, read Update contacts . A basic update \u00b6 To update the profile Contact and all of its RawContacts, val updateResult = Contacts ( context ) . profile () . update () . contact ( profile . mutableCopy { // make changes }) . commit () To update a profile RawContact directly, val updateResult = Contacts ( context ) . update () . rawContacts ( profile . rawContacts . first (). mutableCopy { // make changes }) . commit () Deleting blanks \u00b6 The API allows you to specify if you want the update operation to delete blank RawContacts or not, . deleteBlanks ( true | false ) For more info, read about Blank contacts . Blank data are deleted \u00b6 Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are deleted by update APIs, unless the corresponding fields are not included in the operation. For more info, read about Blank data . Including only specific data \u00b6 To perform update operations only the given set of fields (data), . include ( fields ) For example, to perform updates on only email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations . Executing the update \u00b6 To execute the update, . commit () Handling the update result \u00b6 The commit function returns a Result , val contactsApi = Contacts ( context ) val mutableProfile = profile . mutableCopy { ... } val updateResult = contactsApi . profile () . update () . contact ( mutableProfile ) . commit () To check if all updates succeeded, val allUpdatesSuccessful = updateResult . isSuccessful To check if a particular update succeeded, val firstUpdateSuccessful = updateResult . isSuccessful ( mutableProfile . rawContacts . first ()) Once you have performed the updates, you can retrieve the updated profile Contact reference via the Query API, val updatedProfile = Contacts ( context ). profile (). query (). find () \u2139\ufe0f For more info, read Query device owner Contact profile . Alternatively, you may use the extensions provided in ContactRefresh and RawContactRefresh . To get the updated profile Contact and all of its RawContacts and Data, val updatedProfile = profile . refresh ( contactsApi ) To get an updated RawContact and Data, val updatedProfileRawContact = profile . rawContacts . first (). refresh ( contactsApi ) Cancelling the update \u00b6 To cancel an update amid execution, . commit { returnTrueIfUpdateShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel update processing as soon as possible. The function is called numerous times during update processing to check if processing should stop or continue. This gives you the option to cancel the update. For example, to automatically cancel the update inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val updateResult = update . commit { ! isActive } } } Performing the update and result processing asynchronously \u00b6 Updates are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the update with permission \u00b6 Updates require the android.permission.WRITE_CONTACTS permissions. If not granted, the update will do nothing and return a failed result. \u2139\ufe0f For API 22 and below, the permission \"android.permission.WRITE_PROFILE\" is also required but only at the manifest level. Prior to API 23 (Marshmallow), permissions needed to be granted prior to installation instead of at runtime. To perform the update with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Custom data support \u00b6 The ProfileUpdate API supports custom data. For more info, read Update custom data . Modifiable Contact fields \u00b6 As per documentation in android.provider.ContactsContract.Profile , The profile Contact has the same update restrictions as Contacts in general... Only certain columns of Contact are modifiable: STARRED, CUSTOM_RINGTONE, SEND_TO_VOICEMAIL. Changing any of these columns on the Contact also changes them on all constituent raw contacts.","title":"Update device owner Contact profile"},{"location":"profile/update-profile/#update-device-owner-contact-profile","text":"This library provides the ProfileUpdate API that allows you to update the Profile contact in the Contacts Provider database to ensure that it contains the same data as the contact and raw contacts you have in memory. \u2139\ufe0f There can be only one device owner Contact, which is either set (not null) or not yet set (null). However, like other regular Contacts, the Profile Contact may have one or more RawContacts. An instance of the ProfileUpdate API is obtained by, val update = Contacts ( context ). profile (). update () If you want to update non-Profile Contacts, read Update contacts .","title":"Update device owner Contact profile"},{"location":"profile/update-profile/#a-basic-update","text":"To update the profile Contact and all of its RawContacts, val updateResult = Contacts ( context ) . profile () . update () . contact ( profile . mutableCopy { // make changes }) . commit () To update a profile RawContact directly, val updateResult = Contacts ( context ) . update () . rawContacts ( profile . rawContacts . first (). mutableCopy { // make changes }) . commit ()","title":"A basic update"},{"location":"profile/update-profile/#deleting-blanks","text":"The API allows you to specify if you want the update operation to delete blank RawContacts or not, . deleteBlanks ( true | false ) For more info, read about Blank contacts .","title":"Deleting blanks"},{"location":"profile/update-profile/#blank-data-are-deleted","text":"Blank data are data entities that have only null, empty, or blank primary value(s). Blanks are deleted by update APIs, unless the corresponding fields are not included in the operation. For more info, read about Blank data .","title":"Blank data are deleted"},{"location":"profile/update-profile/#including-only-specific-data","text":"To perform update operations only the given set of fields (data), . include ( fields ) For example, to perform updates on only email and name fields, . include { Email . all + Name . all } For more info, read Include only certain fields for read and write operations .","title":"Including only specific data"},{"location":"profile/update-profile/#executing-the-update","text":"To execute the update, . commit ()","title":"Executing the update"},{"location":"profile/update-profile/#handling-the-update-result","text":"The commit function returns a Result , val contactsApi = Contacts ( context ) val mutableProfile = profile . mutableCopy { ... } val updateResult = contactsApi . profile () . update () . contact ( mutableProfile ) . commit () To check if all updates succeeded, val allUpdatesSuccessful = updateResult . isSuccessful To check if a particular update succeeded, val firstUpdateSuccessful = updateResult . isSuccessful ( mutableProfile . rawContacts . first ()) Once you have performed the updates, you can retrieve the updated profile Contact reference via the Query API, val updatedProfile = Contacts ( context ). profile (). query (). find () \u2139\ufe0f For more info, read Query device owner Contact profile . Alternatively, you may use the extensions provided in ContactRefresh and RawContactRefresh . To get the updated profile Contact and all of its RawContacts and Data, val updatedProfile = profile . refresh ( contactsApi ) To get an updated RawContact and Data, val updatedProfileRawContact = profile . rawContacts . first (). refresh ( contactsApi )","title":"Handling the update result"},{"location":"profile/update-profile/#cancelling-the-update","text":"To cancel an update amid execution, . commit { returnTrueIfUpdateShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel update processing as soon as possible. The function is called numerous times during update processing to check if processing should stop or continue. This gives you the option to cancel the update. For example, to automatically cancel the update inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val updateResult = update . commit { ! isActive } } }","title":"Cancelling the update"},{"location":"profile/update-profile/#performing-the-update-and-result-processing-asynchronously","text":"Updates are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the update and result processing asynchronously"},{"location":"profile/update-profile/#performing-the-update-with-permission","text":"Updates require the android.permission.WRITE_CONTACTS permissions. If not granted, the update will do nothing and return a failed result. \u2139\ufe0f For API 22 and below, the permission \"android.permission.WRITE_PROFILE\" is also required but only at the manifest level. Prior to API 23 (Marshmallow), permissions needed to be granted prior to installation instead of at runtime. To perform the update with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the update with permission"},{"location":"profile/update-profile/#custom-data-support","text":"The ProfileUpdate API supports custom data. For more info, read Update custom data .","title":"Custom data support"},{"location":"profile/update-profile/#modifiable-contact-fields","text":"As per documentation in android.provider.ContactsContract.Profile , The profile Contact has the same update restrictions as Contacts in general... Only certain columns of Contact are modifiable: STARRED, CUSTOM_RINGTONE, SEND_TO_VOICEMAIL. Changing any of these columns on the Contact also changes them on all constituent raw contacts.","title":"Modifiable Contact fields"},{"location":"setup/installation/","text":"Installation guide \u00b6 \u2139\ufe0f This library is a multi-module project published with JitPack First, include JitPack in the repositories list, repositories { maven { url \"https://jitpack.io\" } } To install individual modules, dependencies { implementation 'com.github.vestrel00.contacts-android:core:' implementation 'com.github.vestrel00.contacts-android:async:' implementation 'com.github.vestrel00.contacts-android:customdata-gender:' implementation 'com.github.vestrel00.contacts-android:customdata-googlecontacts:' implementation 'com.github.vestrel00.contacts-android:customdata-handlename:' implementation 'com.github.vestrel00.contacts-android:customdata-pokemon:' implementation 'com.github.vestrel00.contacts-android:customdata-rpg:' implementation 'com.github.vestrel00.contacts-android:debug:' implementation 'com.github.vestrel00.contacts-android:permissions:' implementation 'com.github.vestrel00.contacts-android:test:' implementation 'com.github.vestrel00.contacts-android:ui:' // Notice that when installing individual modules, the first \":\" comes after \"contacts-android\". } The core module is really all you need. All other modules are optional. It is recommended that you install individual modules to make sure that unused code is not included in your application, which will increase your app's APK size. If you still want to install all modules in a single line, read the Installing all modules in one line section below. Modules \u00b6 Here is a brief description of the individual modules you can install. core : All of the contacts management APIs the library has to offer. This is the only required module. All other modules are optional . async : Extension functions for executing core API functions asynchronously using Kotlin Coroutines . permissions : Extension functions for executing core API functions with permissions granted using Kotlin Coroutines . test : APIs for mocking core APIs during tests or at production runtime. debug : Extension functions for logging internal database tables into the Logcat and other debugging related stuff. This is only meant for development use . ui : Rudimentary UI views and functions that are already integrated with the core APIs. You may use these for rapid prototyping or just for reference . customdata-gender : Custom data for gender . customdata-googlecontacts : Custom data managed by the Google Contacts app . customdata-handlename : Custom data for handle . customdata-pokemon : Custom data for pokemon . customdata-rpg : Custom data for role playing games (RPG) . Installing all modules in one line \u00b6 To install all modules in a single line, dependencies { implementation 'com.github.vestrel00:contacts-android:' // Notice that when installing all modules, the first \":\" comes after \"vestrel00\". } Starting with version 0.2.0 , installing all modules in a single line is only supported when using the dependencyResolutionManagement in settings.gradle . In your settings.gradle , dependencyResolutionManagement { repositoriesMode . set ( RepositoriesMode . FAIL_ON_PROJECT_REPOS ) repositories { maven { url \"https://jitpack.io\" } } } For versions 0.1.10 and below, you can still install all modules in a single line using the old common method of dependency resolution. In your root build.gradle , allprojects { repositories { maven { url \"https://jitpack.io\" } } }","title":"Installation"},{"location":"setup/installation/#installation-guide","text":"\u2139\ufe0f This library is a multi-module project published with JitPack First, include JitPack in the repositories list, repositories { maven { url \"https://jitpack.io\" } } To install individual modules, dependencies { implementation 'com.github.vestrel00.contacts-android:core:' implementation 'com.github.vestrel00.contacts-android:async:' implementation 'com.github.vestrel00.contacts-android:customdata-gender:' implementation 'com.github.vestrel00.contacts-android:customdata-googlecontacts:' implementation 'com.github.vestrel00.contacts-android:customdata-handlename:' implementation 'com.github.vestrel00.contacts-android:customdata-pokemon:' implementation 'com.github.vestrel00.contacts-android:customdata-rpg:' implementation 'com.github.vestrel00.contacts-android:debug:' implementation 'com.github.vestrel00.contacts-android:permissions:' implementation 'com.github.vestrel00.contacts-android:test:' implementation 'com.github.vestrel00.contacts-android:ui:' // Notice that when installing individual modules, the first \":\" comes after \"contacts-android\". } The core module is really all you need. All other modules are optional. It is recommended that you install individual modules to make sure that unused code is not included in your application, which will increase your app's APK size. If you still want to install all modules in a single line, read the Installing all modules in one line section below.","title":"Installation guide"},{"location":"setup/installation/#modules","text":"Here is a brief description of the individual modules you can install. core : All of the contacts management APIs the library has to offer. This is the only required module. All other modules are optional . async : Extension functions for executing core API functions asynchronously using Kotlin Coroutines . permissions : Extension functions for executing core API functions with permissions granted using Kotlin Coroutines . test : APIs for mocking core APIs during tests or at production runtime. debug : Extension functions for logging internal database tables into the Logcat and other debugging related stuff. This is only meant for development use . ui : Rudimentary UI views and functions that are already integrated with the core APIs. You may use these for rapid prototyping or just for reference . customdata-gender : Custom data for gender . customdata-googlecontacts : Custom data managed by the Google Contacts app . customdata-handlename : Custom data for handle . customdata-pokemon : Custom data for pokemon . customdata-rpg : Custom data for role playing games (RPG) .","title":"Modules"},{"location":"setup/installation/#installing-all-modules-in-one-line","text":"To install all modules in a single line, dependencies { implementation 'com.github.vestrel00:contacts-android:' // Notice that when installing all modules, the first \":\" comes after \"vestrel00\". } Starting with version 0.2.0 , installing all modules in a single line is only supported when using the dependencyResolutionManagement in settings.gradle . In your settings.gradle , dependencyResolutionManagement { repositoriesMode . set ( RepositoriesMode . FAIL_ON_PROJECT_REPOS ) repositories { maven { url \"https://jitpack.io\" } } } For versions 0.1.10 and below, you can still install all modules in a single line using the old common method of dependency resolution. In your root build.gradle , allprojects { repositories { maven { url \"https://jitpack.io\" } } }","title":"Installing all modules in one line"},{"location":"setup/setup-contacts-api/","text":"Contacts API Setup \u00b6 The main library functions are all accessible via the contacts.core.Contacts API. There's no setup required. Just create an instance of Contacts and the world of contacts is at your disposal =) In Kotlin, Contacts ( context ) in Java, ContactsFactory . create ( context ); \u2139\ufe0f The context parameter can come from anywhere; Application, Activity, Fragment, or View. It does not matter what context you pass in. The API will only use and store the Application context, to avoid leaks. It's up to you if you just want to create instances on demand. Or, hold on to instances as a singleton that is injected to your dependency graph (via something like dagger , hilt , or koin ), which will make black box testing and white box testing a walk in the park! Logging support \u00b6 Instances of Contacts hold on to a Logger for logging support. For more info, read Log API input and output . Custom data integration \u00b6 Instances of Contacts hold on to an instance of CustomDataRegistry for custom data integration. For more info, read Integrate custom data . Optional, but recommended setup \u00b6 It is recommended to use a single instance of the Contacts API throughout your application using dependency injection . This will allow you to; Use the same Contacts API instance throughout your app. This especially important when integrating custom data. Easily substitute your Contacts API instance with an instance of TestContacts . This is useful in black box testing (UI instrumentation tests; androidTest/ ). It may also be used in your production apps \"test mode\". Easily substitute your Contacts API instance with an instance of MockContacts This is useful in white box testing (unit & integration tests; test/ ). For more info, read Contacts API Testing . \u2139\ufe0f This library does not (and will not) force you to do things you don't want. If you don't care about all of the above and just want to get out a quick prototype of a feature in your app or an entire app, then go for it!","title":"Contacts API Setup"},{"location":"setup/setup-contacts-api/#contacts-api-setup","text":"The main library functions are all accessible via the contacts.core.Contacts API. There's no setup required. Just create an instance of Contacts and the world of contacts is at your disposal =) In Kotlin, Contacts ( context ) in Java, ContactsFactory . create ( context ); \u2139\ufe0f The context parameter can come from anywhere; Application, Activity, Fragment, or View. It does not matter what context you pass in. The API will only use and store the Application context, to avoid leaks. It's up to you if you just want to create instances on demand. Or, hold on to instances as a singleton that is injected to your dependency graph (via something like dagger , hilt , or koin ), which will make black box testing and white box testing a walk in the park!","title":"Contacts API Setup"},{"location":"setup/setup-contacts-api/#logging-support","text":"Instances of Contacts hold on to a Logger for logging support. For more info, read Log API input and output .","title":"Logging support"},{"location":"setup/setup-contacts-api/#custom-data-integration","text":"Instances of Contacts hold on to an instance of CustomDataRegistry for custom data integration. For more info, read Integrate custom data .","title":"Custom data integration"},{"location":"setup/setup-contacts-api/#optional-but-recommended-setup","text":"It is recommended to use a single instance of the Contacts API throughout your application using dependency injection . This will allow you to; Use the same Contacts API instance throughout your app. This especially important when integrating custom data. Easily substitute your Contacts API instance with an instance of TestContacts . This is useful in black box testing (UI instrumentation tests; androidTest/ ). It may also be used in your production apps \"test mode\". Easily substitute your Contacts API instance with an instance of MockContacts This is useful in white box testing (unit & integration tests; test/ ). For more info, read Contacts API Testing . \u2139\ufe0f This library does not (and will not) force you to do things you don't want. If you don't care about all of the above and just want to get out a quick prototype of a feature in your app or an entire app, then go for it!","title":"Optional, but recommended setup"},{"location":"sim/about-sim-contacts/","text":"SIM Contacts \u00b6 This library gives you APIs that allow you to read and write Contacts stored in the SIM card. SimContactsQuery SimContactsInsert SimContactsUpdate SimContactsDelete SIM Contact data \u00b6 SIM Contact data consists of the name and number . \u2139\ufe0f Support for email was recently added in Android 12. I don't think it is stable yet. Regardless, it is too new so this library will wait a bit before adding support for it. Character limits \u00b6 The name and number are subject to the SIM card's maximum character limit, which is typically around 20-30 characters (in modern times). This may vary per SIM card. Inserts or updates will fail if the limit is breached. SIM Contact row ID \u00b6 The SIM contact that an ID is pointing to may change if the contact is deleted in the database and another contact is inserted. The inserted contact may be assigned the ID of the deleted contact. DO NOT RELY ON THIS TO MATCH VALUES IN THE DATABASE! The SIM table does not support selection by ID so you can't use this for anything anyways. Some OEMs automatically sync SIM card data with Contacts Provider data \u00b6 Samsung phones import contacts from SIM into the Contacts Provider. When using the builtin Samsung Contacts app, modifications made to the SIM contacts from the Contacts Provider are propagated to the SIM card and vice versa. Samsung is most likely syncing the SIM contacts with the copy in the Contacts Provider via SyncAdapters. The RawContacts created in the Contacts Provider have a non-remote account name and type (pointing to the SIM card), accountName: primary.sim.account_name, accountType: vnd.sec.contact.sim Furthermore, SIM contacts imported into the Contacts Provider have the same restrictions as the SIM card in that only columns available in the SIM are editable (_id, name, number, emails). Editing SIM contacts using 3rd party apps such as the Google Contacts app are not supported. If you find any issues when using the SimContacts APIs, please raise an issue if you find any bugs or start a discussion and share your thoughts or knowledge =) Multi SIM card support \u00b6 Android 5.1 adds support for using more than one cellular carrier SIM card at a time . This feature lets users activate and use additional SIMs on devices that have two or more SIM card slots. The APIs in this library have not been tested against dual SIM card configurations. It should still work, at the very least the current active SIM card should be accessible. Please raise an issue if you find any bugs or start a discussion and share your thoughts or knowledge =) Limitations \u00b6 Projections, selections, and order is not supported by the IccProvider . Therefore, we are unable to provide include , where , orderBy , limit , and offset functions in our SimContactsQuery API. Due to all of these limitations, all queries will return all contacts in the SIM card. Consumers of this library can perform their own sorting and pagination if they wish. Depending on memory size, SIM cards can hold 200 to 500+ contacts . The most common being around 250. Most, if not all, SIM cards have less than 1mb memory (averaging 32KB to 64KB). Therefore, memory and speed should not be affected much by not being able to sort/order and paginate at the query level. Debugging \u00b6 To look at all of the rows in the SIM Contacts table, use the Context.logSimContactsTable function in the debug module. For more info, read Debug the Sim Contacts table . Known issues \u00b6 Samsung phones (and perhaps other OEMs) support emails (in addition to name and number) data ahead of the Android 12 release. Updating and deleting SIM contacts that have email data using the APIs provided in this library may fail. This issue does not occur when moving the SIM card to a different phone that does not support emails. Developer notes (or for advanced users) \u00b6 In building the SimContacts APIs provided in this library, I used the following hardware to observe the behavior of reading/writing to the SIM card. Smart phones Non-smart phones SIM cards Nexus 6P (Android 8) BLU Z5 (unknown OS) Mint Mobile Samsung Galaxy A71 (Android 11) For software, I used the following apps. Apps Smart phones SIM Card Info v1.1.6 Nexus 6P Samsung Contacts v12.7.10.12 Samsung Galaxy A71 \u2139\ufe0f The AOSP Contacts app and Google Contacts app can only import contacts from SIM card so they are not very helpful for us with this investigation. For Android code references, I used the internal IccProvider.java as reference to what the Android OS might be doing when 3rd party applications perform CRUD operations on SIM contacts. IccProvider @ Android 8 IccProvider @ Android 11 IccProvider @ Android 12 I'm using the content://icc/adn URI to read/write from/to SIM card. \u2139\ufe0f All of the investigation that I have done here may not apply for all SIM cards and phone OEMs! There is just way too many different SIM cards and phones out there for a single person (me) to test. However, I think that my findings should apply to most cases. Figuring out how to perform CRUD operations \u00b6 First, I added 20 contacts (name and number) to the SIM contacts using the BLU Z5 . The first contact is named \"a\" with number \"1\", the second is named \"ab\" with number \"12\", and so on. The last contact is named \"abcdefghijklmnopqrst\" with number \"12345678901234567890\". I did this because the BLU Z5 has determined that the maximum character limit for the name and number for my Mint Mobile SIM card is 20. \u2139\ufe0f The character limits are most likely set by the SIM card and/or calculated by the OS managing it based on how much total memory is available. I also added a contact named \"bro\" with no number and a nameless contact with with number \"5555555555\". For a total of 22 contacts in the SIM card. I loaded the SIM card to my Nexus 6P . Then, I logged all of the rows in content://icc/adn using the Context.logSimContactsTable debug function I wrote up in the debug module. SIM Contact id: 0, name: A, number: 1, emails: null SIM Contact id: 1, name: Ab, number: 12, emails: null SIM Contact id: 2, name: Abc, number: 123, emails: null SIM Contact id: 3, name: Abcd, number: 1234, emails: null SIM Contact id: 4, name: Abcde, number: 12345, emails: null SIM Contact id: 5, name: Abcdef, number: 123456, emails: null SIM Contact id: 6, name: Abcdefg, number: 1234567, emails: null SIM Contact id: 7, name: Abcdefgh, number: 12345678, emails: null SIM Contact id: 8, name: Abcdefghi, number: 123456789, emails: null SIM Contact id: 9, name: Abcdefghij, number: 1234567890, emails: null SIM Contact id: 10, name: Abcdefghijk, number: 12345678901, emails: null SIM Contact id: 11, name: Abcdefghijkl, number: 123456789012, emails: null SIM Contact id: 12, name: Abcdefghijklm, number: 1234567890123, emails: null SIM Contact id: 13, name: Abcdefghijklmn, number: 12345678901234, emails: null SIM Contact id: 14, name: Abcdefghijklmno, number: 123456789012345, emails: null SIM Contact id: 15, name: Abcdefghijklmnop, number: 1234567890123456, emails: null SIM Contact id: 16, name: Abcdefghijklmnopq, number: 12345678901234567, emails: null SIM Contact id: 17, name: Abcdefghijklmnopqr, number: 123456789012345678, emails: null SIM Contact id: 18, name: Abcdefghijklmnopqrs, number: 1234567890123456789, emails: null SIM Contact id: 19, name: Abcdefghijklmnopqrst, number: 12345678901234567890, emails: null SIM Contact id: 20, name: Bro, number: , emails: null SIM Contact id: 21, name: , number: 5555555555, emails: null Our SimContactsQuery also retrieves the same exact results! I am able to see all of the contacts in the SIM Info app except for the nameless contact with number \"5555555555\". I attempted to add a nameless contact using the SIM Info app but it does not allow reading/writing nameless contacts. \u2139\ufe0f This is probably a bug in the SIM Info app or a limitation that is intentionally imposed for some reason. I wish I could see the source code of the app! Deleting the first contact with ID of 0 using the SIM Info app works just fine. Deleting the contact with ID of 2 using our SimContactsDelete works just fine too. At this point the first 5 rows in the table are; SIM Contact id: 1, name: Ab, number: 12, emails: null SIM Contact id: 3, name: Abcd, number: 1234, emails: null SIM Contact id: 4, name: Abcde, number: 12345, emails: null SIM Contact id: 5, name: Abcdef, number: 123456, emails: null SIM Contact id: 6, name: Abcdefg, number: 1234567, emails: null Inserting a contact using the SIM Info app and our SimContactsInsert (in that order) works just fine, resulting in two new rows being added. One very interesting to note is that the IDs of the previously deleted rows (0 and 2) have been assigned to the newly inserted contacts! SIM Contact id: 0, name: SIM Info Contact, number: 8, emails: null SIM Contact id: 1, name: Ab, number: 12, emails: null SIM Contact id: 2, name: SimContactsInsert, number: 9, emails: null SIM Contact id: 3, name: Abcd, number: 1234, emails: null SIM Contact id: 4, name: Abcde, number: 12345, emails: null SIM Contact id: 5, name: Abcdef, number: 123456, emails: null SIM Contact id: 6, name: Abcdefg, number: 1234567, emails: null This means that the IDs should not be used as a reference to a particular contact because it could \"change\" in the process of deleting and inserting. As for updates, let's start with this table... SIM Contact id: 3, name: Abcd, number: 1234, emails: null SIM Contact id: 4, name: Abcde, number: 12345, emails: null Notice that Contact ID 0, 1, and 2 are available. Using the SIM Info app to \"update\" the contact with ID 4, we get... SIM Contact id: 3, name: Abcd, number: 1234, emails: null SIM Contact id: 4, name: xxx, number: 12345, emails: null The ID remains 4. We get the same result using our SimContactsUpdate API =) Thus, we have implemented CRUD APIs!!! Figuring out character limits \u00b6 The BLU Z5 non-smartphone has determined that the maximum character limit for the name and number for my Mint Mobile SIM card is 20. I inserted a contact with a name with 26 characters and another contact with a number with 21 characters using the SIM Info app. The first insert (26 char name) succeeded but the second failed (21 char number). SIM Contact id: 0, name: abcdefghijklmnopqrstuvwxyz, number: 1, emails: null I did the same using our SimContactsInsert ... The same thing occurred. This means that the character limit is imposed on the number but perhaps not the name OR maybe the name has not reached the maximum. I tried inserting a name with over 100 characters and it failed. So there is a character limit for the name. I tried inserting names of shorter and shorter lengths until I find the max. It seems to be 30 characters. The character limits for the name is different for my Mint Mobile SIM card is different in the BLU Z5 vs Nexus 6P . BLU Z5 Nexus 6P name 20 30 number 20 20 I took out the SIM card from the Nexus 6P and plugging it back into the BLU Z5 to see if it will show the contacts that go over the 20 character limit. Both contacts with names longer than 20 characters are shown in the BLU Z5 BUT the name is truncated to 20. This could mean one of two things; The phones determine the character limits based on SIM card memory. The SIM card specifies the character limits but the BLU Z5 hard codes it to 20 regardless. Time to check with the Samsung Galaxy A71 ! The Samsung yielded the same results as the Nexus. So, perhaps it is just the self-imposed limitation of the BLU phone. One interesting difference between the Samsung and the Nexus is that our SimContactsInsert was indicating that the insert succeeded in the Samsung even though no new row was created in the SIM table (oh Samsung lol). The result Uri returned by the insert operation is null in the Nexus but not null in the Samsung. What this all means? Our SimContactsInsert and SimContactsUpdate APIs need to be able to detect the maximum character limits for the name and number before performing the actual insert or update operation. To figure out the max character limits, we can attempt to insert a string of length 35 (most names should fit there and most SIM cards have lower limits). Keep attempting to insert until insert succeeds, making the string shorter each time. Delete the successful insert and record the length of the string. Do this for both name and number and store the results in shared preferences mapped to a unique ID of the SIM card. We do not want to do this calculation everytime our APIs are used! Max character limits should be exposed to our API users also. Furthermore, we cannot rely on the result of the insert operation alone. If the result Uri is not null, we must perform a query to sanity check that the actual name and number was inserted! Emails \u00b6 There is an \"emails\" column in the SIM table. CRUD operations for it was not officially supported until recently in Android 12. IccProvider @ Android 11 IccProvider @ Android 12 Look for \"TODO\" comments in the `IccProvider``. You will see TODOs for emails in Android 11 but not Android 12. On my Samsung Galaxy A71 running Android 11... The column name is actually \"emails\" with an \"s\" (plural). What I observed, no email = \",\" at least one email = \"email,\" There seems to be a trailing \",\" regardless. It seems like the emails are in CSV format (comma separated values). I was not able to delete rows with emails in them. I even tried updating the where clause used in our SimContactsDelete to include the email but it does not work. The builtin Samsung Contacts app is able to insert, update, and delete rows with emails. This probably means that we don't have access to the internal APIs that the Samsung Contacts app has. Keep in mind that my Samsung is running Android 11 and support for email was not added until Android 12. \u2139\ufe0f Classic Samsung to add features farther ahead of time than vanilla Android =) On my Nexus 6P running Android 8... The contacts with emails are shown without email data (emails are null in the SIM table). These rows are able to be updated and deleted. On my BLU Z5... SIM contacts with emails are shown without the email data. These rows are able to be updated and deleted. Other considerations \u00b6 It seems like there are new APIs around SIM Contacts that were introduced in API 31; https://developer.android.com/reference/android/provider/ContactsContract.SimContacts https://developer.android.com/reference/android/provider/SimPhonebookContract Those APIs are too new to be used by this library, which supports API levels down to 19. So, we'll stick with using the content://icc/adn uri to read/write to SIM card until it becomes deprecated, if ever.","title":"About SIM contacts"},{"location":"sim/about-sim-contacts/#sim-contacts","text":"This library gives you APIs that allow you to read and write Contacts stored in the SIM card. SimContactsQuery SimContactsInsert SimContactsUpdate SimContactsDelete","title":"SIM Contacts"},{"location":"sim/about-sim-contacts/#sim-contact-data","text":"SIM Contact data consists of the name and number . \u2139\ufe0f Support for email was recently added in Android 12. I don't think it is stable yet. Regardless, it is too new so this library will wait a bit before adding support for it.","title":"SIM Contact data"},{"location":"sim/about-sim-contacts/#character-limits","text":"The name and number are subject to the SIM card's maximum character limit, which is typically around 20-30 characters (in modern times). This may vary per SIM card. Inserts or updates will fail if the limit is breached.","title":"Character limits"},{"location":"sim/about-sim-contacts/#sim-contact-row-id","text":"The SIM contact that an ID is pointing to may change if the contact is deleted in the database and another contact is inserted. The inserted contact may be assigned the ID of the deleted contact. DO NOT RELY ON THIS TO MATCH VALUES IN THE DATABASE! The SIM table does not support selection by ID so you can't use this for anything anyways.","title":"SIM Contact row ID"},{"location":"sim/about-sim-contacts/#some-oems-automatically-sync-sim-card-data-with-contacts-provider-data","text":"Samsung phones import contacts from SIM into the Contacts Provider. When using the builtin Samsung Contacts app, modifications made to the SIM contacts from the Contacts Provider are propagated to the SIM card and vice versa. Samsung is most likely syncing the SIM contacts with the copy in the Contacts Provider via SyncAdapters. The RawContacts created in the Contacts Provider have a non-remote account name and type (pointing to the SIM card), accountName: primary.sim.account_name, accountType: vnd.sec.contact.sim Furthermore, SIM contacts imported into the Contacts Provider have the same restrictions as the SIM card in that only columns available in the SIM are editable (_id, name, number, emails). Editing SIM contacts using 3rd party apps such as the Google Contacts app are not supported. If you find any issues when using the SimContacts APIs, please raise an issue if you find any bugs or start a discussion and share your thoughts or knowledge =)","title":"Some OEMs automatically sync SIM card data with Contacts Provider data"},{"location":"sim/about-sim-contacts/#multi-sim-card-support","text":"Android 5.1 adds support for using more than one cellular carrier SIM card at a time . This feature lets users activate and use additional SIMs on devices that have two or more SIM card slots. The APIs in this library have not been tested against dual SIM card configurations. It should still work, at the very least the current active SIM card should be accessible. Please raise an issue if you find any bugs or start a discussion and share your thoughts or knowledge =)","title":"Multi SIM card support"},{"location":"sim/about-sim-contacts/#limitations","text":"Projections, selections, and order is not supported by the IccProvider . Therefore, we are unable to provide include , where , orderBy , limit , and offset functions in our SimContactsQuery API. Due to all of these limitations, all queries will return all contacts in the SIM card. Consumers of this library can perform their own sorting and pagination if they wish. Depending on memory size, SIM cards can hold 200 to 500+ contacts . The most common being around 250. Most, if not all, SIM cards have less than 1mb memory (averaging 32KB to 64KB). Therefore, memory and speed should not be affected much by not being able to sort/order and paginate at the query level.","title":"Limitations"},{"location":"sim/about-sim-contacts/#debugging","text":"To look at all of the rows in the SIM Contacts table, use the Context.logSimContactsTable function in the debug module. For more info, read Debug the Sim Contacts table .","title":"Debugging"},{"location":"sim/about-sim-contacts/#known-issues","text":"Samsung phones (and perhaps other OEMs) support emails (in addition to name and number) data ahead of the Android 12 release. Updating and deleting SIM contacts that have email data using the APIs provided in this library may fail. This issue does not occur when moving the SIM card to a different phone that does not support emails.","title":"Known issues"},{"location":"sim/about-sim-contacts/#developer-notes-or-for-advanced-users","text":"In building the SimContacts APIs provided in this library, I used the following hardware to observe the behavior of reading/writing to the SIM card. Smart phones Non-smart phones SIM cards Nexus 6P (Android 8) BLU Z5 (unknown OS) Mint Mobile Samsung Galaxy A71 (Android 11) For software, I used the following apps. Apps Smart phones SIM Card Info v1.1.6 Nexus 6P Samsung Contacts v12.7.10.12 Samsung Galaxy A71 \u2139\ufe0f The AOSP Contacts app and Google Contacts app can only import contacts from SIM card so they are not very helpful for us with this investigation. For Android code references, I used the internal IccProvider.java as reference to what the Android OS might be doing when 3rd party applications perform CRUD operations on SIM contacts. IccProvider @ Android 8 IccProvider @ Android 11 IccProvider @ Android 12 I'm using the content://icc/adn URI to read/write from/to SIM card. \u2139\ufe0f All of the investigation that I have done here may not apply for all SIM cards and phone OEMs! There is just way too many different SIM cards and phones out there for a single person (me) to test. However, I think that my findings should apply to most cases.","title":"Developer notes (or for advanced users)"},{"location":"sim/about-sim-contacts/#figuring-out-how-to-perform-crud-operations","text":"First, I added 20 contacts (name and number) to the SIM contacts using the BLU Z5 . The first contact is named \"a\" with number \"1\", the second is named \"ab\" with number \"12\", and so on. The last contact is named \"abcdefghijklmnopqrst\" with number \"12345678901234567890\". I did this because the BLU Z5 has determined that the maximum character limit for the name and number for my Mint Mobile SIM card is 20. \u2139\ufe0f The character limits are most likely set by the SIM card and/or calculated by the OS managing it based on how much total memory is available. I also added a contact named \"bro\" with no number and a nameless contact with with number \"5555555555\". For a total of 22 contacts in the SIM card. I loaded the SIM card to my Nexus 6P . Then, I logged all of the rows in content://icc/adn using the Context.logSimContactsTable debug function I wrote up in the debug module. SIM Contact id: 0, name: A, number: 1, emails: null SIM Contact id: 1, name: Ab, number: 12, emails: null SIM Contact id: 2, name: Abc, number: 123, emails: null SIM Contact id: 3, name: Abcd, number: 1234, emails: null SIM Contact id: 4, name: Abcde, number: 12345, emails: null SIM Contact id: 5, name: Abcdef, number: 123456, emails: null SIM Contact id: 6, name: Abcdefg, number: 1234567, emails: null SIM Contact id: 7, name: Abcdefgh, number: 12345678, emails: null SIM Contact id: 8, name: Abcdefghi, number: 123456789, emails: null SIM Contact id: 9, name: Abcdefghij, number: 1234567890, emails: null SIM Contact id: 10, name: Abcdefghijk, number: 12345678901, emails: null SIM Contact id: 11, name: Abcdefghijkl, number: 123456789012, emails: null SIM Contact id: 12, name: Abcdefghijklm, number: 1234567890123, emails: null SIM Contact id: 13, name: Abcdefghijklmn, number: 12345678901234, emails: null SIM Contact id: 14, name: Abcdefghijklmno, number: 123456789012345, emails: null SIM Contact id: 15, name: Abcdefghijklmnop, number: 1234567890123456, emails: null SIM Contact id: 16, name: Abcdefghijklmnopq, number: 12345678901234567, emails: null SIM Contact id: 17, name: Abcdefghijklmnopqr, number: 123456789012345678, emails: null SIM Contact id: 18, name: Abcdefghijklmnopqrs, number: 1234567890123456789, emails: null SIM Contact id: 19, name: Abcdefghijklmnopqrst, number: 12345678901234567890, emails: null SIM Contact id: 20, name: Bro, number: , emails: null SIM Contact id: 21, name: , number: 5555555555, emails: null Our SimContactsQuery also retrieves the same exact results! I am able to see all of the contacts in the SIM Info app except for the nameless contact with number \"5555555555\". I attempted to add a nameless contact using the SIM Info app but it does not allow reading/writing nameless contacts. \u2139\ufe0f This is probably a bug in the SIM Info app or a limitation that is intentionally imposed for some reason. I wish I could see the source code of the app! Deleting the first contact with ID of 0 using the SIM Info app works just fine. Deleting the contact with ID of 2 using our SimContactsDelete works just fine too. At this point the first 5 rows in the table are; SIM Contact id: 1, name: Ab, number: 12, emails: null SIM Contact id: 3, name: Abcd, number: 1234, emails: null SIM Contact id: 4, name: Abcde, number: 12345, emails: null SIM Contact id: 5, name: Abcdef, number: 123456, emails: null SIM Contact id: 6, name: Abcdefg, number: 1234567, emails: null Inserting a contact using the SIM Info app and our SimContactsInsert (in that order) works just fine, resulting in two new rows being added. One very interesting to note is that the IDs of the previously deleted rows (0 and 2) have been assigned to the newly inserted contacts! SIM Contact id: 0, name: SIM Info Contact, number: 8, emails: null SIM Contact id: 1, name: Ab, number: 12, emails: null SIM Contact id: 2, name: SimContactsInsert, number: 9, emails: null SIM Contact id: 3, name: Abcd, number: 1234, emails: null SIM Contact id: 4, name: Abcde, number: 12345, emails: null SIM Contact id: 5, name: Abcdef, number: 123456, emails: null SIM Contact id: 6, name: Abcdefg, number: 1234567, emails: null This means that the IDs should not be used as a reference to a particular contact because it could \"change\" in the process of deleting and inserting. As for updates, let's start with this table... SIM Contact id: 3, name: Abcd, number: 1234, emails: null SIM Contact id: 4, name: Abcde, number: 12345, emails: null Notice that Contact ID 0, 1, and 2 are available. Using the SIM Info app to \"update\" the contact with ID 4, we get... SIM Contact id: 3, name: Abcd, number: 1234, emails: null SIM Contact id: 4, name: xxx, number: 12345, emails: null The ID remains 4. We get the same result using our SimContactsUpdate API =) Thus, we have implemented CRUD APIs!!!","title":"Figuring out how to perform CRUD operations"},{"location":"sim/about-sim-contacts/#figuring-out-character-limits","text":"The BLU Z5 non-smartphone has determined that the maximum character limit for the name and number for my Mint Mobile SIM card is 20. I inserted a contact with a name with 26 characters and another contact with a number with 21 characters using the SIM Info app. The first insert (26 char name) succeeded but the second failed (21 char number). SIM Contact id: 0, name: abcdefghijklmnopqrstuvwxyz, number: 1, emails: null I did the same using our SimContactsInsert ... The same thing occurred. This means that the character limit is imposed on the number but perhaps not the name OR maybe the name has not reached the maximum. I tried inserting a name with over 100 characters and it failed. So there is a character limit for the name. I tried inserting names of shorter and shorter lengths until I find the max. It seems to be 30 characters. The character limits for the name is different for my Mint Mobile SIM card is different in the BLU Z5 vs Nexus 6P . BLU Z5 Nexus 6P name 20 30 number 20 20 I took out the SIM card from the Nexus 6P and plugging it back into the BLU Z5 to see if it will show the contacts that go over the 20 character limit. Both contacts with names longer than 20 characters are shown in the BLU Z5 BUT the name is truncated to 20. This could mean one of two things; The phones determine the character limits based on SIM card memory. The SIM card specifies the character limits but the BLU Z5 hard codes it to 20 regardless. Time to check with the Samsung Galaxy A71 ! The Samsung yielded the same results as the Nexus. So, perhaps it is just the self-imposed limitation of the BLU phone. One interesting difference between the Samsung and the Nexus is that our SimContactsInsert was indicating that the insert succeeded in the Samsung even though no new row was created in the SIM table (oh Samsung lol). The result Uri returned by the insert operation is null in the Nexus but not null in the Samsung. What this all means? Our SimContactsInsert and SimContactsUpdate APIs need to be able to detect the maximum character limits for the name and number before performing the actual insert or update operation. To figure out the max character limits, we can attempt to insert a string of length 35 (most names should fit there and most SIM cards have lower limits). Keep attempting to insert until insert succeeds, making the string shorter each time. Delete the successful insert and record the length of the string. Do this for both name and number and store the results in shared preferences mapped to a unique ID of the SIM card. We do not want to do this calculation everytime our APIs are used! Max character limits should be exposed to our API users also. Furthermore, we cannot rely on the result of the insert operation alone. If the result Uri is not null, we must perform a query to sanity check that the actual name and number was inserted!","title":"Figuring out character limits"},{"location":"sim/about-sim-contacts/#emails","text":"There is an \"emails\" column in the SIM table. CRUD operations for it was not officially supported until recently in Android 12. IccProvider @ Android 11 IccProvider @ Android 12 Look for \"TODO\" comments in the `IccProvider``. You will see TODOs for emails in Android 11 but not Android 12. On my Samsung Galaxy A71 running Android 11... The column name is actually \"emails\" with an \"s\" (plural). What I observed, no email = \",\" at least one email = \"email,\" There seems to be a trailing \",\" regardless. It seems like the emails are in CSV format (comma separated values). I was not able to delete rows with emails in them. I even tried updating the where clause used in our SimContactsDelete to include the email but it does not work. The builtin Samsung Contacts app is able to insert, update, and delete rows with emails. This probably means that we don't have access to the internal APIs that the Samsung Contacts app has. Keep in mind that my Samsung is running Android 11 and support for email was not added until Android 12. \u2139\ufe0f Classic Samsung to add features farther ahead of time than vanilla Android =) On my Nexus 6P running Android 8... The contacts with emails are shown without email data (emails are null in the SIM table). These rows are able to be updated and deleted. On my BLU Z5... SIM contacts with emails are shown without the email data. These rows are able to be updated and deleted.","title":"Emails"},{"location":"sim/about-sim-contacts/#other-considerations","text":"It seems like there are new APIs around SIM Contacts that were introduced in API 31; https://developer.android.com/reference/android/provider/ContactsContract.SimContacts https://developer.android.com/reference/android/provider/SimPhonebookContract Those APIs are too new to be used by this library, which supports API levels down to 19. So, we'll stick with using the content://icc/adn uri to read/write to SIM card until it becomes deprecated, if ever.","title":"Other considerations"},{"location":"sim/delete-sim-contacts/","text":"Delete contacts from SIM card \u00b6 This library provides the SimContactsDelete API that allows you to delete existing contacts from the SIM card. An instance of the SimContactsDelete API is obtained by, val delete = Contacts ( context ). sim (). delete () A basic delete \u00b6 To delete a set of existing contacts from the SIM card, val deleteResult = Contacts ( context ) . sim () . delete () . simContacts ( existingSimContacts ) . commit () Executing the delete \u00b6 To execute the delete, . commit () Handling the delete result \u00b6 The commit function returns a Result , To check if all deletes succeeded, val allDeletesSuccessful = deleteResult . isSuccessful To check if a particular delete succeeded, val firstDeleteSuccessful = deleteResult . isSuccessful ( simContact ) Performing the delete and result processing asynchronously \u00b6 Deletes are executed when the commit or commitInOneTransaction function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the delete with permission \u00b6 Deletes require the android.permission.WRITE_CONTACTS permissions. If not granted, the delete will do nothing and return a failed result. To perform the delete with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Known issues \u00b6 Samsung phones (and perhaps other OEMs) support emails (in addition to name and number) data ahead of the Android 12 release. Updating and deleting SIM contacts that have email data using the APIs provided in this library may fail. This issue does not occur when moving the SIM card to a different phone that does not support emails.","title":"Delete contacts from SIM card"},{"location":"sim/delete-sim-contacts/#delete-contacts-from-sim-card","text":"This library provides the SimContactsDelete API that allows you to delete existing contacts from the SIM card. An instance of the SimContactsDelete API is obtained by, val delete = Contacts ( context ). sim (). delete ()","title":"Delete contacts from SIM card"},{"location":"sim/delete-sim-contacts/#a-basic-delete","text":"To delete a set of existing contacts from the SIM card, val deleteResult = Contacts ( context ) . sim () . delete () . simContacts ( existingSimContacts ) . commit ()","title":"A basic delete"},{"location":"sim/delete-sim-contacts/#executing-the-delete","text":"To execute the delete, . commit ()","title":"Executing the delete"},{"location":"sim/delete-sim-contacts/#handling-the-delete-result","text":"The commit function returns a Result , To check if all deletes succeeded, val allDeletesSuccessful = deleteResult . isSuccessful To check if a particular delete succeeded, val firstDeleteSuccessful = deleteResult . isSuccessful ( simContact )","title":"Handling the delete result"},{"location":"sim/delete-sim-contacts/#performing-the-delete-and-result-processing-asynchronously","text":"Deletes are executed when the commit or commitInOneTransaction function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the delete and result processing asynchronously"},{"location":"sim/delete-sim-contacts/#performing-the-delete-with-permission","text":"Deletes require the android.permission.WRITE_CONTACTS permissions. If not granted, the delete will do nothing and return a failed result. To perform the delete with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the delete with permission"},{"location":"sim/delete-sim-contacts/#known-issues","text":"Samsung phones (and perhaps other OEMs) support emails (in addition to name and number) data ahead of the Android 12 release. Updating and deleting SIM contacts that have email data using the APIs provided in this library may fail. This issue does not occur when moving the SIM card to a different phone that does not support emails.","title":"Known issues"},{"location":"sim/insert-sim-contacts/","text":"Insert contacts into SIM card \u00b6 This library provides the SimContactsInsert API that allows you to create/insert contacts into the SIM card. An instance of the SimContactsInsert API is obtained by, val insert = Contacts ( context ). sim (). insert () A basic insert \u00b6 To create/insert a new contact into the SIM card, val insertResult = Contacts ( context ) . sim () . insert () . simContact ( NewSimContact ( name = \"Dude\" , number = \"5555555555\" )) . commit () If you need to insert multiple contacts, val newContact1 = NewSimContact ( name = \"Dude1\" , number = \"1234567890\" ) val newContact2 = NewSimContact ( name = \"Dude2\" , number = \"0987654321\" ) val insertResult = Contacts ( context ) . sim () . insert () . simContacts ( newContact1 , newContact2 ) . commit () Blank contacts are ignored \u00b6 Blank contacts (name AND number are both null or blank) will NOT be inserted. The name OR number can be null or blank but not both. Character limits \u00b6 The name and number are subject to the SIM card's maximum character limit, which is typically around 20-30 characters (in modern times). This may vary per SIM card. Inserts or updates will fail if the limit is breached. Executing the insert \u00b6 To execute the insert, . commit () Handling the insert result \u00b6 The commit function returns a Result . To check if all inserts succeeded, val allInsertsSuccessful = insertResult . isSuccessful To check if a particular insert succeeded, val firstInsertSuccessful = insertResult . isSuccessful ( newContact1 ) \u2139\ufe0f The IccProvider does not yet return the row ID os newly inserted contacts. Look at the \"TODO\" at line 259 of Android's IccProvider . Therefore, this library's insert API is does not yet support getting the new rows from the result. Cancelling the insert \u00b6 To cancel an insert amid execution, . commit { returnTrueIfInsertShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel insert processing as soon as possible. The function is called numerous times during insert processing to check if processing should stop or continue. This gives you the option to cancel the insert. For example, to automatically cancel the insert inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val insertResult = insert . commit { ! isActive } } } Performing the insert and result processing asynchronously \u00b6 Inserts are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the insert with permission \u00b6 Inserts require the android.permission.WRITE_CONTACTS permission. If not granted, the insert will do nothing and return a failed result. To perform the insert with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Insert contacts into SIM card"},{"location":"sim/insert-sim-contacts/#insert-contacts-into-sim-card","text":"This library provides the SimContactsInsert API that allows you to create/insert contacts into the SIM card. An instance of the SimContactsInsert API is obtained by, val insert = Contacts ( context ). sim (). insert ()","title":"Insert contacts into SIM card"},{"location":"sim/insert-sim-contacts/#a-basic-insert","text":"To create/insert a new contact into the SIM card, val insertResult = Contacts ( context ) . sim () . insert () . simContact ( NewSimContact ( name = \"Dude\" , number = \"5555555555\" )) . commit () If you need to insert multiple contacts, val newContact1 = NewSimContact ( name = \"Dude1\" , number = \"1234567890\" ) val newContact2 = NewSimContact ( name = \"Dude2\" , number = \"0987654321\" ) val insertResult = Contacts ( context ) . sim () . insert () . simContacts ( newContact1 , newContact2 ) . commit ()","title":"A basic insert"},{"location":"sim/insert-sim-contacts/#blank-contacts-are-ignored","text":"Blank contacts (name AND number are both null or blank) will NOT be inserted. The name OR number can be null or blank but not both.","title":"Blank contacts are ignored"},{"location":"sim/insert-sim-contacts/#character-limits","text":"The name and number are subject to the SIM card's maximum character limit, which is typically around 20-30 characters (in modern times). This may vary per SIM card. Inserts or updates will fail if the limit is breached.","title":"Character limits"},{"location":"sim/insert-sim-contacts/#executing-the-insert","text":"To execute the insert, . commit ()","title":"Executing the insert"},{"location":"sim/insert-sim-contacts/#handling-the-insert-result","text":"The commit function returns a Result . To check if all inserts succeeded, val allInsertsSuccessful = insertResult . isSuccessful To check if a particular insert succeeded, val firstInsertSuccessful = insertResult . isSuccessful ( newContact1 ) \u2139\ufe0f The IccProvider does not yet return the row ID os newly inserted contacts. Look at the \"TODO\" at line 259 of Android's IccProvider . Therefore, this library's insert API is does not yet support getting the new rows from the result.","title":"Handling the insert result"},{"location":"sim/insert-sim-contacts/#cancelling-the-insert","text":"To cancel an insert amid execution, . commit { returnTrueIfInsertShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel insert processing as soon as possible. The function is called numerous times during insert processing to check if processing should stop or continue. This gives you the option to cancel the insert. For example, to automatically cancel the insert inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val insertResult = insert . commit { ! isActive } } }","title":"Cancelling the insert"},{"location":"sim/insert-sim-contacts/#performing-the-insert-and-result-processing-asynchronously","text":"Inserts are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the insert and result processing asynchronously"},{"location":"sim/insert-sim-contacts/#performing-the-insert-with-permission","text":"Inserts require the android.permission.WRITE_CONTACTS permission. If not granted, the insert will do nothing and return a failed result. To perform the insert with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the insert with permission"},{"location":"sim/query-sim-contacts/","text":"Query contacts in SIM card \u00b6 This library provides the SimContactsQuery API that allows you to get contacts stored in the SIM card. An instance of the SimContactsQuery API is obtained by, val query = Contacts ( context ). sim (). query () A basic query \u00b6 To get all of the contacts in the SIM card, val simContacts = Contacts ( context ). sim (). query (). find () Limitations \u00b6 Projections, selections, and order is not supported by the IccProvider . Therefore, we are unable to provide include , where , orderBy , limit , and offset functions in our SimContactsQuery API. Due to all of these limitations, all queries will return all contacts in the SIM card. You may perform your own sorting and pagination if you wish. \u2139\ufe0f Depending on memory size, SIM cards can hold 200 to 500+ contacts . The most common being around 250. Most, if not all, SIM cards have less than 1mb memory (averaging 32KB to 64KB). Therefore, memory and speed should not be affected much by not being able to sort/order and paginate at the query level. For more info, read about SIM Contacts Executing the query \u00b6 To execute the query, . find () Cancelling the query \u00b6 To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val simContacts = query . find { ! isActive } } } Performing the query asynchronously \u00b6 Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the query with permission \u00b6 Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Query contacts in SIM card"},{"location":"sim/query-sim-contacts/#query-contacts-in-sim-card","text":"This library provides the SimContactsQuery API that allows you to get contacts stored in the SIM card. An instance of the SimContactsQuery API is obtained by, val query = Contacts ( context ). sim (). query ()","title":"Query contacts in SIM card"},{"location":"sim/query-sim-contacts/#a-basic-query","text":"To get all of the contacts in the SIM card, val simContacts = Contacts ( context ). sim (). query (). find ()","title":"A basic query"},{"location":"sim/query-sim-contacts/#limitations","text":"Projections, selections, and order is not supported by the IccProvider . Therefore, we are unable to provide include , where , orderBy , limit , and offset functions in our SimContactsQuery API. Due to all of these limitations, all queries will return all contacts in the SIM card. You may perform your own sorting and pagination if you wish. \u2139\ufe0f Depending on memory size, SIM cards can hold 200 to 500+ contacts . The most common being around 250. Most, if not all, SIM cards have less than 1mb memory (averaging 32KB to 64KB). Therefore, memory and speed should not be affected much by not being able to sort/order and paginate at the query level. For more info, read about SIM Contacts","title":"Limitations"},{"location":"sim/query-sim-contacts/#executing-the-query","text":"To execute the query, . find ()","title":"Executing the query"},{"location":"sim/query-sim-contacts/#cancelling-the-query","text":"To cancel a query amid execution, . find { returnTrueIfQueryShouldBeCancelled () } The find function optionally takes in a function that, if it returns true, will cancel query processing as soon as possible. The function is called numerous times during query processing to check if processing should stop or continue. This gives you the option to cancel the query. This is useful when used in multi-threaded environments. One scenario where this would be frequently used is when performing queries as the user types a search text. You are able to cancel the current query when the user enters new text. For example, to automatically cancel the query inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val simContacts = query . find { ! isActive } } }","title":"Cancelling the query"},{"location":"sim/query-sim-contacts/#performing-the-query-asynchronously","text":"Queries are executed when the find function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the query asynchronously"},{"location":"sim/query-sim-contacts/#performing-the-query-with-permission","text":"Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will do nothing and return an empty list. To perform the query with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the query with permission"},{"location":"sim/update-sim-contacts/","text":"Update contacts in SIM card \u00b6 This library provides the SimContactsUpdate API that allows you to update contacts in the SIM card. An instance of the SimContactsUpdate API is obtained by, val update = Contacts ( context ). sim (). update () A basic update \u00b6 To update an existing contact in the SIM card, var current : SimContact var modified : MutableSimContact = current . mutableCopy { // change the name and/or number } val updateResult = Contacts ( context ) . sim () . update () . simContact ( current , modified ) . commit () Making further updates \u00b6 The current entry in the SIM table is not updated based on the ID. Instead, the name AND number are used to lookup the entry to update. Continuing the example above, if you need to make another update, then you must use the modified copy as the current, current = modified modified = current . newCopy { // change the name and/or number } val result = update . simContact ( current , modified ) . commit () \u2139\ufe0f This limitation comes from Android, not this library. Updating multiple contacts \u00b6 If you need to update multiple contacts, val update1 = SimContactsUpdate . Entry ( contact1 , contact1 . mutableCopy { ... }) val update2 = SimContactsUpdate . Entry ( contact2 , contact2 . mutableCopy { ... }) val updateResult = Contacts ( context ) . sim () . update () . simContacts ( update1 , update2 ) . commit () Blank contacts are ignored \u00b6 Blank contacts (name AND number are both null or blank) will NOT be updated. The name OR number can be null or blank but not both. Character limits \u00b6 The name and number are subject to the SIM card's maximum character limit, which is typically around 20-30 characters (in modern times). This may vary per SIM card. Inserts or updates will fail if the limit is breached. Executing the update \u00b6 To execute the update, . commit () Handling the update result \u00b6 The commit function returns a Result . To check if all updates succeeded, val allUpdatesSuccessful = updateResult . isSuccessful To check if a particular update succeeded, val firstUpdateSuccessful = updateResult . isSuccessful ( simContact ) Cancelling the update \u00b6 To cancel an update amid execution, . commit { returnTrueIfUpdateShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel update processing as soon as possible. The function is called numerous times during update processing to check if processing should stop or continue. This gives you the option to cancel the update. For example, to automatically cancel the update inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val updateResult = update . commit { ! isActive } } } Performing the update and result processing asynchronously \u00b6 Updates are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap. Performing the update with permission \u00b6 Updates require the android.permission.WRITE_CONTACTS permission. If not granted, the update will do nothing and return a failed result. To perform the update with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =) Known issues \u00b6 Samsung phones (and perhaps other OEMs) support emails (in addition to name and number) data ahead of the Android 12 release. Updating and deleting SIM contacts that have email data using the APIs provided in this library may fail. This issue does not occur when moving the SIM card to a different phone that does not support emails.","title":"Update contacts in SIM card"},{"location":"sim/update-sim-contacts/#update-contacts-in-sim-card","text":"This library provides the SimContactsUpdate API that allows you to update contacts in the SIM card. An instance of the SimContactsUpdate API is obtained by, val update = Contacts ( context ). sim (). update ()","title":"Update contacts in SIM card"},{"location":"sim/update-sim-contacts/#a-basic-update","text":"To update an existing contact in the SIM card, var current : SimContact var modified : MutableSimContact = current . mutableCopy { // change the name and/or number } val updateResult = Contacts ( context ) . sim () . update () . simContact ( current , modified ) . commit ()","title":"A basic update"},{"location":"sim/update-sim-contacts/#making-further-updates","text":"The current entry in the SIM table is not updated based on the ID. Instead, the name AND number are used to lookup the entry to update. Continuing the example above, if you need to make another update, then you must use the modified copy as the current, current = modified modified = current . newCopy { // change the name and/or number } val result = update . simContact ( current , modified ) . commit () \u2139\ufe0f This limitation comes from Android, not this library.","title":"Making further updates"},{"location":"sim/update-sim-contacts/#updating-multiple-contacts","text":"If you need to update multiple contacts, val update1 = SimContactsUpdate . Entry ( contact1 , contact1 . mutableCopy { ... }) val update2 = SimContactsUpdate . Entry ( contact2 , contact2 . mutableCopy { ... }) val updateResult = Contacts ( context ) . sim () . update () . simContacts ( update1 , update2 ) . commit ()","title":"Updating multiple contacts"},{"location":"sim/update-sim-contacts/#blank-contacts-are-ignored","text":"Blank contacts (name AND number are both null or blank) will NOT be updated. The name OR number can be null or blank but not both.","title":"Blank contacts are ignored"},{"location":"sim/update-sim-contacts/#character-limits","text":"The name and number are subject to the SIM card's maximum character limit, which is typically around 20-30 characters (in modern times). This may vary per SIM card. Inserts or updates will fail if the limit is breached.","title":"Character limits"},{"location":"sim/update-sim-contacts/#executing-the-update","text":"To execute the update, . commit ()","title":"Executing the update"},{"location":"sim/update-sim-contacts/#handling-the-update-result","text":"The commit function returns a Result . To check if all updates succeeded, val allUpdatesSuccessful = updateResult . isSuccessful To check if a particular update succeeded, val firstUpdateSuccessful = updateResult . isSuccessful ( simContact )","title":"Handling the update result"},{"location":"sim/update-sim-contacts/#cancelling-the-update","text":"To cancel an update amid execution, . commit { returnTrueIfUpdateShouldBeCancelled () } The commit function optionally takes in a function that, if it returns true, will cancel update processing as soon as possible. The function is called numerous times during update processing to check if processing should stop or continue. This gives you the option to cancel the update. For example, to automatically cancel the update inside a Kotlin coroutine when the coroutine is cancelled, launch { withContext ( coroutineContext ) { val updateResult = update . commit { ! isActive } } }","title":"Cancelling the update"},{"location":"sim/update-sim-contacts/#performing-the-update-and-result-processing-asynchronously","text":"Updates are executed when the commit function is invoked. The work is done in the same thread as the call-site. This may result in a choppy UI. To perform the work in a different thread, use the Kotlin coroutine extensions provided in the async module. For more info, read Execute work outside of the UI thread using coroutines . You may, of course, use other multi-threading libraries or just do it yourself =) \u2139\ufe0f Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.","title":"Performing the update and result processing asynchronously"},{"location":"sim/update-sim-contacts/#performing-the-update-with-permission","text":"Updates require the android.permission.WRITE_CONTACTS permission. If not granted, the update will do nothing and return a failed result. To perform the update with permission, use the extensions provided in the permissions module. For more info, read Permissions handling using coroutines . You may, of course, use other permission handling libraries or just do it yourself =)","title":"Performing the update with permission"},{"location":"sim/update-sim-contacts/#known-issues","text":"Samsung phones (and perhaps other OEMs) support emails (in addition to name and number) data ahead of the Android 12 release. Updating and deleting SIM contacts that have email data using the APIs provided in this library may fail. This issue does not occur when moving the SIM card to a different phone that does not support emails.","title":"Known issues"},{"location":"testing/test-contacts-api/","text":"Contacts API Testing \u00b6 TODO Complete this docs when implementation is complete TODO Add a reference to this docs in the README and the blog This library provides the TestContacts and MockContacts , which you can use as a substitute to your Contacts API instance in; black box tests ; UI instrumentation tests in androidTest/ white box tests ; unit & integration tests in test/ UI instrumentation tests \u00b6 TODO Show usage of TestContacts Unit & integration tests \u00b6 TODO Show usage of MockContacts Production test mode \u00b6 The TestContacts may also be used in your production apps, not just in tests. If you want your production app to interact (query, insert, update, delete) with only \"test contacts\", all you would need to do is substitute your Contacts API instance with an instance of TestContacts . @Singleton fun provideContactsApi ( context : Context ): Contacts = if ( test ) { TestContacts ( context ) } else { Contacts ( context ) } \u2139\ufe0f The above code block is just pseudo-code for a dependency injection setup. For example, if you are building a contacts app, you can add a \"test\" or \"debug\" mode such that only test contacts are; returned by query APIs updated by update APIs inserted by insert APIs deleted by delete APIs When turning off test/debug mode, you can easily delete all test contacts created during the session and return to normal mode.","title":"Contacts API Testing"},{"location":"testing/test-contacts-api/#contacts-api-testing","text":"TODO Complete this docs when implementation is complete TODO Add a reference to this docs in the README and the blog This library provides the TestContacts and MockContacts , which you can use as a substitute to your Contacts API instance in; black box tests ; UI instrumentation tests in androidTest/ white box tests ; unit & integration tests in test/","title":"Contacts API Testing"},{"location":"testing/test-contacts-api/#ui-instrumentation-tests","text":"TODO Show usage of TestContacts","title":"UI instrumentation tests"},{"location":"testing/test-contacts-api/#unit-integration-tests","text":"TODO Show usage of MockContacts","title":"Unit & integration tests"},{"location":"testing/test-contacts-api/#production-test-mode","text":"The TestContacts may also be used in your production apps, not just in tests. If you want your production app to interact (query, insert, update, delete) with only \"test contacts\", all you would need to do is substitute your Contacts API instance with an instance of TestContacts . @Singleton fun provideContactsApi ( context : Context ): Contacts = if ( test ) { TestContacts ( context ) } else { Contacts ( context ) } \u2139\ufe0f The above code block is just pseudo-code for a dependency injection setup. For example, if you are building a contacts app, you can add a \"test\" or \"debug\" mode such that only test contacts are; returned by query APIs updated by update APIs inserted by insert APIs deleted by delete APIs When turning off test/debug mode, you can easily delete all test contacts created during the session and return to normal mode.","title":"Production test mode"},{"location":"ui/integrate-rudimentary-contacts-integrated-ui-components/","text":"Integrate rudimentary contacts ui components \u00b6 TODO Coming soon","title":"Integrate rudimentary contacts ui components"},{"location":"ui/integrate-rudimentary-contacts-integrated-ui-components/#integrate-rudimentary-contacts-ui-components","text":"TODO Coming soon","title":"Integrate rudimentary contacts ui components"}]} \ No newline at end of file diff --git a/setup/installation/index.html b/setup/installation/index.html index 6eb65b13..45c7cc6a 100644 --- a/setup/installation/index.html +++ b/setup/installation/index.html @@ -1841,7 +1841,7 @@

        Installation guide

        -

        This library is a multi-module project published with JitPack +

        ℹ️ This library is a multi-module project published with JitPack JitPack

        First, include JitPack in the repositories list,

        diff --git a/setup/setup-contacts-api/index.html b/setup/setup-contacts-api/index.html index 96fe10c4..5b4ec57e 100644 --- a/setup/setup-contacts-api/index.html +++ b/setup/setup-contacts-api/index.html @@ -1864,9 +1864,9 @@

        Contacts API Setup
        ContactsFactory.create(context);
         
        -

        The context parameter can come from anywhere; Application, Activity, Fragment, or View. It does -not matter what context you pass in. The API will only use and store the Application context, to -avoid leaks :D

        +

        ℹ️ The context parameter can come from anywhere; Application, Activity, Fragment, or View. It +does not matter what context you pass in. The API will only use and store the Application context, +to avoid leaks.

        It's up to you if you just want to create instances on demand. Or, hold on to instances as a singleton that is injected to your dependency graph (via something like @@ -1899,9 +1899,9 @@

        SIM ContactsSIM Contact data

        SIM Contact data consists of the name and number.

        -

        Support for email was recently added in Android 12. I don't think it is stable yet. Regardless, -it is too new so this library will wait a bit before adding support for it.

        +

        ℹ️ Support for email was recently added in Android 12. I don't think it is stable yet. +Regardless, it is too new so this library will wait a bit before adding support for it.

        -

        Character limits

        +

        Character limits

        The name and number are subject to the SIM card's maximum character limit, which is typically around 20-30 characters (in modern times). This may vary per SIM card. Inserts or updates will fail if the limit is breached.

        -

        SIM Contact row ID

        +

        SIM Contact row ID

        The SIM contact that an ID is pointing to may change if the contact is deleted in the database and another contact is inserted. The inserted contact may be assigned the ID of the deleted contact.

        @@ -2130,7 +2130,7 @@

        Developer notes (or for advanced
        -

        Note that the AOSP Contacts app and Google Contacts app +

        ℹ️ The AOSP Contacts app and Google Contacts app can only import contacts from SIM card so they are not very helpful for us with this investigation.

        For Android code references, I used the internal IccProvider.java as reference to what the Android @@ -2142,19 +2142,19 @@

        Developer notes (or for advanced

      I'm using the content://icc/adn URI to read/write from/to SIM card.

      -

      All of the investigation that I have done here may not apply for all SIM cards and phone OEMs! +

      ℹ️ All of the investigation that I have done here may not apply for all SIM cards and phone OEMs! There is just way too many different SIM cards and phones out there for a single person (me) to test. However, I think that my findings should apply to most cases.

      -

      Figuring out how to perform CRUD operations

      +

      Figuring out how to perform CRUD operations

      First, I added 20 contacts (name and number) to the SIM contacts using the BLU Z5. The first contact is named "a" with number "1", the second is named "ab" with number "12", and so on. The last contact is named "abcdefghijklmnopqrst" with number "12345678901234567890". I did this because the BLU Z5 has determined that the maximum character limit for the name and number for my Mint Mobile SIM card is 20.

      -

      Note that the character limits are most likely set by the SIM card and/or calculated by the OS -managing it based on how much total memory is available.

      +

      ℹ️ The character limits are most likely set by the SIM card and/or calculated by the OS managing +it based on how much total memory is available.

      I also added a contact named "bro" with no number and a nameless contact with with number "5555555555". For a total of 22 contacts in the SIM card.

      @@ -2188,7 +2188,7 @@

      Figuring out how to perform number "5555555555". I attempted to add a nameless contact using the SIM Info app but it does not allow reading/writing nameless contacts.

      -

      This is probably a bug in the SIM Info app or a limitation that is intentionally imposed for +

      ℹ️ This is probably a bug in the SIM Info app or a limitation that is intentionally imposed for some reason. I wish I could see the source code of the app!

      Deleting the first contact with ID of 0 using the SIM Info app works just fine. Deleting the contact @@ -2224,7 +2224,7 @@

      Figuring out how to perform

      The ID remains 4. We get the same result using our SimContactsUpdate API =)

      Thus, we have implemented CRUD APIs!!!

      -

      Figuring out character limits

      +

      Figuring out character limits

      The BLU Z5 non-smartphone has determined that the maximum character limit for the name and number for my Mint Mobile SIM card is 20.

      I inserted a contact with a name with 26 characters and another contact with a number with 21 @@ -2292,7 +2292,7 @@

      Figuring out character limitsEmails

      +

      Emails

      There is an "emails" column in the SIM table. CRUD operations for it was not officially supported until recently in Android 12.

        @@ -2316,7 +2316,7 @@

        EmailsEmailsOther considerations

        +

        Other considerations

        It seems like there are new APIs around SIM Contacts that were introduced in API 31;

        • https://developer.android.com/reference/android/provider/ContactsContract.SimContacts
        • diff --git a/sim/delete-sim-contacts/index.html b/sim/delete-sim-contacts/index.html index bad0f363..28251663 100644 --- a/sim/delete-sim-contacts/index.html +++ b/sim/delete-sim-contacts/index.html @@ -1940,7 +1940,7 @@

          Performing t For more info, read Execute work outside of the UI thread using coroutines.

          You may, of course, use other multi-threading libraries or just do it yourself =)

          -

          Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

          +

          ℹ️ Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

          Performing the delete with permission

          Deletes require the android.permission.WRITE_CONTACTS permissions. If not granted, the delete diff --git a/sim/insert-sim-contacts/index.html b/sim/insert-sim-contacts/index.html index d60111ed..929b77b5 100644 --- a/sim/insert-sim-contacts/index.html +++ b/sim/insert-sim-contacts/index.html @@ -1979,7 +1979,7 @@

          Handling the insert result
          val firstInsertSuccessful = insertResult.isSuccessful(newContact1)
           
          -

          The IccProvider does not yet return the row ID os newly inserted contacts. Look at the "TODO" +

          ℹ️ The IccProvider does not yet return the row ID os newly inserted contacts. Look at the "TODO" at line 259 of Android's IccProvider. Therefore, this library's insert API is does not yet support getting the new rows from the result.

          @@ -2005,7 +2005,7 @@

          Performing t read Execute work outside of the UI thread using coroutines.

          You may, of course, use other multi-threading libraries or just do it yourself =)

          -

          Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

          +

          ℹ️ Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

          Performing the insert with permission

          Inserts require the android.permission.WRITE_CONTACTS permission. If not granted, the insert will diff --git a/sim/query-sim-contacts/index.html b/sim/query-sim-contacts/index.html index f2d15359..8ffd80dc 100644 --- a/sim/query-sim-contacts/index.html +++ b/sim/query-sim-contacts/index.html @@ -1912,8 +1912,7 @@

          LimitationsDue to all of these limitations, all queries will return all contacts in the SIM card. You may perform your own sorting and pagination if you wish.

          -

          Depending on memory size, -SIM cards can hold 200 to 500+ contacts. +

          ℹ️ Depending on memory size, SIM cards can hold 200 to 500+ contacts. The most common being around 250. Most, if not all, SIM cards have less than 1mb memory (averaging 32KB to 64KB). Therefore, memory and speed should not be affected much by not being able to sort/order and paginate at the query level.

          @@ -1947,7 +1946,7 @@

          Performing the query asynchronously For more info, read Execute work outside of the UI thread using coroutines.

          You may, of course, use other multi-threading libraries or just do it yourself =)

          -

          Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

          +

          ℹ️ Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

          Performing the query with permission

          Queries require the android.permission.READ_CONTACTS permission. If not granted, the query will diff --git a/sim/update-sim-contacts/index.html b/sim/update-sim-contacts/index.html index 25f04f8a..78606b7c 100644 --- a/sim/update-sim-contacts/index.html +++ b/sim/update-sim-contacts/index.html @@ -2007,7 +2007,7 @@

          A basic update .simContact(current, modified) .commit() -

          Making further updates

          +

          Making further updates

          The current entry in the SIM table is not updated based on the ID. Instead, the name AND number are used to lookup the entry to update. Continuing the example above, if you need to make another update, then you must use the modified copy as the current,

          @@ -2021,9 +2021,9 @@

          Making further updates .commit()
          -

          This limitation comes from Android, not this library.

          +

          ℹ️ This limitation comes from Android, not this library.

          -

          Updating multiple contacts

          +

          Updating multiple contacts

          If you need to update multiple contacts,

          val update1 = SimContactsUpdate.Entry(contact1, contact1.mutableCopy { ... })
           val update2 = SimContactsUpdate.Entry(contact2, contact2.mutableCopy { ... })
          @@ -2075,7 +2075,7 @@ 

          Performing t read Execute work outside of the UI thread using coroutines.

          You may, of course, use other multi-threading libraries or just do it yourself =)

          -

          Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

          +

          ℹ️ Extensions for Kotlin Flow and RxJava are also in the v1 roadmap.

          Performing the update with permission

          Updates require the android.permission.WRITE_CONTACTS permission. If not granted, the update will diff --git a/sitemap.xml b/sitemap.xml index 28e8a1e8..94ea3f40 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -2,332 +2,332 @@ https://vestrel00.github.io/contacts-android/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/contributing/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/dev-notes/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/accounts/associate-device-local-raw-contacts-to-an-account/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/accounts/query-accounts/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/accounts/query-raw-contacts/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/async/async-execution-coroutines/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/basics/delete-contacts/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/basics/insert-contacts/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/basics/query-contacts-advanced/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/basics/query-contacts/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/basics/update-contacts/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/blockednumbers/about-blocked-numbers/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/blockednumbers/delete-blocked-numbers/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/blockednumbers/insert-blocked-numbers/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/blockednumbers/query-blocked-numbers/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/customdata/delete-custom-data/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/customdata/insert-custom-data/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/customdata/integrate-custom-data-from-other-apps/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/customdata/integrate-custom-data/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/customdata/integrate-gender-custom-data/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/customdata/integrate-googlecontacts-custom-data/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/customdata/integrate-handlename-custom-data/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/customdata/integrate-pokemon-custom-data/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/customdata/integrate-rpg-custom-data/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/customdata/query-custom-data/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/customdata/update-custom-data/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/data/delete-data-sets/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/data/insert-data-sets/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/data/query-data-sets/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/data/update-data-sets/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/debug/debug-blockednumber-provider-tables/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/debug/debug-contacts-provider-tables/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/debug/debug-sim-contacts-tables/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/entities/about-api-entities/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/entities/about-blank-contacts/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/entities/about-blank-data/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/entities/about-contact-lookup-key/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/entities/about-local-contacts/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/entities/include-only-desired-data/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/entities/redact-apis-and-entities/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/entities/sync-contact-data/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/groups/delete-groups/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/groups/insert-groups/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/groups/query-groups/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/groups/update-groups/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/logging/log-api-input-output/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/other/convenience-functions/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/other/get-set-clear-contact-raw-contact-options/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/other/get-set-clear-default-data/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/other/get-set-remove-contact-raw-contact-photo/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/other/link-unlink-contacts/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/permissions/permissions-handling-coroutines/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/profile/delete-profile/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/profile/insert-profile/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/profile/query-profile/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/profile/update-profile/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/setup/installation/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/setup/setup-contacts-api/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/sim/about-sim-contacts/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/sim/delete-sim-contacts/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/sim/insert-sim-contacts/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/sim/query-sim-contacts/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/sim/update-sim-contacts/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/testing/test-contacts-api/ - 2022-03-31 + 2022-05-19 daily https://vestrel00.github.io/contacts-android/ui/integrate-rudimentary-contacts-integrated-ui-components/ - 2022-03-31 + 2022-05-19 daily \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz index 291f6bea..424b05f5 100644 Binary files a/sitemap.xml.gz and b/sitemap.xml.gz differ diff --git a/testing/test-contacts-api/index.html b/testing/test-contacts-api/index.html index 8b562f55..2df2fa9f 100644 --- a/testing/test-contacts-api/index.html +++ b/testing/test-contacts-api/index.html @@ -1878,7 +1878,7 @@

          Production test mode}

          -

          The above code block is just pseudo-code for a dependency injection setup.

          +

          ℹ️ The above code block is just pseudo-code for a dependency injection setup.

          For example, if you are building a contacts app, you can add a "test" or "debug" mode such that only test contacts are;