工作資料夾聯絡人

本開發人員指南說明如何強化應用程式,以便使用工作資料夾中的聯絡資料。如果您從未使用過 Android 的聯絡人 API,請參閱聯絡人提供者一文,熟悉 API。

總覽

設有工作資料夾的裝置會將聯絡人儲存在工作資料夾和個人資料夾的個別本機目錄中。根據預設,應用程式在個人資料夾中執行時,不會顯示工作聯絡人。但是,應用程式可以從工作資料夾存取聯絡資訊。舉例來說,這類應用程式是 Google 的 Android 聯絡人應用程式,會在搜尋結果中顯示個人和工作目錄聯絡人。

使用者通常會想使用個人裝置和應用程式處理工作。只要利用工作資料夾聯絡人,應用程式就能成為使用者的工作日。

使用者體驗

考量應用程式顯示工作資料夾聯絡資訊的方式。最佳做法取決於應用程式的性質和使用原因,但請考慮以下幾點:

  • 應用程式是否應該預設包含工作資料夾聯絡人,還是使用者應選擇加入?
  • 合併或分離工作資料夾與個人資料夾中的聯絡人,會對使用者的流程造成什麼影響?
  • 不小心輕觸工作資料夾聯絡人會有什麼影響?
  • 如果無法使用工作資料夾聯絡人,應用程式介面會有什麼影響?

您的應用程式應明確顯示工作資料夾聯絡人。您可以用公事包等熟悉的工作圖示 標記聯絡人。

顯示清單內搜尋結果的螢幕截圖
圖 1. Google 聯絡人應用程式如何區隔工作資料夾聯絡人

例如,Google 聯絡人應用程式 (如圖 1 所示) 會執行下列作業,列出工作和個人檔案聯絡人:

  1. 插入子標題,以分隔清單的工作和個人部分。
  2. 帶有公事包圖示的徽章工作聯絡人。
  3. 輕觸工作資料夾後開啟工作聯絡人。

如果使用裝置的使用者關閉工作資料夾,您的應用程式就無法從工作資料夾或機構的遠端聯絡人目錄中查詢聯絡資訊。視您使用工作資料夾聯絡人的方式而定,您可以將這些聯絡人靜音,或是可能需要停用使用者介面控制項。

權限

如果您的應用程式已處理使用者的聯絡人,您將在應用程式資訊清單檔案中,取得所要求的 READ_CONTACTS (或 WRITE_CONTACTS) 權限。同一個使用者會使用個人資料夾和工作資料夾,因此不必取得進一步權限,就能存取工作資料夾中的聯絡人資料。

IT 管理員可以封鎖與個人資料夾共用的工作資料夾聯絡資訊。如果 IT 管理員封鎖存取權,您的聯絡人搜尋作業會傳回空白的結果。當使用者關閉工作資料夾時,應用程式不需要處理特定錯誤。目錄內容供應器會持續傳回使用者工作聯絡人目錄的相關資訊 (請參閱「目錄」一節)。如要測試這些權限,請參閱「開發與測試」一節。

聯絡人搜尋

您可以透過應用程式用來取得個人資料夾中聯絡人的 API 和程序,從工作資料夾取得聯絡人。Android 7.0 (API 級別 24) 以上版本支援聯絡人的企業 URI。您必須對 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,取得電子郵件地址的個人或工作聯絡人資料。先查詢這個網址,系統就會先搜尋個人聯絡人以完全比對。如果供應商沒有與任何個人聯絡人相符,則供應商會搜尋工作聯絡人來進行比對。這個 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 列出工作和個人設定檔中的所有目錄。

開發和測試

如要建立工作資料夾,請按照下列步驟操作:

  1. 安裝 Test DPC 應用程式。
  2. 開啟「設定 Test DPC」應用程式 (而不是「Test DPC」應用程式圖示)。
  3. 按照畫面上的指示設定受管理的設定檔。
  4. 在工作資料夾中開啟「聯絡人」應用程式,然後新增一些聯絡人範例。

如要模擬 IT 管理員禁止存取工作資料夾聯絡人,請按照下列步驟操作:

  1. 在工作資料夾中開啟「Test DPC」應用程式。
  2. 搜尋「停用跨設定檔聯絡人搜尋」設定或「停用跨設定檔來電顯示」設定。
  3. 將設定切換為「開啟」。

如要進一步瞭解如何使用工作資料夾測試應用程式,請參閱「測試應用程式與工作資料夾的相容性」。

其他資源

如要進一步瞭解聯絡人或工作資料夾,請參閱下列資源: