工作配置文件联系人

本开发者指南介绍了如何增强您的应用以使用工作配置文件中的联系人数据。如果您以前从未使用过 Android 的联系人 API,请阅读 联系人提供程序 以熟悉这些 API。

概述

带有 工作配置文件 的设备会将联系人存储在工作和个人配置文件的单独本地目录中。默认情况下,当应用在个人配置文件中运行时,它不会显示工作联系人。但是,应用可以访问工作配置文件中的联系人信息。例如,执行此操作的应用是 Google 的 Android 联系人应用,该应用在搜索结果中显示个人和工作目录联系人。

用户通常希望将他们的个人设备和应用用于工作。通过使用工作配置文件联系人,您的应用可以成为用户工作日的一部分。

用户体验

考虑您的应用如何呈现来自工作配置文件的联系人信息。最佳方法取决于您的应用的性质以及人们使用它的原因,但请考虑以下几点

  • 您的应用是否应默认包含工作配置文件联系人,或者用户是否应选择加入?
  • 混合或分离工作和个人配置文件联系人将如何影响用户的流程?
  • 意外点击工作配置文件联系人会产生什么影响?
  • 当工作配置文件联系人不可用时,您的应用界面会发生什么变化?

您的应用应清楚地指示工作配置文件联系人。也许您可以使用熟悉的商务图标(例如公文包)为联系人添加徽章。

Screenshot showing search results in a list
图 1. Google 联系人应用如何分离工作配置文件联系人

例如,Google 联系人应用(如图 1 所示)执行以下操作以列出工作和个人配置文件联系人的混合列表

  1. 插入子标题以分隔列表的工作和个人部分。
  2. 使用公文包图标为工作联系人添加徽章。
  3. 点击时在工作配置文件中打开工作联系人。

如果使用设备的人员关闭工作配置文件,您的应用将无法从工作配置文件或组织的远程联系人目录中查找联系人信息。根据您使用工作配置文件联系人的方式,您可以静默地忽略这些联系人,或者可能需要禁用用户界面控件。

权限

如果您的应用已经在使用用户的联系人,那么您将拥有在应用清单文件中请求的READ_CONTACTS(或可能是WRITE_CONTACTS)权限。由于同一个人使用个人配置文件和工作配置文件,因此您无需其他权限即可访问工作配置文件中的联系人数据。

IT 管理员可以阻止工作配置文件与个人配置文件共享联系人信息。如果 IT 管理员阻止访问,您的联系人搜索将返回为空结果。如果用户关闭了工作配置文件,您的应用无需处理特定的错误。目录内容提供程序将继续返回有关用户工作联系人目录的信息(请参阅目录部分)。要测试这些权限,请参阅开发和测试部分。

联系人搜索

您可以使用应用用于获取个人配置文件中联系人的相同 API 和流程来获取工作配置文件中的联系人。企业联系人 URI 在 Android 7.0(API 级别 24)或更高版本中受支持。您需要对 URI 进行以下调整

  1. 将内容提供程序 URI 设置为Contacts.ENTERPRISE_CONTENT_FILTER_URI,并提供联系人的姓名作为查询字符串。
  2. 设置要搜索的联系人目录。例如,ENTERPRISE_DEFAULT 查找工作配置文件本地存储中的联系人。

更改 URI 可与任何内容提供程序机制(例如CursorLoader)一起使用,非常适合将联系人数据加载到用户界面中,因为数据访问发生在工作线程上。为简单起见,本指南中的示例调用ContentResolver.query()。以下是您如何在工作配置文件的本地联系人目录中查找联系人的方法

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();
}

目录

许多组织使用远程目录(如 Microsoft Exchange 或 LDAP),其中包含整个组织的联系人信息。您的应用可以帮助用户与组织目录中找到的工作同事进行沟通和共享。请注意,这些目录通常包含数千个联系人,并且您的应用还需要活动的网络连接才能搜索它们。您可以使用Directory 内容提供程序来获取用户帐户使用的目录,并了解有关单个目录的更多信息。

