本开发者指南将介绍如何增强您的应用,以使用工作资料中的联系人数据。如果您之前未使用过 Android 的联系人 API,请参阅联系人提供程序,熟悉一下这些 API。
概览
具有工作资料的设备会将联系人存储在工作资料和个人资料的不同本地目录中。默认情况下,当应用在个人资料中运行时,它不会显示工作联系人。不过,应用可以访问工作资料中的联系信息。例如,Google 的 Android 通讯录应用执行此操作后,会在搜索结果中同时显示个人联系人和工作目录联系人。
用户常常希望使用自己的个人设备和应用办公。通过使用工作资料通讯录,您的应用可以成为用户的日常工作的一部分。
用户体验
考虑应用可能会如何显示工作资料中的联系信息。 最佳方法取决于应用的性质以及用户使用该应用的原因,但请考虑以下问题:
- 您的应用是否应默认包含工作资料联系人,还是应该由用户选择启用?
- 将工作资料联系人与个人资料联系人混用或分开对用户体验流程有何影响?
- 不小心点按工作资料联系人会有什么影响?
- 如果工作资料联系人不可用,应用界面会发生什么变化?
您的应用应明确指明工作资料联系人。或许您可以使用熟悉的工作图标(如公文包)标记该联系人。
例如,Google 通讯录应用(如图 1 所示)执行以下操作来混合列出工作资料联系人和个人资料联系人:
- 插入子标题来分隔列表的工作和个人部分。
- 标记工作联系人带有公文包图标。
- 点按即可在工作资料中打开工作联系人。
如果设备的用户关闭了工作资料,您的应用将无法从工作资料或组织的远程联系人目录中查找联系信息。根据您使用工作资料联系人的方式,您可以在静默时排除这些联系人,或者可能需要停用界面控件。
权限
如果您的应用已在处理用户的联系人,您将获得您在应用清单文件中请求的 READ_CONTACTS
(也可能是 WRITE_CONTACTS
)权限。由于同一个人使用个人资料和工作资料,因此您无需更多权限即可访问工作资料中的联系人数据。
IT 管理员可以阻止与个人资料共享联系信息的工作资料。如果 IT 管理员阻止访问,您的联系人搜索将返回空结果。如果用户关闭了工作资料,您的应用无需处理特定错误。目录 content provider 将继续返回有关用户的工作联系人目录的信息(请参阅目录部分)。如需测试这些权限,请参阅开发和测试部分。
联系人搜索
您可以使用您的应用在个人资料中获取联系人时所用的相同 API 和流程从工作资料中获取联系人。Android 7.0(API 级别 24)或更高版本支持联系人的企业 URI。您需要对 URI 进行以下调整:
- 将 content provider URI 设置为
Contacts.ENTERPRISE_CONTENT_FILTER_URI
,并以查询字符串的形式提供联系人姓名。 - 设置要搜索的联系人目录。例如,
ENTERPRISE_DEFAULT
可在工作资料的本地存储空间中查找联系人。
更改 URI 适用于任何 content provider 机制(例如 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
content provider 来获取用户帐号所使用的目录,并了解关于单个目录的更多信息。
查询 Directory.ENTERPRISE_CONTENT_URI
content provider,从一起返回的个人资料和工作资料中获取目录。Android 7.0(API 级别 24)或更高版本支持搜索工作资料目录。您的应用仍需要用户授予 READ_CONTACTS
权限才能使用其联系人目录。
由于 Android 将联系信息存储在不同类型的本地和远程目录中,因此,您可以调用 Directory
类提供的方法来查找有关目录的更多信息:
isEnterpriseDirectoryId()
- 调用此方法,以确定目录是否来自工作资料帐号。请记住,
ENTERPRISE_CONTENT_URI
content provider 会同时返回个人资料和工作资料的联系人目录。 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
来获取电子邮件地址的个人或工作联系人数据。如果查询此网址,首先会搜索个人联系人,查找完全匹配的结果。如果提供程序与任何个人联系人均不匹配,则提供程序会在工作联系人中搜索匹配项。此 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 = "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(); }
显示工作联系信息
在个人资料中运行的应用可以显示工作资料中的联系人卡片。在 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 列出工作资料和个人资料中的所有目录。 |
开发和测试
如需创建工作资料,请按以下步骤操作:
- 安装 Test DPC 应用。
- 打开设置 Test DPC 应用(而非 Test DPC 应用图标)。
- 按照屏幕上的说明设置受管理资料。
- 在工作资料中,打开通讯录应用并添加一些示例联系人。
如要模拟阻止访问工作资料联系人的 IT 管理员,请按以下步骤操作:
- 在工作资料中,打开 Test DPC 应用。
- 搜索停用跨资料联系人搜索设置或停用跨资料来电显示设置。
- 将设置切换为开启。
如需详细了解如何使用工作资料测试应用,请参阅测试应用与工作资料的兼容性。
其他资源
如需详细了解联系人或工作资料,请参阅以下资源: