Work profile contacts

This developer’s guide explains how you can enhance your app to use contact data from the work profile. If you haven’t used Android's contacts APIs before, read Contacts Provider to familiarize yourself with the APIs.

Overview

Devices with work profiles store contacts in separate local directories for the work and personal profiles. By default, when an app runs in the personal profile, it doesn't display the work contacts. However, an app can access contact information from the work profile. For example, an app that does this is Google's Android Contacts app which shows both personal and work-directory contacts in search results.

Users often want to use their personal devices and apps for work. By using work-profile contacts, your app can become part of your user’s working day.

User experience

Consider how your app might present contact information from the work profile. The best approach depends on the nature of your app and the reason that people use it, but think about the following:

  • Should your app include work profile contacts by default or should the user opt-in?
  • How will mixing or separating work and personal profile contacts affect the user’s flow?
  • What is the impact of accidentally tapping a work profile contact?
  • What happens to your app’s interface when the work profile contacts aren’t available?

Your app should clearly indicate a work profile contact. Perhaps you can badge the contact using a familiar work icon—like a briefcase.

Screenshot showing search results in a list
Figure 1. How the Google Contacts app separates work profile contacts

As an example, the Google Contacts app (shown in figure 1) does the following to list a mix of work and personal profile contacts:

  1. Inserts a subheader to separate work and personal sections of the list.
  2. Badges work contacts with a briefcase icon.
  3. Opens a work contact in the work profile when tapped.

If the person using the device turns off the work profile, your app isn’t able to look up contact information from the work profile or the organization’s remote contact directories. Depending on how you use work-profile contacts, you can silently leave these contacts out or you might need to disable user interface controls.

Permissions

If your app is already working with the user’s contacts, you’ll have their READ_CONTACTS (or possibly WRITE_CONTACTS) permission that you requested in your app manifest file. Because the same person uses the personal profile and the work profile, you don’t need further permission to access contact data from the work profile.

An IT admin can block the work profile sharing contact information with the personal profile. If an IT admin blocks access, your contact searches are returned as empty results. Your app doesn’t need to handle specific errors if the user switched off the work profile. The directory content provider continues to return information about the user’s work contact directories (see the Directories section). To test these permissions, see the Developement and testing section.

Contact searches

You can get contacts from the work profile using the same APIs and processes that your app uses to get contacts in the personal profile. The enterprise URI for contacts is supported in Android 7.0 (API level 24) or higher. You need to make the following adjustments to the URI:

  1. Set the content provider URI to Contacts.ENTERPRISE_CONTENT_FILTER_URI, and supply the contact’s name as a query string.
  2. Set a contact directory to search. For example, ENTERPRISE_DEFAULT finds contacts in the work profile’s local store.

Changing the URI works with any content provider mechanism such as a CursorLoader—ideal for loading contact data into user interfaces because data access happens on a worker thread. For simplicity, the examples in this guide call ContentResolver.query(). Here's how you can find contacts in the work profile’s local contact directory:

Kotlin

// First confirm the device user has given permission for the personal profile.
// There isn't a separate work permission, but an IT admin can block access.
val readContactsPermission = ContextCompat.checkSelfPermission(
        getBaseContext(), Manifest.permission.READ_CONTACTS)
if (readContactsPermission != PackageManager.PERMISSION_GRANTED) {
    return
}

// Fetch Jackie, James, & Jason (and anyone else whose names begin with "ja").
val nameQuery = Uri.encode("ja")

// Build the URI to look up work profile contacts whose name matches. Query
// the default work profile directory which is the locally-stored contacts.
val contentFilterUri = ContactsContract.Contacts.ENTERPRISE_CONTENT_FILTER_URI
        .buildUpon()
        .appendPath(nameQuery)
        .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
                ContactsContract.Directory.ENTERPRISE_DEFAULT.toString())
        .build()

// Query the content provider using the generated URI.
var cursor = getContentResolver().query(
        contentFilterUri,
        arrayOf(ContactsContract.Contacts._ID,
                ContactsContract.Contacts.LOOKUP_KEY,
                ContactsContract.Contacts.DISPLAY_NAME_PRIMARY),
        null,
        null,
        null)

// Print any results found using the work profile contacts' display name.
cursor?.use {
    while (it.moveToNext()) {
        Log.i(TAG, "Work profile contact: ${it.getString(2)}")
    }
}

Java

// First confirm the device user has given permission for the personal profile.
// There isn't a separate work permission, but an IT admin can block access.
int readContactsPermission = ContextCompat.checkSelfPermission(
    getBaseContext(), Manifest.permission.READ_CONTACTS);
