직장 프로필 연락처

이 개발자 가이드에서는 직장 프로필의 연락처 데이터를 사용하도록 앱을 개선하는 방법을 설명합니다. Android의 연락처 API를 사용해 본 적이 없다면 연락처 제공자를 읽고 API를 숙지하세요.

개요

직장 프로필이 있는 기기는 직장 프로필과 개인 프로필의 개별 로컬 디렉터리에 연락처를 저장합니다. 기본적으로 앱이 개인 프로필에서 실행될 때는 직장 연락처가 표시되지 않습니다. 그러나 앱은 직장 프로필의 연락처 정보에 액세스할 수 있습니다. 예를 들어, 이렇게 하는 앱이 Google의 Android 연락처 앱으로, 검색결과에 개인 연락처와 직장 디렉터리 연락처를 모두 표시합니다.

많은 사용자가 개인 기기와 앱을 업무에 사용하기를 원합니다. 직장 프로필 연락처를 사용하면 내 앱이 사용자의 근무일 일부가 될 수 있습니다.

사용자 환경

앱에서 직장 프로필의 연락처 정보를 표시하는 방법을 고려합니다. 가장 좋은 접근 방식은 앱의 특성과 사람들이 앱을 사용하는 이유에 따라 다르지만 다음 사항을 고려하세요.

  • 앱에 기본적으로 직장 프로필 연락처가 포함되어야 하나요, 아니면 사용자가 선택해야 하나요?
  • 직장 연락처와 개인 프로필 연락처를 함께 사용하거나 분리하면 사용자 흐름에 어떤 영향을 미칠까요?
  • 실수로 직장 프로필 연락처를 탭하면 어떤 영향을 미치나요?
  • 직장 프로필 연락처를 사용할 수 없으면 앱 인터페이스는 어떻게 되나요?

앱에 직장 프로필 연락처를 명확하게 표시해야 합니다. 서류 가방과 같이 익숙한 직장 아이콘을 사용하여 연락처에 배지를 표시할 수도 있습니다.

검색결과를 목록으로 보여주는 스크린샷
그림 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 = "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. 설정을 사용으로 전환합니다.

직장 프로필로 앱을 테스트하는 방법에 관한 자세한 내용은 앱과 직장 프로필과의 호환성 테스트를 참고하세요.

추가 리소스

연락처 또는 직장 프로필에 관해 자세히 알아보려면 다음 리소스를 참고하세요.