查询Directory.ENTERPRISE_CONTENT_URI 内容提供程序以获取个人配置文件和工作配置文件一起返回的目录。在 Android 7.0(API 级别 24)或更高版本中支持搜索工作配置文件目录。您的应用仍然需要用户授予READ_CONTACTS 权限才能使用其联系人目录。

由于 Android 将联系人信息存储在不同类型的本地和远程目录中,因此Directory 类具有可调用的方法来查找有关目录的更多信息

isEnterpriseDirectoryId()
调用此方法以了解目录是否来自工作配置文件帐户。请记住,ENTERPRISE_CONTENT_URI 内容提供程序将个人和工作配置文件的联系人目录一起返回。
isRemoteDirectoryId()
调用此方法以了解目录是否为远程目录。远程目录可能是企业联系人存储,也可能是用户的社交网络。

以下示例显示了如何使用这些方法来筛选工作配置文件目录

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();
}

该示例获取目录的 ID 和包名称。要显示帮助用户选择联系人目录源的用户界面,您可能需要获取有关目录的更多信息。要查看可能可用的其他元数据字段,请阅读Directory 类参考。

电话查找

应用可以查询PhoneLookup.CONTENT_FILTER_URI 以有效地查找电话号码的联系人数据。如果将此 URI 替换为PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI,则可以从个人和工作配置文件联系人提供程序中获取查找结果。此工作配置文件内容 URI 在 Android 5.0(API 级别 21)或更高版本中可用。

以下示例显示了一个应用查询工作配置文件内容 URI 以配置来电用户界面的情况

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.ENTERPRISE_CONTENT_LOOKUP_URI 获取电子邮件地址的个人或工作联系人数据。首先查询此 URL 以在个人联系人中搜索完全匹配项。如果提供程序与任何个人联系人都不匹配,则提供程序随后将在工作联系人中搜索匹配项。此 URI 在 Android 6.0(API 级别 23)或更高版本中可用。

以下是查找电子邮件地址的联系人信息的方法

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 = "[email protected]"
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 = "[email protected]";
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();
}

显示工作联系人

在个人配置文件中运行的应用可以在工作配置文件中显示联系人卡片。在 Android 5.0 或更高版本中调用ContactsContract.QuickContact.showQuickContact() 以启动工作配置文件中的联系人应用并显示联系人的卡片。

要为工作配置文件生成正确的 URI,您需要调用ContactsContract.Contacts.getLookupUri() 并传递联系人 ID 和查找密钥。以下示例显示了如何获取 URI,然后显示卡片

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();
}

可用性

下表总结了哪些 Android 版本支持个人配置文件中的工作配置文件联系人数据

Android 版本 支持
5.0(API 级别 21) 使用PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI 查找电话号码的工作联系人姓名。
6.0(API 级别 23) 使用Email.ENTERPRISE_CONTENT_LOOKUP_URI 查找电子邮件地址的工作联系人姓名。
7.0(API 级别 24) 使用Contacts.ENTERPRISE_CONTENT_FILTER_URI 查询工作目录中的工作联系人姓名。
使用Directory.ENTERPRISE_CONTENT_URI 列出工作和个人配置文件中的所有目录。

开发和测试

要创建工作配置文件,请按照以下步骤操作

  1. 安装我们的Test DPC 应用。
  2. 打开设置 Test DPC 应用(不是 Test DPC 应用图标)。
  3. 按照屏幕上的说明设置托管配置文件。
  4. 在工作配置文件中,打开联系人应用并添加一些示例联系人。

要模拟 IT 管理员阻止访问工作配置文件联系人,请按照以下步骤操作

  1. 在工作配置文件中,打开Test DPC 应用。
  2. 搜索禁用跨配置文件联系人搜索设置或禁用跨配置文件主叫方 ID设置。
  3. 将设置切换到开启

要了解有关使用工作配置文件测试应用的更多信息,请阅读测试应用与工作配置文件的兼容性

其他资源

要了解有关联系人或工作配置文件的更多信息,请参阅以下资源