if (readContactsPermission != PackageManager.PERMISSION_GRANTED) {
  return;
}

// Fetch Jackie, James, & Jason (and anyone else whose names begin with "ja").
String nameQuery = Uri.encode("ja");

// Build the URI to look up work profile contacts whose name matches. Query
// the default work profile directory which is the locally stored contacts.
Uri contentFilterUri = ContactsContract.Contacts.ENTERPRISE_CONTENT_FILTER_URI
    .buildUpon()
    .appendPath(nameQuery)
    .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
        String.valueOf(ContactsContract.Directory.ENTERPRISE_DEFAULT))
    .build();

// Query the content provider using the generated URI.
Cursor cursor = getContentResolver().query(
    contentFilterUri,
    new String[] {
        ContactsContract.Contacts._ID,
        ContactsContract.Contacts.LOOKUP_KEY,
        ContactsContract.Contacts.DISPLAY_NAME_PRIMARY
    },
    null,
    null,
    null);
if (cursor == null) {
  return;
}

// Print any results found using the work profile contacts' display name.
try {
  while (cursor.moveToNext()) {
    Log.i(TAG, "Work profile contact: " + cursor.getString(2));
  }
} finally {
  cursor.close();
}

Directories

Many organizations use remote directories, such as Microsoft Exchange or LDAP, that contain contact information for the entire organization. Your app can help users communicate and share with work colleagues found in their organization’s directory. Note that these directories typically contain thousands of contacts, and your app also needs an active network connection to search them. You can use the Directory content provider to get the directories used by the user’s accounts and find out more about an individual directory.

Query the Directory.ENTERPRISE_CONTENT_URI content provider to get directories from the personal profile and the work profile returned together. Searching work-profile directories is supported in Android 7.0 (API level 24) or higher. Your app still needs the user to give READ_CONTACTS permissions to work with their contact directories.

Because Android stores contact information in different types of local and remote directories, the Directory class has methods you can call to find more about a directory:

isEnterpriseDirectoryId()
Call this method to find out if the directory is from a work profile account. Remember that the ENTERPRISE_CONTENT_URI content provider returns contact directories for the personal and work profile together.
isRemoteDirectoryId()
Call this method to find out if the directory is remote. Remote directories might be enterprise contact stores or could be the user’s social networks.

The following example shows how you can use these methods to filter work profile directories:

Kotlin

// First, confirm the device user has given READ_CONTACTS permission.
// This permission is still needed for directory listings ...

// Query the content provider to get directories for BOTH the personal and
// work profiles.
val cursor = getContentResolver().query(
        ContactsContract.Directory.ENTERPRISE_CONTENT_URI,
        arrayOf(ContactsContract.Directory._ID,
                ContactsContract.Directory.PACKAGE_NAME),
        null,
        null,
        null)

// Print the package name of the work profile's local or remote contact directories.
cursor?.use {
    while (it.moveToNext()) {
        val directoryId = it.getLong(0)
        if (ContactsContract.Directory.isEnterpriseDirectoryId(directoryId)) {
            Log.i(TAG, "Directory: ${it.getString(1)}")
        }
    }
}

Java

// First, confirm the device user has given READ_CONTACTS permission.
// This permission is still needed for directory listings ...

// Query the content provider to get directories for BOTH the personal and
// work profiles.
Cursor cursor = getContentResolver().query(
    ContactsContract.Directory.ENTERPRISE_CONTENT_URI,
    new String[]{
        ContactsContract.Directory._ID,
        ContactsContract.Directory.PACKAGE_NAME
    },
    null,
    null,
    null);
if (cursor == null) {
  return;
}

// Print the package name of the work profile's local or remote contact directories.
try {
  while (cursor.moveToNext()) {
    long directoryId = cursor.getLong(0);

    if (ContactsContract.Directory.isEnterpriseDirectoryId(directoryId)) {
      Log.i(TAG, "Directory: " + cursor.getString(1));
    }
  }
} finally {
  cursor.close();
}

The example fetches the ID and package name for the directory. To display a user interface that helps users pick a contact-directory source, you might need to fetch more information about the directory. To see other metadata fields that might be available, read the Directory class reference.

Phone lookups

Apps can query PhoneLookup.CONTENT_FILTER_URI to efficiently look up contact data for a telephone number. You can get lookup results from both personal and work profile contacts provider if you replace this URI with PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI. This work-profile content URI is available in Android 5.0 (API level 21) or higher.

The following example shows an app querying the work-profile content URI to configure the user interface for an incoming call:

