Integrate custom data¶
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¶
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/<package>.<mimetype>"
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/<package>.<mimetype>"
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" |
"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¶
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
orCustomDataEntityWithTypeAndLabel
. - Implement the
mimeType
using the mimetype you defined in the previous step. - Implement the
isBlank
using thecontacts.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
andImmutableCustomDataEntityWithMutableType
(orImmutableCustomDataEntityWithNullableMutableType
). - All properties and types defined here must be immutable (
val
).
- These would also need to inherit from
- 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
).
- These would also need to inherit from
- 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
).
- These would also need to inherit from
- Properties that map to your custom data fields should be nullable (
?
). - The following properties should always be immutable (
val
);id
,rawContactId
,contactId
,isPrimary
,isSuperPrimary
, andisRedacted
.
- Be mindful of what properties should be redacted when implementing the
redactedCopy
function. - All entity class must implement
Parecelable
.
3. Define the fields¶
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 aAbstractCustomDataFieldSet
. - 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
andforMatching
.- If you are using
ColumnName.BLOB
, do not put it inall
orforMatching
! For more info, read the in-code documentation on it.
- If you are using
4. Implement the cursor¶
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 incontacts.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¶
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 tofalse
(unless you are already performing the redaction) here.
6. Implement the operation¶
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(
callerIsSyncAdapter: Boolean,
isProfile: Boolean,
includeFields: Set<GenderField>?
): AbstractCustomDataOperation<GenderField, GenderEntity> = GenderOperation(
callerIsSyncAdapter = callerIsSyncAdapter,
isProfile = isProfile,
includeFields = includeFields
)
}
private class GenderOperation(
callerIsSyncAdapter: Boolean,
isProfile: Boolean,
includeFields: Set<GenderField>?
) : AbstractCustomDataOperation<GenderField, GenderEntity>(
callerIsSyncAdapter = callerIsSyncAdapter,
isProfile = isProfile,
includeFields = 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(
callerIsSyncAdapter: Boolean, isProfile: Boolean, includeFields: Set<HandleNameField>?
): AbstractCustomDataOperation<HandleNameField, HandleNameEntity> = HandleNameOperation(
callerIsSyncAdapter = callerIsSyncAdapter,
isProfile = isProfile,
includeFields = includeFields
)
}
private class HandleNameOperation(
callerIsSyncAdapter: Boolean,
isProfile: Boolean,
includeFields: Set<HandleNameField>?
) : AbstractCustomDataOperation<HandleNameField, HandleNameEntity>(
callerIsSyncAdapter = callerIsSyncAdapter,
isProfile = isProfile,
includeFields = 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 thesetCustomData
function.
7. Define the count restriction¶
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¶
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.
- Consider returning
- 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.
- the
- Define getters and setters for
RawContact
,MutableRawContact
, andNewRawContact
.- 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
- When setting/adding a new custom data entity,
NewRawContact
->NewGender
,NewHandleName
- Ensure to match the type of RawContact with the type of the custom data. For example,
- Setters for custom data with count restriction of
AT_MOST_ONE
should usesetXXX
for the function name. - Setters for custom data with count restriction of
NO_LIMIT
should useaddXXX
andremoveXXX
for the function names.
9. Define Contact getters and setters¶
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
andMutableContact
.- 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
- When setting/adding a new custom data entity,
- Ensure to match the type of Contact with the type of the custom data. For example,
10. Define exceptions¶
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
,
For HandleName
,
11. Implement the field mapper¶
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¶
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¶
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¶
The entry registration provides a way for you to keep your Entry internal
to your library module.
ℹ️ 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¶
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,
ℹ️ This is not possible with
Gender
andHandleName
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¶
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¶
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¶
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¶
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.
ℹ️ 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 inContactsContract.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<ContactsAccountType>
element and one or more of its<ContactsDataKind>
child elements. This is described in more detail in the section<ContactsDataKind>
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,
<ContactsAccountType
xmlns:android="http://schemas.android.com/apk/res/android"
inviteContactActivity="activity_name"
inviteContactActionLabel="invite_command_text"
viewContactNotifyService="view_notify_service"
viewGroupActivity="group_view_activity"
viewGroupActionLabel="group_action_text"
viewStreamItemActivity="viewstream_activity_name"
viewStreamItemPhotoActivity="viewphotostream_activity_name">
<ContactsDataKind
android:mimeType="MIMEtype"
android:icon="icon_resources"
android:summaryColumn="column_name"
android:detailColumn="column_name" />
</ContactsAccountType>
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,
<ContactsAccountType xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Gender -->
<ContactsDataKind
android:mimeType="vnd.android.cursor.item/contacts.entities.custom.gender"
android:summaryColumn="data2"
android:detailColumn="data3" />
<!-- HandleName -->
<ContactsDataKind
android:mimeType="vnd.android.cursor.item/contacts.entities.custom.handlename"
android:summaryColumn="data1" />
</ContactsAccountType>
A few things to note,
- The value of
android:mimeType
corresponds to the String value defined inGenderMimeType
andHandleNameMimeType
as seen in the previous sections of this guide. - The value of
android:summaryColumn
andandroid:detailColumn
corresponds to the values defined incontacts.core.Fields.kt#AbstractCustomDataField.ColumnName
that are used byGenderFields
andHandleNameFields
.- These values, as raw strings, are;
data1
,data2
,data3
,...data15
- These values, as raw strings, are;
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¶
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;
Also provided are CRUD API integration with custom data from other apps that do have sync adapters;
ℹ️ Please update the above list whenever adding new custom data modules.