工作配置文件联系人

本开发者指南介绍了如何增强您的应用以使用工作配置文件中的联系人数据。如果您以前从未使用过 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. 将设置切换到开启

要了解有关使用工作资料测试应用的更多信息,请阅读测试您的应用以确保其与工作资料兼容

其他资源

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