Kotlin

fun onCreateIncomingConnection(
  connectionManagerPhoneAccount: PhoneAccountHandle,
  request: ConnectionRequest
): Connection {
    var request = request
    // Get the telephone number from the incoming request URI.
    val phoneNumber = this.extractTelephoneNumber(request.address)

    var displayName = "Unknown caller"
    var isCallerInWorkProfile = false

    // Look up contact details for the caller in the personal and work profiles.
    val lookupUri = Uri.withAppendedPath(
            ContactsContract.PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI,
            Uri.encode(phoneNumber))
    val cursor = getContentResolver().query(
            lookupUri,
            arrayOf(ContactsContract.PhoneLookup._ID,
                    ContactsContract.PhoneLookup.DISPLAY_NAME,
                    ContactsContract.PhoneLookup.CUSTOM_RINGTONE),
            null,
            null,
            null)

    // Use the first contact found and check if they're from the work profile.
    cursor?.use {
        if (it.moveToFirst() == true) {
            displayName = it.getString(1)
            isCallerInWorkProfile =
                    ContactsContract.Contacts.isEnterpriseContactId(it.getLong(0))
        }
    }

    // Return a configured connection object for the incoming call.
    val connection = MyAudioConnection()
    connection.setCallerDisplayName(displayName, TelecomManager.PRESENTATION_ALLOWED)

    // Our app's activity uses this value to decide whether to show a work badge.
    connection.setIsCallerInWorkProfile(isCallerInWorkProfile)

    // Configure the connection further ...
    return connection
}

Java

public Connection onCreateIncomingConnection (
    PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) {
  // Get the telephone number from the incoming request URI.
  String phoneNumber = this.extractTelephoneNumber(request.getAddress());

  String displayName = "Unknown caller";
  boolean isCallerInWorkProfile = false;

  // Look up contact details for the caller in the personal and work profiles.
  Uri lookupUri = Uri.withAppendedPath(
      ContactsContract.PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI,
      Uri.encode(phoneNumber));
  Cursor cursor = getContentResolver().query(
      lookupUri,
      new String[]{
          ContactsContract.PhoneLookup._ID,
          ContactsContract.PhoneLookup.DISPLAY_NAME,
          ContactsContract.PhoneLookup.CUSTOM_RINGTONE
      },
      null,
      null,
      null);

  // Use the first contact found and check if they're from the work profile.
  if (cursor != null) {
    try {
      if (cursor.moveToFirst() == true) {
        displayName = cursor.getString(1);
        isCallerInWorkProfile =
            ContactsContract.Contacts.isEnterpriseContactId(cursor.getLong(0));
      }
    } finally {
      cursor.close();
    }
  }

  // Return a configured connection object for the incoming call.
  MyConnection connection = new MyConnection();
  connection.setCallerDisplayName(displayName, TelecomManager.PRESENTATION_ALLOWED);

  // Our app's activity uses this value to decide whether to show a work badge.
  connection.setIsCallerInWorkProfile(isCallerInWorkProfile);

  // Configure the connection further ...
  return connection;
}

Email lookups

Your app can get personal or work contact data for an email address by querying Email.ENTERPRISE_CONTENT_LOOKUP_URI. Querying this URL first searches the personal contacts for an exact match. If the provider doesn't match any personal contacts, the provider then searches work contacts for a match. This URI is available in Android 6.0 (API level 23) or higher.

Here's how you can look up contact information for an email address:

Kotlin

// Build the URI to look up contacts from the personal and work profiles that
// are an exact (case-insensitive) match for the email address.
val emailAddress = "somebody@example.com"
val contentFilterUri = Uri.withAppendedPath(
        ContactsContract.CommonDataKinds.Email.ENTERPRISE_CONTENT_LOOKUP_URI,
        Uri.encode(emailAddress))

// Query the content provider to first try to match personal contacts and,
// if none are found, then try to match the work contacts.
val cursor = contentResolver.query(
        contentFilterUri,
        arrayOf(ContactsContract.CommonDataKinds.Email.CONTACT_ID,
                ContactsContract.CommonDataKinds.Email.ADDRESS,
                ContactsContract.Contacts.DISPLAY_NAME),
        null, null, null)
        ?: return

// Print the name of the matching contact. If we want to work-badge contacts,
// we can call ContactsContract.Contacts.isEnterpriseContactId() with the ID.
cursor.use {
    while (it.moveToNext()) {
        Log.i(TAG, "Matching contact: ${it.getString(2)}")
    }
}

Java

