How do I integrate custom data?

Android Contacts API Library written in Kotlin with Java interoperability. No more ContentProviders and cursors. Say goodbye to ContactsContract. Build your own contacts app!

How do I integrate custom data?

If you are looking to integrate custom data from other apps, read How do I 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”;

  1. Providing create (insert), read (query), update, and delete (CRUD) APIs for custom data associated with RawContacts.
  2. 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,

  1. Define the mimetype
  2. Define the entities
  3. Define the fields
  4. Implement the cursor
  5. Implement the mapper
  6. Implement the operation
  7. Define the count restriction
  8. Define RawContact getters and setters
  9. Define Contact getters and setters
  10. Define exceptions
  11. Implement the field mapper
  12. Define the data query function
  13. Define the custom data entry
  14. Define the custom data entry registration
  15. Register your custom data with the Contacts API instance
  16. 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 How do I integrate the gender custom data? and How do I integrate the handle name custom data?

At the bottom of this page, we’ll also discuss,

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”
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

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,

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,

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,

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,

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(
        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,

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,

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,

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,

class GenderDataException(message: String) : CustomDataException(message)

For HandleName,

class HandleNameDataException(message: String) : CustomDataException(message)

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,

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 How do I get a list of specific data kinds? and How do I use query APIs to get 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 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

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(contacts.customDataRegistry)
HandleNameRegistration().registerTo(contacts.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

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.

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 and customdata-handlename 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 How do I 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.

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 <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.

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,

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 How do I 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…

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.