// Build the URI to look up contacts from the personal and work profiles that
// are an exact (case-insensitive) match for the email address.
String emailAddress = "somebody@example.com";
Uri contentFilterUri = Uri.withAppendedPath(
    ContactsContract.CommonDataKinds.Email.ENTERPRISE_CONTENT_LOOKUP_URI,
    Uri.encode(emailAddress));

// Query the content provider to first try to match personal contacts and,
// if none are found, then try to match the work contacts.
Cursor cursor = getContentResolver().query(
    contentFilterUri,
    new String[]{
        ContactsContract.CommonDataKinds.Email.CONTACT_ID,
        ContactsContract.CommonDataKinds.Email.ADDRESS,
        ContactsContract.Contacts.DISPLAY_NAME
    },
    null,
    null,
    null);
if (cursor == null) {
  return;
}

// Print the name of the matching contact. If we want to work-badge contacts,
// we can call ContactsContract.Contacts.isEnterpriseContactId() with the ID.
try {
  while (cursor.moveToNext()) {
    Log.i(TAG, "Matching contact: " + cursor.getString(2));
  }
} finally {
  cursor.close();
}

Show a work contact

Apps running in the personal profile can show a contact card in the work profile. Call ContactsContract.QuickContact.showQuickContact() in Android 5.0 or higher to launch the Contacts app in the work profile and show the contact’s card.

To generate a correct URI for the work profile, you need to call ContactsContract.Contacts.getLookupUri() and pass a contact ID and lookup key. The following example shows how you can get the URI and then show the card:

Kotlin

// Query the content provider using the ENTERPRISE_CONTENT_FILTER_URI address.
// We use the _ID and LOOKUP_KEY columns to generate a work-profile URI.
val cursor = getContentResolver().query(
        contentFilterUri,
        arrayOf(ContactsContract.Contacts._ID,
                ContactsContract.Contacts.LOOKUP_KEY),
        null,
        null)

// Show the contact details card in the work profile's Contacts app. The URI
// must be created with getLookupUri().
cursor?.use {
    if (it.moveToFirst() == true) {
        val uri = ContactsContract.Contacts.getLookupUri(
                it.getLong(0), it.getString(1))
        ContactsContract.QuickContact.showQuickContact(
                activity,
                Rect(20, 20, 100, 100),
                uri,
                ContactsContract.QuickContact.MODE_LARGE,
                null)
    }
}

Java

// Query the content provider using the ENTERPRISE_CONTENT_FILTER_URI address.
// We use the _ID and LOOKUP_KEY columns to generate a work-profile URI.
Cursor cursor = getContentResolver().query(
    contentFilterUri,
    new String[] {
        ContactsContract.Contacts._ID,
        ContactsContract.Contacts.LOOKUP_KEY,
    },
    null,
    null,
    null);
if (cursor == null) {
  return;
}

// Show the contact details card in the work profile's Contacts app. The URI
// must be created with getLookupUri().
try {
  if (cursor.moveToFirst() == true) {
    Uri uri = ContactsContract.Contacts.getLookupUri(
        cursor.getLong(0), cursor.getString(1));
    ContactsContract.QuickContact.showQuickContact(
        getActivity(),
        new Rect(20, 20, 100, 100),
        uri,
        ContactsContract.QuickContact.MODE_LARGE,
        null);
  }
} finally {
  cursor.close();
}

Availability

The following table summarizes which Android versions support work profile contact data in the personal profile:

Android version Support
5.0 (API level 21) Look up work contact names for telephone numbers using PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI.
6.0 (API level 23) Look up work contact names for email addresses using Email.ENTERPRISE_CONTENT_LOOKUP_URI.
7.0 (API level 24) Query work contact names from work directories using Contacts.ENTERPRISE_CONTENT_FILTER_URI.
List all directories in the work and personal profiles using Directory.ENTERPRISE_CONTENT_URI.

Development and testing

To create a work profile, follow these steps:

  1. Install our Test DPC app.
  2. Open the Set up Test DPC app (not the Test DPC app icon).
  3. Follow the on-screen instructions to set up a managed profile.
  4. In the work profile, open the Contacts app and add some sample contacts.

To simulate an IT admin blocking access to work profile contacts, follow these steps:

  1. In the work profile, open the Test DPC app.
  2. Search for the Disable cross-profile contacts search setting or the Disable cross-profile caller ID setting.
  3. Switch the setting to On.

To learn more about testing your app with work profiles, read Test your App for Compatibility with Work Profiles.

Additional resources

To learn more about contacts or the work profile, see these resources: