연락처 provider는 사람에 대한 정보를 담은 중앙 데이터 저장소를 관리하는 강력하고 유연한 안드로이드 구성요소입니다. 연락처 provider는 기기의 연락처 애플리케이션에서 볼 수 있는 데이터의 소스이며, 개발자는 자신의 애플리케이션에서 연락처 provider의 데이터에 접근하고 기기와 온라인 서비스 간에 데이터를 전송할 수도 있습니다.
이 provider는 광범위한 데이터 소스를 수용하고 각 개인에 대해 가능한 한 많은 데이터를 관리하려고 하므로 조직이 복잡합니다. 따라서 이 provider의 API에는 데이터 검색과 수정을 모두 용이하게 하는 광범위한 contract class와 인터페이스 세트가 포함되어 있습니다.
연락처 provider는 안드로이드 content provider 구성요소입니다.

사용자에 관한 세 가지 유형의 데이터를 유지합니다. 각 유형은 그림에서와 같이 provider가 제공하는 테이블에 대응합니다. 이 세 개의 테이블은 일반적으로 해당 contract class 이름으로 불립니다. 이 클래스는 테이블에서 사용하는 콘텐츠 URI, 열 이름, 열 값의 상수를 정의합니다.
ContactsContract.Contacts: 원시 연락처 행들을 종합하여 얻은 서로 다른 사람을 나타내는 행.ContactsContract.RawContacts: 사용자 계정 및 유형에 따라 개인의 데이터 요약이 포함된 행.ContactsContract.Data: 이메일 주소나 전화번호와 같은 원시 연락처의 세부정보가 포함된 행.ContactsContract의 contract class에서의 이 외의 다른 테이블은 연락처 provider가 작업을 관리하거나 기기의 연락처 또는 애플리케이션에서 특정 기능을 지원하는 데 사용하는 보조 테이블입니다.
원시 연락처는 특정 계정 유형과 계정 이름에서 가져온 한 사람의 데이터를 의미합니다. 이때 계정 유형은 Google, Naver 같이 연락처를 제공하는 서비스의 종류를 의미하고 계정 이름은 계정을 식별하는 고유 문자열을 의미합니다. 보통 계정의 로그인 ID나 이메일 주소를 뜻합니다.
연락처 provider는 한 사람의 데이터 출처로 여러 온라인 서비스를 허용하기 때문에, 동일한 사람에 대해 여러 개의 원시 연락처를 가질 수 있습니다. 또한, 동일한 계정 유형 안에서 서로 다른 계정을 통해 한 사람의 데이터를 결합하는 것도 가능합니다.
원시 연락처의 대부분의 데이터는 ContactsContract.RawContacts 테이블에 저장되지 않습니다. 대신, 하나 이상의 행으로 ContactsContract.Data 테이블에 저장됩니다. 각 데이터 행에는 Data.RAW_CONTACT_ID 컬럼이 있으며, 이는 해당 데이터 행이 속한 ContactsContract.RawContacts 행의 _ID 값을 가집니다.
ContactsContract.RawContacts 테이블의 중요 열은 다음과 같습니다.
ACCOUNT_NAMEACCOUNT_TYPEcom.google. DELETED다음은 ContactsContract.RawContacts 테이블에 관한 중요한 참고사항입니다.
원시 연락처의 이름은 ContactsContract.RawContacts의 행에 저장되지 않음. 대신 ContactsContract.Data 테이블의 ContactsContract.CommonDataKinds.StructuredName 행에 저장. StructuredName은 이름을 담는 데이터 타입. 하나의 원시 연락처에는 이름이 하나만 있으므로 이 타입의 행은 1개만 존재
원시 연락처 행에서 본인 소유의 계정 데이터를 사용하려면 이를 먼저 AccountManager에 등록해야 함. 이렇게 하려면 사용자에게 계정 유형과 계정 이름을 계정 목록에 추가하라는 메시지를 표시해야 함. 이렇게 하지 않으면 연락처 provider가 원시 연락처 행을 자동으로 삭제. 즉, 연락처 provider는 해당 계정이 AccountManager에 등록되어 있어야 유효하다고 판단하기 때문에 유효한 계정의 대한 행만 유지. 예를 들어 앱에서 com.example.dataservice 도메인을 사용하는 웹 기반 서비스의 연락처 데이터를 유지하고 서비스의 사용자 계정이 becky.sharp@dataservice.example.com인 경우 사용자가 먼저 계정 '유형' (com.example.dataservice)과 계정 '이름'(becky.smart@dataservice.example.com)을 추가해야 앱에서 원시 연락처 행 추가 가능. 이 요구사항을 사용자에게 문서로 설명하거나 사용자에게 유형과 이름을 추가하라는 메시지를 표시하거나 두 가지를 모두 수행해도 됨.
원시 연락처의 작동 방식을 이해하려면 기기에 다음 세 개의 사용자 계정이 정의된 사용자 'Emily Dickinson'이 있다고 가정해 보겠습니다.
이 사용자는 계정 설정에서 세 계정 모두의 연락처 동기화를 사용 설정했습니다. Emily Dickinson이 브라우저 창을 열고 emily.dickinson@gmail.com으로 Gmail에 로그인한 다음 연락처를 열고 'Thomas Higginson'을 추가한다고 가정해 보겠습니다. 나중에 그녀는 emilyd@gmail.com으로 Gmail에 로그인하고 'Thomas Higginson'에게 이메일을 보냅니다. 그러면 자동으로 연락처에 추가됩니다. 또한 트위터에서 'colonel_tom' (Thomas Higginson의 Twitter ID)도 팔로우합니다.
연락처 provider는 이 작업의 결과로 원시 연락처를 세 개 생성합니다.
앞서 언급했듯이 원시 연락처의 데이터는 원시 연락처의 _ID 값과 연결된 ContactsContract.Data 행에 저장됩니다. 이렇게 하면 하나의 원시 연락처에 같은 유형의 데이터를 여러 개의 인스턴스로 생성할 수 있습니다.
예를 들어 Thomas Higginson는 Google 계정으로 emilyd@gmail.com의 원시 연락처를 가집니다. 해당 연락처 행의 집 이메일 주소가 thigg@gmail.com이고 직장 이메일 주소가 thomas.higginson@gmail.com이면, 연락처 provider는 두 이메일 주소 행을 저장하고 두 가지를 원시 연락처에 연결합니다. 즉, 1개의 원시 연락처에 대해 2개의 Data 테이블이 참조합니다. 이때 원시 연락처의 _ID값을 통해 연결됩니다.
이 테이블 하나에 여러 가지 유형의 데이터가 저장될 수 있습니다. 표시 이름, 전화번호, 이메일, 우편 주소, 사진, 웹사이트 세부정보 행은 모두 ContactsContract.Data 테이블에 있습니다. 이를 관리하는 데 도움이 되도록 ContactsContract.Data 테이블에는 descriptive 열 몇 개와 generic 열 몇 개 있습니다. Descriptive 열은 행의 데이터 유형과 관계없이 동일한 의미를 지니며, generic 열은 데이터 유형에 따라 의미가 달라집니다.
다음은 설명이 포함된 열 이름의 몇 가지 예입니다.
RAW_CONTACT_ID
_ID 열 값MIMETYPE
ContactsContract.CommonDataKinds의 서브클래스에 정의된 MIME 유형 사용. 이러한 MIME 유형은 오픈소스이며 연락처 provider와 호환되는 모든 애플리케이션 또는 동기화 어댑터에서 사용 가능.IS_PRIMARY
IS_PRIMARY 열은 그 유형에서 기본데이터가 무엇인지 표시ContactsContract.Data row의 IS_PRIMARY 열 값이 0이 아닌 값으로 설정일반적으로 사용 가능한 DATA1~DATA15라는 generic 열 15개와 동기화 어댑터에서만 사용되는 추가 generic 열 SYNC1~SYNC4 4개가 있습니다.
Generic 열 이름 상수는 행에 포함된 데이터 유형과 관계없이 항상 작동합니다. DATA1 열은 인덱스로 사용됩니다. 연락처 provider는 provider가 가장 자주 쿼리의 대상이 될 것으로 예상하는 데이터에 항상 DATA1 열을 사용합니다.
예를 들어 이메일 행의 경우 이 열에 실제 이메일 주소가 저장됩니다. 쿼리 대상이 주로 휴대전화면 실제 전화번호 값이 저장됩니다. 그렇기 때문에 위에서 데이터 유형에 따라 의미가 달라진다고 하는 겁니다.
규칙에 따라 DATA15 열은 썸네일 사진과 같은 BLOB(Binary Large Object) 데이터 저장에 사용됩니다.
특정 유형의 행에 관한 열 작업을 용이하게 하기 위해 연락처 provider는 ContactsContract.CommonDataKinds의 서브클래스에 정의된 유형별 열 이름 상수도 제공합니다. 동일한 열 이름을 쉽게 구분하여 사용할 수 있는 여러 개의 상수 이름을 부여하므로 특정 유형의 행에 있는 데이터 접근에 도움이 됩니다.
예를 들어 ContactsContract.CommonDataKinds.Email 클래스는 ContactsContract.Data 행에서 MIME 유형이 Email.CONTENT_ITEM_TYPE인 유형별 열 이름 상수를 정의합니다. 이 클래스에는 이메일 주소 열에 대해 ADDRESS 상수를 제공됩니다. ADDRESS의 실제 값은 generic column과 동일한 DATA1입니다. DATA1을 유형에 따라 상수로 쉽게 구분하는 겁니다.
Provider에서 사전 정의된 MIME 유형을 지정한 ContactsContract.Data 테이블에 사용자 정의 데이터를 추가하면 안됩니다. 그렇게 하면 데이터가 손실되거나 provider가 오작동을 일으킬 수 있습니다. 예를 들어 DATA1 열에 이메일 타입으로 지정했으면 이메일 주소를 입력해야하지 사용자 이름이 포함된 MIME 유형 Email.CONTENT_ITEM_TYPE 행을 추가하면 안 됩니다. 행에 사용자 정의 MIME 유형을 사용해서 고유한 유형별 열 이름을 자유롭게 정의해야 원하는 대로 열을 사용할 수 있습니다.

그림은 descriptive열과 data열이 ContactsContract.Data 행에 표시되는 방식과 유형별 열 이름이 generic 열 이름에 오버레이되는 방식을 보여줍니다.
다음은 가장 보편적으로 사용되는 유형별 열 이름 클래스를 나열한 겁니다.
ContactsContract.CommonDataKinds.StructuredName
ContactsContract.CommonDataKinds.Photo
ContactsContract.CommonDataKinds.Email
ContactsContract.CommonDataKinds.StructuredPostal
ContactsContract.CommonDataKinds.GroupMembership
연락처 provider는 계정 유형과 이름으로 이루어진 모든 원시 연락처 행들을 모으고 동일 인물일 경우 하나로 합쳐서 연락처를 형성합니다. 이렇게 하면 사용자가 한 사람에 관해 수집한 모든 데이터를 쉽게 표시하고 수정할 수 있습니다.
연락처 provider는 새로운 연락처 행의 생성과 원시 연락처를 기존 연락처 행과 합치는 작업을 관리합니다. 그렇기 때문에 연락처 자체는 애플리케이션과 동기화 어댑터에서 직접 추가할 수는 없으며 연락처 행의 일부 열은 읽기만 가능합니다. 이때 '읽기 전용'으로 표시된 열을 업데이트하려고 하면 업데이트가 무시됩니다. insert()를 사용하여 연락처 provider에 연락처를 추가하려고 하면 UnsupportedOperationException 예외가 발생합니다.
연락처 provider는 기존 연락처와 일치하지 않는 새로운 원시 연락처가 추가되면 새 연락처를 생성합니다. Provider는 기존 원시 연락처의 데이터가 변경되어 이전에 연결된 연락처와 더 이상 일치하지 않는 경우에도 이 작업을 실행합니다. 애플리케이션이나 동기화 어댑터가 기존 연락처와 일치하는 새로운 원시 연락처를 만들면 새로운 원시 연락처가 기존 연락처에 합쳐집니다.
연락처 provider는 연락처 테이블에 있는 연락처 행의 _ID 열을 사용하여 연락처 행과 원시 연락처 행을 연결합니다. 원시 연락처 테이블 ContactsContract.RawContacts의 CONTACT_ID 열에는 각 원시 연락처 행과 연결된 연락처 행의 _ID 값이 포함됩니다.
ContactsContract.Contacts 테이블에는 연락처 행에 대한 '영구' 링크인 LOOKUP_KEY 열도 있습니다. 연락처 provider는 연락처를 자동으로 유지하므로, 집계 또는 동기화에 응답하여 연락처 행의 _ID 값을 변경할 수도 있습니다. 이 경우에도 연락처의 LOOKUP_KEY와 결합된 콘텐츠 URI인 CONTENT_LOOKUP_URI는 여전히 연락처 행을 가리키므로 LOOKUP_KEY를 사용하여 '즐겨찾기' 연락처 등에 대한 링크를 유지할 수 있습니다. 이 열에는 _ID 열의 형식과 관련 없는 자체 형식이 있습니다.

그림은 세 가지 기본 테이블이 서로 연결되는 방식을 나타낸겁니다.
Google Play 스토어에 앱을 게시하거나 Android 10 (API 수준 29) 이상을 실행하는 기기에 앱이 있는 경우 일부 연락처 데이터 필드와 메서드들은 더 이상 사용되지 않습니다. 시스템은 아래의 데이터 필드에 작성된 값을 주기적으로 지웁니다.
ContactsContract.ContactOptionsColumns.LAST_TIME_CONTACTEDContactsContract.ContactOptionsColumns.TIMES_CONTACTEDContactsContract.DataUsageStatColumns.LAST_TIME_USEDContactsContract.DataUsageStatColumns.TIMES_USED위의 데이터 필드를 설정하는 데 사용하는 API도 사용이 중단되었습니다.
ContactsContract.Contacts.markAsContacted()ContactsContract.DataUsageFeedback또한 다음 필드에서는 자주 사용되는 연락처를 반환하지 않습니다. 이러한 필드 중 일부는 연락처가 특정 데이터 종류의 일부인 경우에만 연락처 순위에 영향을 줍니다.
ContactsContract.Contacts.CONTENT_FREQUENT_URIContactsContract.Contacts.CONTENT_STREQUENT_URIContactsContract.Contacts.CONTENT_STREQUENT_FILTER_URICONTENT_FILTER_URI (Email, Phone, Callable, Contactables 데이터 종류에만 영향을 미침)ENTERPRISE_CONTENT_FILTER_URI (Email, Phone 및 Callable 데이터 종류에만 영향을 미침)앱에서 이러한 필드 또는 API에 접근하거나 이를 업데이트하는 경우 다른 방법을 사용해야 합니다. 예를 들어 private content provider 또는 앱이나 백엔드 시스템 내에 저장된 기타 데이터를 사용하여 처리할 수 있습니다. 이 변경사항으로 인해 앱 기능이 영향을 받지 않는지 확인하려면 이러한 데이터 필드를 수동으로 지우면 됩니다. 이렇게 하려면 Android 4.1 (API 수준 16) 이상을 실행하는 기기에서 다음 ADB 명령어를 실행하세요.
adb shell content delete \
--uri content://com.android.contacts/contacts/delete_usage
사용자는 연락처 데이터를 기기에 직접 입력하지만 데이터는 동기화 어댑터를 통해 웹 서비스에서 연락처 provider로 전송되어 기기와 서비스 간에 데이터 전송을 자동화합니다. 동기화 어댑터는 시스템의 제어하에 백그라운드에서 실행되며 content resolver 메서드를 호출하여 데이터를 관리합니다.
안드로이드에서 동기화 어댑터와 함께 작업하는 웹 서비스는 계정 유형으로 식별됩니다. 각 동기화 어댑터는 하나의 계정 유형에서 작동하지만 이러한 유형에는 여러 계정 이름을 지원할 수 있습니다.
계정 유형은 어떤 서비스에 연결되는지를 나타내며, 사용자는 인증을 해야 해당 서비스를 사용할 수 있습니다. 계정 유형은 단순한 문자열이 아니라, AccountManager가 계정을 관리할 때 실제로 사용하는 key 값입니다. 예를 들어 Google 주소록의 계정 유형은 google.com이라는 문자열이 쓰입니다.
계정 이름은 계정 유형에 대한 특정 계정 또는 로그인을 식별합니다. Google 연락처 계정은 Google 계정과 동일합니다. 이때 Google 계정은 계정 이름으로 이메일 주소를 사용합니다. 다른 서비스에서는 단일 단어 사용자 이름 또는 숫자 ID를 사용할 수도 있습니다.
계정 유형은 고유하지 않아도 됩니다. 사용자는 여러 Google 주소록 계정을 구성하고 연락처 provider에 데이터를 다운로드할 수 있습니다. 이는 사용자에게 개인 계정 이름으로 개인 연락처 목록이 있고 업무용으로 다른 연락처 목록이 있는 경우에 발생할 수 있습니다. 계정 이름은 일반적으로 고유합니다. 이 둘은 함께 사용되어 연락처 provider와 외부 서비스 간의 특정 데이터 흐름을 식별합니다.
서비스의 데이터를 연락처 provider로 전송하려면 자체 동기화 어댑터를 작성해야 합니다. 이때 동기화 해줄 별도의 서버 또한 필요합니다.

그림은 연락처 provider가 사람에 관한 데이터 흐름 속에서 어떤 위치와 역할을 하는지를 보여줍니다. 동기화 어댑터라고 표시된 상자에서 각 어댑터는 계정 유형에 따라 이름이 지정됩니다.
또한 동기화 어댑터는 각 어댑터에 해당하는 계정 유형을 사용한다는 말은 계정 유형은 해당 동기화 어댑터에서만 접근 가능하다는 의미입니다. 그렇기에 우리는 임의로 구글 계정용 동기화 어댑터를 사용하지 못하는 겁니다. 그래서 API를 사용하게 됩니다. 물론 Google People API는 네트워크 API를 통한 연동이지, Contacts Provider 사용하는 건 아닙니다.
연락처 provider에 접근하려는 애플리케이션은 다음 권한을 요청해야 합니다.
하나 이상의 테이블에 대한 읽기 권한: READ_CONTACTS, AndroidManifest.xml에 <uses-permission> 요소와 <uses-permission android:name="android.permission.READ_CONTACTS">로 지정
하나 이상의 테이블에 대한 쓰기 권한: WRITE_CONTACTS, AndroidManifest.xml에 <uses-permission> 요소와 <uses-permission android:name="android.permission.WRITE_CONTACTS">로 지정
이들 권한은 사용자 프로필 데이터와는 별개입니다.
사용자의 연락처 데이터는 개인 정보이며 민감한 정보라는 점을 기억해야 합니다. 사용자는 자신의 개인정보 보호를 중요하게 생각하므로 애플리케이션이 본인 또는 연락처에 관한 데이터를 수집하는 걸 원하지 않습니다. 사용자의 연락처 데이터에 접근할 권한이 필요한 이유가 분명하지 않으면 애플리케이션에 낮은 평점을 매기거나 설치를 거부할 수도 있습니다.
ContactsContract.Contacts 테이블에는 기기 사용자의 프로필 데이터가 포함된 단일 행이 있습니다. 이 데이터는 사용자의 연락처 중 하나가 아니라 기기의 사용자를 설명합니다.
프로필 연락처 행은 프로필을 사용하는 각 시스템의 원시 연락처 행에 연결됩니다. 각 프로필 원시 연락처 행에는 여러 개의 데이터 행이 있을 수 있습니다. 사용자 프로필에 접근하기 위한 상수는 ContactsContract.Profile 클래스에서 사용할 수 있습니다.
사용자 프로필에 접근하려면 특수 권한이 필요합니다. 읽기와 쓰기에 필요한 READ_CONTACTS 및 WRITE_CONTACTS 권한 외에 사용자 프로필에 접근하려면 읽기 및 쓰기 접근에 관한 android.Manifest.permission#READ_PROFILE과 android.Manifest.permission#WRITE_PROFILE 권한이 각각 필요합니다.
사용자의 프로필은 민감한 정보로 간주해야 합니다. android.Manifest.permission#READ_PROFILE 권한을 사용하면 개발자가 기기 사용자의 개인 식별 데이터에 접근할 수 있게 해줍니다. 애플리케이션 설명에서 사용자에게 사용자 프로필 접근 권한이 필요한 이유를 알려야 합니다.
사용자 프로필이 포함된 연락처 행을 검색하려면 ContentResolver.query()를 호출합니다. 콘텐츠 URI는 CONTENT_URI로 설정하고 선택 기준은 필요없습니다. 콘텐츠 URI는 원시 연락처 또는 프로필의 데이터를 검색하기 위한 기본 URI로 사용할 수도 있습니다. 아래 코드는 프로필에 대한 데이터를 검색합니다.
//java
// 사용자 프로필 검색을 위한 열 설정
projection = new String[]
{
Profile._ID,
Profile.DISPLAY_NAME_PRIMARY,
Profile.LOOKUP_KEY,
Profile.PHOTO_THUMBNAIL_URI
};
//연락처 provider로부터 프로필 검색
profileCursor =
getContentResolver().query(
Profile.CONTENT_URI,
projection ,
null,
null,
null);
//kotlin
// 사용자 프로필 검색을 위한 열 설정
projection = arrayOf(
ContactsContract.Profile._ID,
ContactsContract.Profile.DISPLAY_NAME_PRIMARY,
ContactsContract.Profile.LOOKUP_KEY,
ContactsContract.Profile.PHOTO_THUMBNAIL_URI
)
//연락처 provider로부터 프로필 검색
profileCursor = contentResolver.query(
ContactsContract.Profile.CONTENT_URI,
projection,
null,
null,
null
)
여러 연락처 행을 검색하고 그중 하나가 사용자 프로필인지 확인하려면 행의 IS_USER_PROFILE 열을 검사하면 됩니다. 이 열은 연락처가 사용자 프로필인 경우 '1'로 설정됩니다.
연락처 provider는 저장소에 있는 연락처 데이터의 상태를 추적하는 데이터를 관리합니다. 저장소에 대한 이 메타데이터는 원시 연락처, 데이터, 연락처 테이블 행, ContactsContract.Settings 테이블, ContactsContract.SyncState 테이블 등의 다양한 위치에 저장됩니다.
다음 표는 이러한 메타데이터 각각이 미치는 영향을 보여줍니다.

연락처 provider의 테이블들은 계층 구조로 구성되어 있기 때문에, 특정 행과 그것에 연결된 모든 자식 행들을 함께 가져오는 게 유용할 때가 많습니다. 예를 들어, 어떤 사람의 모든 정보를 표시하려면 하나의 ContactsContract.Contacts 행에 해당하는 모든 ContactsContract.RawContacts 행을 가져오거나, 하나의 ContactsContract.RawContacts 행에 해당하는 모든 ContactsContract.CommonDataKinds.Email 행을 가져옵니다. 이를 가능하게 하기 위해 연락처 provider는 엔티티라는 구조를 제공하며, 이는 테이블 간의 데이터베이스 조인처럼 동작합니다.
엔티티는 부모 테이블과 자식 테이블에서 선택된 열로 구성된 테이블과 같습니다. 엔티티를 쿼리할 때는, 엔티티에서 제공하는 열을 기준으로 projection과 검색 조건을 지정합니다. 결과는 Cursor로 반환되며, 여기에는 검색된 각 자식 테이블 행마다 한 줄이 포함됩니다. 예를 들어, ContactsContract.Contacts.Entity를 쿼리하여 특정 연락처 이름과 그 이름에 연결된 모든 원시 연락처의 ContactsContract.CommonDataKinds.Email 행을 요청하면, Cursor에는 각 이메일 행마다 하나의 행으로 반환됩니다.
엔티티를 사용하면 쿼리가 단순해집니다. 엔티티를 이용하면 하나의 연락처이나 원시 연락처에 대한 모든 연락처 데이터를 한 번에 가져올 수 있으며, 먼저 부모 테이블을 조회해 ID를 얻고, 그 ID로 다시 자식 테이블을 조회해야 하는 번거로움을 피할 수 있습니다.
또한 연락처 provider는 엔티티에 대한 쿼리를 단일 트랜잭션으로 처리하므로, 가져온 데이터가 내부적으로 일관성을 유지합니다. 엔티티는 보통 부모와 자식 테이블의 모든 열을 포함하지 않습니다. 엔티티의 열 이름 상수 목록에 없는 열 이름을 사용하면 예외가 발생합니다.
아래 코드는 하나의 연락처에 대해 모든 원시 연락처 행을 가져오는 방법을 보여줍니다. "main"과 "detail" 두 개의 activity로 구성된 애플리케이션의 일부입니다. Main activity는 연락처 목록을 보여주고 사용자가 하나를 선택하면 해당 연락처의 ID를 detail activity로 전달합니다. Detail activity는 ContactsContract.Contacts.Entity를 사용해 선택된 연락처에 연결된 모든 원시 연락처의 데이터 행을 표시합니다. 아래 코드는 details의 activity코드입니다.
//java
...
/*
* URI에 엔티티 경로를 추가. 연락 provider의 경우,
* 예상되는 URI는 content://com.google.contacts/#/entity. (#는 ID 값)
*/
contactUri = Uri.withAppendedPath(
contactUri,
ContactsContract.Contacts.Entity.CONTENT_DIRECTORY);
// LOADER_ID로 식별되는 로더 초기화
getLoaderManager().initLoader(
LOADER_ID, // 초기화할 로더의 식별자
null, // 로더에 전달할 인자(해당 코드에는 없음)
this); // activity의 context
// 리스트 뷰에 연결할 새 cursor 어댑터를 생성
cursorAdapter = new SimpleCursorAdapter(
this, // activity의 context
R.layout.detail_list_item, // detail위젯을 포함하는 뷰 아이템
mCursor, // 데이터를 제공하는 cursor
fromColumns, //데이터를 제공하는 커서 열
toViews, // 데이터를 표시할 뷰 아이템의 뷰들
0); // 플래그
// ListView의 백업 어댑터 설정
rawContactList.setAdapter(cursorAdapter);
...
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
/*
* 가져올 열을 설정.
* RAW_CONTACT_ID는 데이터 행과 연결된 원시 연락처를 식별하기 위해 포함.
* DATA1은 데이터 행의 첫 번째 컬럼을 포함(보통 가장 중요한 값).
* MIMETYPE은 데이터 행의 데이터 타입을 나타냄.
*/
String[] projection =
{
ContactsContract.Contacts.Entity.RAW_CONTACT_ID,
ContactsContract.Contacts.Entity.DATA1,
ContactsContract.Contacts.Entity.MIMETYPE
};
/*
* 조회된 cursor를 원시 연락처 id 기준으로 정렬,
* 하나의 원시 연락처에 대한 모든 데이터 행을 모아둠.
*/
String sortOrder =
ContactsContract.Contacts.Entity.RAW_CONTACT_ID +
" ASC";
/*
* 새로운 CursorLoader 반환.
* 인자는 ContentResolver.query()와 비슷하지만,
* context 인자를 통해 사용할 ContentResolver의 위치를 제공.
*/
return new CursorLoader(
getApplicationContext(), // activity의 context
contactUri, // 단일 연락처에 대한 엔티티 콘텐츠 URI
projection, // 가져올 열
null, // 모든 원시 연락처와 그 데이터 행을 가져옴.(selection)
null, //가져올 행의 조건 바인딩의 실제 값(selectionArgs)
sortOrder);// 원시 연락처 ID 기준으로 정렬
}
//kotlin
/*
* URI에 엔티티 경로를 추가. 연락 provider의 경우,
* 예상되는 URI는 content://com.google.contacts/#/entity. (#는 ID 값)
*/
contactUri = Uri.withAppendedPath(
contactUri,
ContactsContract.Contacts.Entity.CONTENT_DIRECTORY
)
// LOADER_ID로 식별되는 로더 초기화
loaderManager.initLoader(
LOADER_ID, // 초기화할 로더의 식별자
null, /// 로더에 전달할 인자(해당 코드에는 없음)
this // activity의 context
)
/ 리스트 뷰에 연결할 새 cursor 어댑터를 생성
cursorAdapter = SimpleCursorAdapter(
this, // activity의 context
R.layout.detail_list_item, // detail 위젯을 포함하는 뷰 아이템
mCursor, // 데이터를 제공하는 cursor
fromColumns, // 데이터를 제공하는 커서 열
toViews, // 데이터를 표시할 뷰 아이템의 뷰들
0) // 플래그
// ListView의 백업 어댑터를 설정
rawContactList.adapter = cursorAdapter
...
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
/*
* 가져올 열을 설정.
* RAW_CONTACT_ID는 데이터 행과 연결된 원시 연락처를 식별하기 위해 포함.
* DATA1은 데이터 행의 첫 번째 컬럼을 포함(보통 가장 중요한 값).
* MIMETYPE은 데이터 행의 데이터 타입을 나타냄.
*/
val projection: Array<String> = arrayOf(
ContactsContract.Contacts.Entity.RAW_CONTACT_ID,
ContactsContract.Contacts.Entity.DATA1,
ContactsContract.Contacts.Entity.MIMETYPE
)
/*
* 조회된 cursor를 원시 연락처 id 기준으로 정렬,
* 하나의 원시 연락처에 대한 모든 데이터 행을 모아둠.
*/
val sortOrder = "${ContactsContract.Contacts.Entity.RAW_CONTACT_ID} ASC"
/*
* 새로운 CursorLoader 반환.
* 인자는 ContentResolver.query()와 비슷하지만,
* context 인자를 통해 사용할 ContentResolver의 위치를 제공.
*/
return CursorLoader(
applicationContext, // activity의 context
contactUri, // 단일 연락처에 대한 엔티티 콘텐츠 URI
projection, // 가져올 열
null, // 모든 원시 연락처와 그 데이터 행을 가져옴(selection)
null, //가져올 행의 조건 바인딩의 실제 값(selectionArgs)
sortOrder // 원시 연락처 ID 기준으로 정렬
)
}
로드가 완료되면 LoaderManager에서 onLoadFinished() 콜백을 호출합니다. 이 메서드로 수신되는 인수 중 하나는 쿼리 결과가 포함된 Cursor입니다. 자체 앱에서는 이 Cursor에서 데이터를 가져와 표시하거나 추가로 작업할 수 있습니다.
가능하면 ContentProviderOperation 객체 타입의 ArrayList를 생성하고 applyBatch()를 호출하여 연락처 provider 데이터를 배치 모드로 삽입, 업데이트 및 삭제해야 합니다. 연락처 provider가 applyBatch()의 모든 작업을 단일 트랜잭션으로 실행하므로 수정사항으로 인해 연락처 저장소가 일관되지 않은 상태로 남아 있지 않습니다.
배치 수정을 사용하면 원시 연락처와 그 세부 데이터의 동시 삽입도 용이합니다. 만약 단일 원시 연락처를 수정하려면 앱에서 수정을 직접 처리하는 대신 기기의 연락처 애플리케이션으로 인텐트를 전송하는 게 좋습니다.
많은 수의 작업이 포함된 일괄 수정은 다른 프로세스를 차단하여 전반적인 사용자 환경을 저하할 수 있습니다. 실행하고자 하는 모든 수정사항을 가능한 한 적은 수의 별도 목록에 정리하고 이와 동시에 시스템이 차단되지 않도록 하려면 하나 이상의 작업에 yield 지점를 설정해야 합니다. Yield 지점은 isYieldAllowed() 값이 true로 설정된 ContentProviderOperation 객체입니다. 연락처 provider가 yield 지점을 만나면 다른 프로세스가 실행되도록 작업을 일시 중지하고 현재 트랜잭션을 닫습니다. Provider가 다시 시작되면 arrayList에서 다음 작업을 계속 진행하고 새 트랜잭션을 시작합니다.
Yield 지점을 설정하면, applyBatch()를 한 번 호출하더라도 내부적으로 여러 개의 트랜잭션으로 나뉘어 실행될 수 있습니다. 따라서 관련된 행 집합의 마지막 작업에 yield 지점을 설정하는 게 좋습니다.
예를 들어, 하나의 원시 연락처와 그에 연결된 데이터행들을 추가하는 작업 집합에서는 마지막 작업에 yield 지점을 설정하거나, 단일 연락처와 관련된 행 집합의 마지막 작업에 yield 지점을 설정해야 합니다.
또한 yield 지점은 원자적 연산 단위이기도 합니다. 두 개의 yield 지점 사이의 모든 접근은 하나의 단위로서 성공하거나 실패합니다. Yield 지점을 전혀 설정하지 않으면, 연산 단위는 배치 전체가 됩니다. 연산 단위는 가장 작은 원자적 단위로써 의미가 없게 됩니다. Yield 지점을 사용하면, 연산이 시스템 성능을 저하시키는 걸 방지하면서도 연산의 일부 집합이 원자적으로 처리되도록 할 수 있습니다.
새로운 원시 연락처 행과 그에 연결된 데이터행들을 ContentProviderOperation 객체 집합으로 삽입할 때, 데이터 행들을 원시 연락처 행에 연결하려면 원시 연락처의 _ID 값을 RAW_CONTACT_ID 열에 넣어야 합니다. 그러나 데이터 행을 위한 ContentProviderOperation을 만들 때는, 아직 원시 연락처 행을 실행하기 전이기 때문에 ID 값이 생성되지 않아서 사용할 수 없습니다.
이를 해결하기 위해 ContentProviderOperation.Builder 클래스에는 withValueBackReference() 메서드가 있습니다. 이 메서드를 사용하면 같은 배치 작업에 포함된 이전 작업의 실행 값을 사용할 수 있기 때문에 해당 결과 값의 ID를 참조하여 열을 삽입하거나 수정할 수 있습니다. withValueBackReference() 메서드에는 두 가지 인수가 있습니다.
keypreviousResultapplyBatch()의 ContentProviderResult 객체 배열에 있는 값의 0 기반 인덱스. 배치 작업이 적용되면 각 작업의 결과가 임시 배열에 저장. previousResult은 이러한 결과 중 어느 인덱스의 값이며 key 값을 사용하여 검색 및 저장. 이렇게 하면 새 원시 연락처행을 삽입하고 _ID 값을 반환하여 다음 ContactsContract.Data 행을 추가할 때 해당 값을 역참조 가능. applyBatch()를 처음 호출하면 사용자가 제공하는 ContentProviderOperation 객체의 ArrayList 크기와 같은 크기로 전체 결과 배열이 생성. 그러나 결과 배열의 모든 요소는 null로 설정되며, 아직 적용되지 않은 작업의 결과를 역참조하려고 하면 withValueBackReference()에서 예외가 발생.다음 예제는 새로운 원시 연락처와 데이터를 배치 삽입하는 방법을 보여줍니다. Yield 지점을 설정하고 역참조를 사용하는 코드가 포함되어 있습니다.
첫 번째 코드는 UI에서 연락처 데이터를 검색합니다. 이 시점에서 사용자는 이미 새 원시 연락처를 추가할 계정을 선택했습니다.
//java
// 현재 선택한 계정을 사용하여 현재 UI 값에서 연락처 항목 생성
protected void createContactEntry() {
/*
* UI에서 값 얻기
*/
String name = contactNameEditText.getText().toString();
String phone = contactPhoneEditText.getText().toString();
String email = contactEmailEditText.getText().toString();
int phoneType = contactPhoneTypes.get(
contactPhoneTypeSpinner.getSelectedItemPosition());
int emailType = contactEmailTypes.get(
contactEmailTypeSpinner.getSelectedItemPosition());
//kotlin
// 현재 선택한 계정을 사용하여 현재 UI 값에서 연락처 항목 생성
private fun createContactEntry() {
/*
* UI에서 값 얻기
*/
val name = contactNameEditText.text.toString()
val phone = contactPhoneEditText.text.toString()
val email = contactEmailEditText.text.toString()
val phoneType: String = contactPhoneTypes[mContactPhoneTypeSpinner.selectedItemPosition]
val emailType: String = contactEmailTypes[mContactEmailTypeSpinner.selectedItemPosition]
다음 코드는 ContactsContract.RawContacts 테이블에 원시 연락처 행의 삽입 연산을 만듭니다.
/*
* 새로운 원시 연락처와 그에 연결된 데이터를 삽입하기 위한 배치 작업을 준비
* 이 사람에 대한 데이터가 연락처 provider에 전혀 없더라도,
* 직접 연락처를 추가한 게 아니라 원시 연락처를 추가.
* 이후 연락처 provider가 자동으로 연락처를 추가
*/
// ContentProviderOperation 객체 타입의 새 배열 생성
ArrayList<ContentProviderOperation> ops =
new ArrayList<ContentProviderOperation>();
/*
* 계정 유형(서버 유형)과 계정 이름(사용자 계정)으로 새로운 원시 연락처 생성
* 표시 이름은 이 행에 저장되지 않고 StructuredName 데이터 행에 저장
* 다른 데이터는 필요하지 않음
*/
ContentProviderOperation.Builder op =
ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, selectedAccount.getType())
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, selectedAccount.getName());
// 연산 구축 후 연산 배열에 추가
ops.add(op.build());
/*
* 새로운 원시 연락처와 그에 연결된 데이터를 삽입하기 위한 배치 작업을 준비
* 이 사람에 대한 데이터가 연락처 provider에 전혀 없더라도,
* 직접 연락처를 추가한 게 아니라 원시 연락처를 추가.
* 이후 연락처 provider가 자동으로 연락처를 추가
*/
// ContentProviderOperation 객체 타입의 새 배열 생성
val ops = arrayListOf<ContentProviderOperation>()
/*
* 계정 유형(서버 유형)과 계정 이름(사용자 계정)으로 새로운 원시 연락처 생성
* 표시 이름은 이 행에 저장되지 않고 StructuredName 데이터 행에 저장
* 다른 데이터는 필요하지 않음
*/
var op: ContentProviderOperation.Builder =
ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, selectedAccount.name)
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, selectedAccount.type)
// 연산 구축 후 연산 배열에 추가
ops.add(op.build())
그런 다음 코드에 표시 이름, 전화 및 이메일 행에 대한 데이터 행을 생성합니다. 각 연산 빌더 객체는 withValueBackReference()를 사용하여 RAW_CONTACT_ID를 가져옵니다. 원시 연락처 행을 추가하면 새 _ID는 값을 생성하는 첫 번째 연산에서 얻는 ContentProviderResult 객체에서 ID값을 참조합니다. 따라서 각 데이터 행은 RAW_CONTACT_ID에 의해 자신이 속한 새로운 ContactsContract.RawContacts 행에 자동으로 연결됩니다. 이메일 행을 추가하는 ContentProviderOperation.Builder 객체는 withYieldAllowed()를 통해 yield 지점을 설정합니다.
// StructuredName data으로 새로 삽입하는 원시 연락처 행의 표시 이름 설정
op =
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
/*
* withValueBackReference는 두번째 인자로 ContentProviderResult 인덱스 값을 받아
* 해당 값을 첫번째 인자에 부여
* 해당 코드에서 StructuredName 데이터의 원시 연락처 ID 열은
* 원시 연락처 행을 추가하는 첫 번째 연산에서 반환된 결과 값으로 설정.
*/
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
// data열의 MIME 타입에 StructuredName으로 설정
.withValue(ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
//data 열의 표시 이름에 UI에 있는 이름으로 설정
.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name);
//연산 구축 후 연산 배열에 추가
ops.add(op.build());
// 특정 휴대폰 번호와 Phone data 행에서의 유형 삽입
op =
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
//배치의 첫번째 연산에서 얻은 새로운 원시 연락처 ID을 원시 연락처 id열에 설정
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
// data행의 MIME유형을 Phone으로 설정
.withValue(ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
//휴대폰 번호와 유형 설정
.withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phone)
.withValue(ContactsContract.CommonDataKinds.Phone.TYPE, phoneType);
//연산 구축 후 연산 배열에 추가
ops.add(op.build());
// 특정 이메일과 Phone data 행에서의 유형 삽입
op =
ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
//배치의 첫번째 연산에서 얻은 새로운 원시 연락처 ID을 원시 연락처 id열에 설정
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
// data행의 MIME유형을 Email으로 설정
.withValue(ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
//이메일 주소와 유형 설정
.withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, email)
.withValue(ContactsContract.CommonDataKinds.Email.TYPE, emailType);
/*
* Yield 지점 설정. 해당 삽입이 끝나면
* 배치 작업의 스레드가 다른 스레드보다 우선순위를 갖게 됨.
* 하나의 연락처에 영향을 주는 일련의 작업 집합을 처리한 뒤에는
* 항상 이 메소드를 사용하여 성능 저하 방지.
*/
op.withYieldAllowed(true);
//연산 구축 후 연산 배열에 추가
ops.add(op.build());
마지막 코드는 새로운 원시 연락처와 데이터 행을 삽입하는 applyBatch() 호출을 보여줍니다.
//java
// 연락처 provider에게 새로운 연락처 생성 요청
Log.d(TAG,"Selected account: " + selectedAccount.getName() + " (" +
selectedAccount.getType() + ")");
Log.d(TAG,"Creating contact: " + name);
/*
* 배치에서 ContentProviderOperation 객체 타입의 배열 적용.
* 해당 결과 값 사용X
*/
try {
getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
} catch (Exception e) {
// 경고 표시
Context ctx = getApplicationContext();
CharSequence txt = getString(R.string.contactCreationFailure);
int duration = Toast.LENGTH_SHORT;
Toast toast = Toast.makeText(ctx, txt, duration);
toast.show();
// 예외 로그출력
Log.e(TAG, "Exception encountered while inserting contact: " + e);
}
}
//kotlin
// 연락처 provider에게 새로운 연락처 생성 요청
Log.d(TAG, "Selected account: ${mSelectedAccount.name} (${mSelectedAccount.type})")
Log.d(TAG, "Creating contact: $name")
/*
* 배치에서 ContentProviderOperation 객체 타입의 배열 적용.
* 해당 결과 값 사용X
*/
try {
contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
} catch (e: Exception) {
// 경고 표시
val txt: String = getString(R.string.contactCreationFailure)
Toast.makeText(applicationContext, txt, Toast.LENGTH_SHORT).show()
// 예외 로그출력
Log.e(TAG, "Exception encountered while inserting contact: $e")
}
}
배치 연산을 사용하면 기본 저장소를 lock 않은 채 수정 트랜잭션을 적용하는 방법인 낙관적 동시 실행 제어를 구현할 수 있습니다. 한 사용자가 데이터를 읽을 때 lock을 걸어 동시 수정을 막는 비관적 동시성 제어와 달리 낙관적 동시성 제어는 lock 설정을 하지 않아 데이터를 수정할 때 다른 사용자에 의해 변경되었는지 검사하는 방식입니다. 그렇기 때문에 해당 기능을 사용하려면 트랜잭션을 적용한 다음 동시 적용에 의한 다른 수정이 있는지 확인해야 합니다. 일관되지 않은 수정이 발견하면 트랜잭션을 롤백하고 다시 시도합니다.
낙관적 동시 실행 제어는 한 번에 한 명의 사용자만 존재하고 데이터 저장소에 대한 동시 접근이 거의 없는 휴대기기에 유용합니다. 잠금을 사용하지 않으므로 잠금을 설정하거나 다른 트랜잭션이 잠금을 해제하기를 기다리면서 시간을 낭비하지 않아도 됩니다.
단일 ContactsContract.RawContacts 행을 업데이트하는 동안 낙관적 동시 실행 제어를 사용하려면 다음 단계를 따라야 합니다.
VERSION 열을 검색newAssertQuery(Uri) 메서드를 사용하여 제약 조건을 적용하는 데 적합한 ContentProviderOperation.Builder 객체 생성. 콘텐츠 URI는 RawContacts.CONTENT_UR에 원시 연락처의 _ID를 추가하여 사용.ContentProviderOperation.Builder 객체에 withValue()를 호출하여 VERSION 열과 방금 검색한 버전 번호를 비교newAssertQuery()로 생성한 ContentProviderOperation.Builder에 withExpectedCount()를 호출하여 이 assertion에서 쿼리의 결과로 하나의 행만 반환하는 걸 검사. 잘못된 쿼리나 상황으로 여러 개의 열이 반환되면 동기화 충돌 위험이 있기에 해당 조건을 걸어 OperationApplicationException 발생시킴.build()를 호출하여 ContentProviderOperation 객체를 만든 다음 이 객체를 applyBatch()에 전달하는 ArrayList의 첫 번째 객체로 추가행을 읽은 시점과 행을 수정하려고 시도하는 시점 사이에 원시 연락처 행을 업데이트하는 다른 연산이 발생하면, assert ContentProviderOperation가 실패하고 전체 배치 작업이 취소됩니다. 그러면 배치 작업을 다시 시도하거나 다른 조치를 취할 수 있습니다.
다음 코드에서는 CursorLoader를 사용하여 단일 원시 연락처를 쿼리한 후 assertion ContentProviderOperation를 만드는 방법을 보여줍니다.
//java
/*
* 애플리케이션에서는 원시 연락처 테이블에 쿼리하기 위해 CursorLoader 사용
* 시스템에서 해당 메서드를 load가 끝날 때 호출
*/
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
// 원시 연락처의 _ID와 VERSION 값 얻기
rawContactID = cursor.getLong(cursor.getColumnIndex(BaseColumns._ID));
mVersion = cursor.getInt(cursor.getColumnIndex(SyncColumns.VERSION));
}
...
// assert 연산을위한 Uri 설정
Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactID);
// assert 연산 빌더 생성
ContentProviderOperation.Builder assertOp = ContentProviderOperation.newAssertQuery(rawContactUri);
// assertions에 assert 연산 추가. 테스트할 열의 개수와 version과 검사
assertOp.withValue(SyncColumns.VERSION, mVersion);
assertOp.withExpectedCount(1);
//ContentProviderOperation 객체를 담을 ArrayList 생성
ArrayList ops = new ArrayList<ContentProviderOperation>;
ops.add(assertOp.build());
//ops에 원하는 배치 연산 추가 가능
...
// 배치 적용. assert가 실패하면 예외 발생
try
{
ContentProviderResult[] results =
getContentResolver().applyBatch(AUTHORITY, ops);
} catch (OperationApplicationException e) {
//assert 연산이 실패할 때 원하는 동작 작동
}
//kotlin
/*
* 애플리케이션에서는 원시 연락처 테이블에 쿼리하기 위해 CursorLoader 사용
* 시스템에서 해당 메서드를 load가 끝날 때 호출
*/
override fun onLoadFinished(loader: Loader<Cursor>, cursor: Cursor) {
// 원시 연락처의 _ID와 VERSION 값 얻기
rawContactID = cursor.getLong(cursor.getColumnIndex(BaseColumns._ID))
mVersion = cursor.getInt(cursor.getColumnIndex(SyncColumns.VERSION))
}
...
// assert 연산을위한 Uri 설정
val rawContactUri: Uri = ContentUris.withAppendedId(
ContactsContract.RawContacts.CONTENT_URI,
rawContactID
)
// assert 연산 빌더 생성
val assertOp: ContentProviderOperation.Builder =
ContentProviderOperation.newAssertQuery(rawContactUri).apply {
//assertions에 assert 연산 추가. 테스트할 열의 개수와 version과 검사
withValue(SyncColumns.VERSION, mVersion)
withExpectedCount(1)
}
//ContentProviderOperation 객체를 담을 ArrayList 생성
val ops = arrayListOf<ContentProviderOperation>()
ops.add(assertOp.build())
//ops에 원하는 배치 연산 추가 가능
...
// 배치 적용. assert가 실패하면 예외 발생
try {
val results: Array<ContentProviderResult> = contentResolver.applyBatch(AUTHORITY, ops)
} catch (e: OperationApplicationException) {
//assert 연산이 실패할 때 원하는 동작 작동
}
기기에 있는 연락처 애플리케이션에 인텐트를 전송하면 연락처 provider에 간접적으로 접근할 수 있습니다. 이 인텐트는 사용자가 연락처 관련 작업을 할 수 있는 기기에 있는 연락처 애플리케이션 UI를 시작합니다. 사용자가 인텐트로 접근할 때 할 수 있는 일은 다음과 같습니다.
사용자가 현재 입력, 수정 중인 값을 최종적으로 적용되기 전에 인텐트의 일부로 전송할 수 있습니다. 인텐트를 통해 기기의 연락처 애플리케이션으로 연락처 provider에 접근하면, provider에 접근하기 위한 고유한 UI나 코드를 작성할 필요가 없습니다.
또한 provider에 대한 읽기 또는 쓰기 권한을 요청할 필요도 없습니다. 기기의 연락처 애플리케이션에서 연락처에 관한 읽기 권한을 위임할 수 있고 다른 애플리케이션을 통해 provider를 수정하므로 쓰기 권한도 필요하지 않기 때문입니다.
Provider에 접근하기 위해 인텐트를 전송하는 기본 과정은 다음을 참고하세요.
사용할 수 있는 action, MIME 유형 및 데이터 값은 표에 요약되어 있습니다.

인텐트를 사용하여 원시 연락처 또는 그 데이터를 삭제할 수 없습니다. 대신 원시 연락처를 삭제하려면 ContentResolver.delete() 또는 ContentProviderOperation.newDelete()를 사용해야 합니다.
putExtra()와 함께 사용할 수 있는 추가 값은 ContactsContract.Intents.Insert 문서를 참고하세요.
https://developer.android.com/reference/android/provider/ContactsContract.Intents.Insert
다음 코드는 새로운 원시 연락처와 데이터를 삽입하는 인텐트를 구성하고 전송하는 방법을 보여줍니다.
//java
// UI에서 값 얻기
String name = contactNameEditText.getText().toString();
String phone = contactPhoneEditText.getText().toString();
String email = contactEmailEditText.getText().toString();
String company = companyName.getText().toString();
String jobtitle = jobTitle.getText().toString();
// 기기의 연락처 앱에 보낼 새로운 인텐트 생성
Intent insertIntent = new Intent(ContactsContract.Intents.Insert.ACTION);
// activity에서 삽입할 거 같은 MIME타입 설정
insertIntent.setType(ContactsContract.RawContacts.CONTENT_TYPE);
//삽입할 연락처 이름 설정
insertIntent.putExtra(ContactsContract.Intents.Insert.NAME, name);
//삽입할 회사와 직업 이름 설정
insertIntent.putExtra(ContactsContract.Intents.Insert.COMPANY, company);
insertIntent.putExtra(ContactsContract.Intents.Insert.JOB_TITLE, jobtitle);
/*
* DATA 키와 연결된 ArrayList 형태로 데이터 행들을 추가하는 방법
*/
// 각 열에 ContentValues 객체를 포함하는 array list 정의
ArrayList<ContentValues> contactData = new ArrayList<ContentValues>();
/*
* 원리 연락처 열 정의
*/
// ContentValues 객체 타입의 열 설정
ContentValues rawContactRow = new ContentValues();
// 열에 계정 타입과 이름 추가
rawContactRow.put(ContactsContract.RawContacts.ACCOUNT_TYPE, selectedAccount.getType());
rawContactRow.put(ContactsContract.RawContacts.ACCOUNT_NAME, selectedAccount.getName());
// array에 열 추가
contactData.add(rawContactRow);
/*
* 휴대폰 번호 데이터 열 설정
*/
//ContentValues 객체 타입의 열 설정
ContentValues phoneRow = new ContentValues();
//해당 데이터 행에 대한 MIME 유형을 지정(모든 데이터 행은 유형별로 표시해야 함)
phoneRow.put(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE
);
//행에 전화번호와 전화번호의 유형 추가
phoneRow.put(ContactsContract.CommonDataKinds.Phone.NUMBER, phone);
// array에 열 추가
contactData.add(phoneRow);
/*
* 이메일 데이터 열 설정
*/
//ContentValues 객체 타입의 열 설정
ContentValues emailRow = new ContentValues();
//해당 데이터 행에 대한 MIME 유형을 지정(모든 데이터 행은 유형별로 표시해야 함)
emailRow.put(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE
);
//행에 이메일과 이메일의 유형 추가
emailRow.put(ContactsContract.CommonDataKinds.Email.ADDRESS, email);
// array에 열 추가
contactData.add(emailRow);
/*
* array에 인텐트 extra추가. 프로세스 간 전달에서는 parcelable 객체 사용 필수
* 기기의 연락처 앱에서의 키는 Intents.Insert.DATA
*/
insertIntent.putParcelableArrayListExtra(ContactsContract.Intents.Insert.DATA, contactData);
// 연락처를 추가하는 activity에서 인텐트를 전송할 연락처 앱 실행
startActivity(insertIntent);
//koltin
// UI에서 값 얻기
val name = contactNameEditText.text.toString()
val phone = contactPhoneEditText.text.toString()
val email = contactEmailEditText.text.toString()
val company = companyName.text.toString()
val jobtitle = jobTitle.text.toString()
/*
* DATA 키와 연결된 ArrayList 형태로 데이터 행들을 추가하는 방법
*/
// 각 열에 ContentValues 객체를 포함하는 array list 정의
val contactData = arrayListOf<ContentValues>()
/*
* 원시 연락처 열 정의
*/
// ContentValues 객체 타입의 열 설정
val rawContactRow = ContentValues().apply {
// 열에 계정 타입과 이름 추가
put(ContactsContract.RawContacts.ACCOUNT_TYPE, selectedAccount.type)
put(ContactsContract.RawContacts.ACCOUNT_NAME, selectedAccount.name)
}
// array에 열 추가
contactData.add(rawContactRow)
/*
* 휴대폰 번호 데이터 열 설정
*/
//ContentValues 객체 타입의 열 설정
val phoneRow = ContentValues().apply {
//해당 데이터 행에 대한 MIME 유형을 지정(모든 데이터 행은 유형별로 표시해야 함) put(ContactsContract.Data.MIMETYPE,ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
//행에 전화번호와 전화번호의 유형 추가
put(ContactsContract.CommonDataKinds.Phone.NUMBER, phone)
}
// array에 열 추가
contactData.add(phoneRow)
/*
* 이메일 데이터 열 설정
*/
//ContentValues 객체 타입의 열 설정
val emailRow = ContentValues().apply {
//해당 데이터 행에 대한 MIME 유형을 지정(모든 데이터 행은 유형별로 표시해야 함)
put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
//행에 이메일과 이메일의 유형 추가
put(ContactsContract.CommonDataKinds.Email.ADDRESS, email)
}
// array에 열 추가
contactData.add(emailRow)
// 기기의 연락처 앱에 보낼 새로운 인텐트 생성
val insertIntent = Intent(ContactsContract.Intents.Insert.ACTION).apply {
// activity에서 삽입할 거 같은 MIME타입 설정
type = ContactsContract.RawContacts.CONTENT_TYPE
//삽입할 연락처 이름 설정
putExtra(ContactsContract.Intents.Insert.NAME, name)
//삽입할 회사와 직업 이름 설정
putExtra(ContactsContract.Intents.Insert.COMPANY, company)
putExtra(ContactsContract.Intents.Insert.JOB_TITLE, jobtitle)
/*
* array에 인텐트 extra추가. 프로세스 간 전달에서는 parcelable 객체 사용 필수
* 기기의 연락처 앱에서의 키는 Intents.Insert.DATA
*/
putParcelableArrayListExtra(ContactsContract.Intents.Insert.DATA, contactData)
}
// 연락처를 추가하는 activity에서 인텐트를 전송할 연락처 앱 실행
startActivity(insertIntent)
연락처 저장소에는 사용자가 정확하고 최신일 거라고 예상하는 중요하고 민감한 데이터가 포함되어 있으므로, 연락처 provider에는 데이터 무결성에 관한 명확한 정의된 규칙이 있습니다. 개발자는 연락처 데이터를 수정할 때 이러한 규칙을 준수해야 합니다. 중요한 규칙은 다음과 같습니다.
ContactsContract.RawContacts 행에 항상 ContactsContract.CommonDataKinds.StructuredName 행을 추가ContactsContract.Data 테이블에 ContactsContract.CommonDataKinds.StructuredName 행이 없는 ContactsContract.RawContacts 행은 집계 중에 문제를 일으킬 수 있음ContactsContract.Data 행을 상위 ContactsContract.RawContacts 행에 연결 ContactsContract.RawContacts에 연결되지 않은 ContactsContract.Data 행은 기기의 연락처 애플리케이션에 표시되지 않으며 동기화 어댑터에 문제를 일으킬 수 있음ContactsContract 및 서브클래스에 정의된 상수를 사용Deprecated 상수가 있는 경우 컴파일러 경고가 전송사용자 정의 MIME 타입을 생성하고 사용하면, ContactsContract.Data 테이블에 자신만의 데이터 행을 삽입·수정·삭제·조회할 수 있습니다. Generic 열 이름에 대해 자체적으로 정의한 타입별 열 이름을 매핑할 수 있지만 사용자 정의 행들은 ContactsContract.DataColumns에 정의된 컬럼만 사용할 수 있도록 제한됩니다. 기기 기본 연락처 앱에서는 이 데이터가 표시되지만, 수정·삭제할 수 없으며 사용자가 추가 데이터를 넣을 수도 없습니다. 사용자가 맞춤 데이터 행을 수정할 수 있게 하려면, 개발자가 자신의 앱 안에서 편집 activity를 직접 제공해야 합니다.
사용자 정의 데이터를 표시하려면 <ContactsAccountType> 요소와 하나 이상의 <ContactsDataKind> 하위 요소가 포함된 contacts.xml 파일을 생성해야 합니다.
맞춤 MIME 유형에 관해 자세히 보려면 다음을 참고하세요
연락처 provider는 기기와 온라인 서비스 간에 연락처 데이터의 동기화를 처리하도록 특별히 설계되었습니다. 이렇게 하면 사용자가 기존 데이터를 새 기기에 다운로드하거나 기존 데이터를 새 계정에 업로드할 수 있습니다.
또한 동기화를 사용하면 추가나 변경의 출처와 관계없이 사용자가 최신 데이터를 사용할 수 있습니다. 동기화의 또 다른 장점은 기기가 네트워크에 연결되어 있지 않아도 연락처 데이터를 사용할 수 있다는 겁니다.
다양한 방식으로 동기화를 구현할 수 있지만 안드로이드 시스템에서는 다음 작업을 자동화하는 플러그인 동기화 프레임워크를 제공합니다.
이 프레임워크를 사용하려면 개발자가 동기화 어댑터 플러그인을 직접 제공해야 합니다. 동기화 어댑터는 서비스와 콘텐츠 provider마다 다르지만 동일한 서비스에 여러 계정 이름을 처리할 수 있습니다. 또한 프레임워크는 동일한 서비스와 provider에 여러 개의 동기화 어댑터를 허용합니다.
동기화 어댑터를 AbstractThreadedSyncAdapter의 서브클래스로 구현하여 안드로이드애플리케이션의 일부로 설치합니다. 시스템은 애플리케이션 manifest의 요소와 manifest가 가리키는 특수 XML 파일에서 동기화 어댑터에 관한 정보를 얻습니다. XML 파일은 온라인 서비스의 계정 유형과 콘텐츠 provider의 권한을 정의하며, 이 권한은 어댑터를 고유하게 식별합니다.
동기화 어댑터는 사용자가 동기화 어댑터의 계정 유형에 관한 값을 추가하고 동기화 어댑터가 동기화되는 콘텐츠 provider의 동기화 사용 설정하기 전까지 활성화되지 않습니다. 그 시점부터 시스템이 어댑터를 관리하기 시작하며, 필요할 때마다 호출하여 콘텐츠 provider와 서버 간 동기화를 수행합니다.
계정 유형을 동기화 어댑터의 식별 정보의 일부로 사용하면, 시스템은 동일한 조직에서 제공하는 서로 다른 서비스를 접근하는 동기화 어댑터들을 감지하고 하나로 묶을 수 있습니다. 예를 들어, Google 온라인 서비스를 위한 동기화 어댑터들은 모두 동일한 계정 유형 com.google을 가집니다. 사용자가 기기에 Google 계정을 추가하면, 설치된 모든 Google 서비스용 동기화 어댑터가 함께 표시되며, 각각의 동기화 어댑터는 기기 내 서로 다른 콘텐츠 provider와 동기화됩니다.
대부분의 서비스에서는 사용자가 데이터에 접근하기 전에 ID를 확인해야 하므로 안드로이드 시스템은 동기화 어댑터 프레임워크와 비슷하고 자주 사용되는 인증 프레임워크를 제공합니다. 인증 프레임워크는 AbstractAccountAuthenticator의 서브클래스인 플러그인 authenticator를 사용합니다. Authenticator는 다음 단계에 따라 사용자의 ID를 확인합니다.
서비스가 사용자 인증 정보를 수락하면 authenticator는 이후에 사용할 수 있도록 해당 사용자 인증 정보를 저장할 수 있습니다. 플러그인 authenticator는 프레임워크로 인해 AccountManager는 OAuth2 인증 토큰과 같이 authenticator가 지원하고 외부에서 사용할 수 있도록 선택한 모든 인증 토큰에 접근할 수 있습니다. 인증이 필수는 아니지만, 대부분 연락처 서비스는 이를 사용합니다. 하지만 인증을 수행하기 위해 안드로이드 인증 프레임워크를 꼭 사용할 필요는 없습니다.
연락처 provider의 동기화 어댑터를 구현하려면 안드로이드 애플리케이션에 다음을 포함해야 합니다.
시스템의 요청에 응답하여 동기화 어댑터에 결합하는 Service 구성요소: 시스템에서 동기화를 실행하려는 경우 service의 onBind() 메서드를 호출하여 동기화 어댑터의 IBinder를 가져옴. IBinder는 시스템이 프로세스 간 통신을 통해 어댑터의 메서드를 사용 가능하게 함.
AbstractThreadedSyncAdapter의 서브클래스로 구현된 실제 동기화 어댑터: 이 클래스는 서버에서 데이터 다운로드, 기기에서 데이터 업로드, 충돌을 해결하는 작업 실행. 어댑터의 기본 작업은 onPerformSync() 메서드에서 실행. 이 클래스는 반드시 싱글톤으로 인스턴스화해야 함.
Application의 서브클래스: 이 클래스는 동기화 어댑터 싱글톤의 팩토리 역할. onCreate() 메서드를 사용하여 동기화 어댑터를 인스턴스화하고 동기화 어댑터 서비스의 onBind() 메서드에 싱글톤을 반환하는 정적 getter 메서드를 제공
(선택사항) 시스템의 사용자 인증 요청에 응답하는 Service 구성요소: AccountManager가 이 service를 통해 인증 프로세스 시작. Service의 onCreate() 메서드는 authenticator 객체를 인스턴스화. 시스템이 애플리케이션 동기화 어댑터의 사용자 계정을 인증하려는 경우 시스템은 srvice의 onBind() 메서드를 호출하여 인증자의 IBinder를 가져옴. IBinder는 시스템이 프로세스 간 통신을 통해 authenticator의 메서드를 사용 가능하게 함.
(선택사항) 인증 요청을 처리하는 AbstractAccountAuthenticator의 구현 서브클래스: 이 클래스는 AccountManager가 서버에서 사용자 인증 정보를 인증하기 위해 호출하는 메서드 제공. 인증 프로세스의 세부 사항은 사용하는 서버 기술에 따라 매우 달라짐.
동기화 어댑터와 시스템 authenticator를 정의하는 XML 파일: 앞에서 설명한 동기화 어댑터와 authenticator 서비스 구성요소는 애플리케이션 manifest의 <service> 요소에서 정의. 이러한 요소는 시스템에 특정 데이터를 제공하는<meta-data> 하위 요소를 포함.
<meta-data> 요소는 XML 파일 res/xml/syncadapter.xml를 가리킴. 이 파일은 연락처 provider와 동기화될 웹 서비스의 URI와 웹 서비스의 계정 유형을 지정.authenticator의 <meta-data> 요소는 XML 파일 res/xml/authenticator.xml를 가리킴. 이 파일은 authenticator가 지원하는 계정 유형과 인증 프로세스 중에 표시되는 UI 리소스(예. 화면 레이아웃)를 지정. 이 요소에 지정된 계정 유형은 동기화 어댑터에 지정된 계정 유형과 동일해야 함.android.provider.ContactsContract.StreamItems 및 android.provider.ContactsContract.StreamItemPhotos 테이블은 소셜 네트워크에서 수신하는 데이터를 관리합니다. 자신의 네트워크에서 오는 스트림 데이터를 이 테이블에 추가하는 동기화 어댑터를 작성하거나, 이 테이블에서 스트림 데이터를 읽어 자신의 애플리케이션에 표시 혹은 둘 다할 수 있습니다. 이러한 기능을 통해 소셜 네트워킹 서비스와 애플리케이션을 안드로이드의 소셜 네트워킹 경험에 통합할 수 있습니다. 즉, SNS 활동 메시지나 사진 등을 연락처에 연결하려는 용도로 연락처에서 전화번호, 이메일, SNS 상태 모두 볼 수 있는 기능입니다. 물론 이 기능은 현재 사실상 폐기가 되었기에 문서에는 남겨져 있지만 더 이상 동작하지 않거나, 빈 테이블로 존재만 되어 있습니다. 그래도 어떤 기능인지 궁금하니까 한번 살펴보겠습니다.
스트림 항목은 항상 원시 연락처와 연관됩니다. android.provider.ContactsContract.StreamItemsColumns#RAW_CONTACT_ID는 원시 연락처의 _ID 값에 연결됩니다. 원시 연락처의 계정 유형과 계정 이름도 스트림 항목 행에 저장됩니다.
스트림에서 가져온 데이터는 다음 열에 저장합니다.
android.provider.ContactsContract.StreamItemsColumns#ACCOUNT_TYPE
android.provider.ContactsContract.StreamItemsColumns#ACCOUNT_NAME
식별자 열
android.provider.ContactsContract.StreamItemsColumns#CONTACT_ ID: 이 스트림 항목과 연결된 연락처의 android.provider.BaseColumns#_ID 값android.provider.ContactsContract.StreamItemsColumns#CONTACT_LOOKUP_KEY: 이 스트림 항목과 연결된 연락처의 android.provider.ContactsContract.ContactsColumns#LOOKUP_KEY 값android.provider.ContactsContract.StreamItemsColumns#RAW_ CONTACT_ID: 이 스트림 항목과 연결된 원시 연락처의 android.provider.BaseColumns#_ID 값android.provider.ContactsContract.StreamItemsColumns#COMMENTS
android.provider.ContactsContract.StreamItemsColumns#TEXT
fromHtml()로 렌더링할 수 있는 포맷팅이나 포함된 리소스 이미지를 담음. android.provider.ContactsContract.StreamItemsColumns#TIMESTAMP
스트림 항목의 식별 정보를 표시하려면 android.provider.ContactsContract.StreamItemsColumns#RES_ICON, android.provider.ContactsContract.StreamItemsColumns#RES_LABEL 및 android.provider.ContactsContract.StreamItemsColumns#RES_PACKAGE를 사용하여 애플리케이션의 리소스에 연결합니다.
android.provider.ContactsContract.StreamItems 테이블에는 동기화 어댑터만 사용하는 android.provider.ContactsContract.StreamItemsColumns#SYNC1부터 android.provider.ContactsContract.StreamItemsColumns#SYNC4까지의 열도 포함되어 있습니다.
android.provider.ContactsContract.StreamItemPhotos 테이블은 스트림 항목과 연결된 사진을 저장합니다. 테이블의 android.provider.ContactsContract.StreamItemPhotosColumns#STREAM_ITEM_ ID 열은 android.provider.ContactsContract.StreamItems 테이블의 _ID 열에 있는 값에 연결됩니다. 사진 참조는 다음 열의 테이블에 저장됩니다.
android.provider.ContactsContract.StreamItemPhotos#PHOTO열(BLOB)android.provider.ContactsContract.StreamItemPhotosColumns#PHOTO_FILE_ID 또는 android.provider.ContactsContract.StreamItemPhotosColumns#PHOTO_URI를 사용하여 사진을 파일로 저장. android.provider.ContactsContract.StreamItemPhotosColumns#PHOTO_ FILE_IDDisplayPhoto.CONTENT_URI에 추가하여 단일 사진 파일을 가리키는 콘텐츠 URI를 가져온 다음 openAssetFileDescriptor()를 호출하여 사진 파일의 핸들을 가져옴android.provider.ContactsContract.StreamItemPhotosColumns#PHOTO_URIopenAssetFileDescriptor()를 호출하여 사진 파일의 핸들을 가져옴.소셜 스트림 테이블은 연락처 provider의 다른 주요 테이블과 똑같이 작동하지만, 다음 예외가 적용됩니다.
이 테이블에는 추가 접근 권한이 필요. 여기서 읽기 작업을 수행하려면 애플리케이션에 android.Manifest.permission#READ_SOCIAL_STREAM 권한이 필요. 이를 수정하려면 애플리케이션에 android.Manifest.permission#WRITE_SOCIAL_STREAM 권한이 필요
android.provider.ContactsContract.StreamItems 테이블의 경우 각 원시 연락처에 저장되는 행 수가 제한. 이 제한에 도달하면 연락처 provider가 가장 오래된 android.provider.ContactsContract.StreamItemsColumns#TIMESTAMP가 있는 행을 자동으로 삭제하여 새 스트림 항목 행을 위한 공간을 만듦. 한도 값을 가져오려면 콘텐츠 URI android.provider.ContactsContract.StreamItems#CONTENT_LIMIT_URI에 쿼리를 실행. 콘텐츠 URI를 제외한 모든 인수는 null로 설정된 상태로 두어도 됨. 이 쿼리는 단일 열 android.provider.ContactsContract.StreamItems#MAX_ITEMS를 포함한 단일 행이 포함된 커서 반환.
android.provider.ContactsContract.StreamItems.StreamItemPhotos 클래스는 단일 스트림 항목의 사진 행을 포함하는 android.provider.ContactsContract.StreamItemPhotos 하위 테이블을 정의합니다.
연락처 provider가 관리하는 소셜 스트림 데이터는 기기의 연락처 애플리케이션과 함께 사용될 때, 소셜 네트워킹 시스템을 기존 provider와 연결할 수 있는 유용한 방법을 제공합니다. 사용할 수 있는 기능은 다음과 같습니다.
android.provider.ContactsContract.StreamItems 및 android.provider.ContactsContract.StreamItemPhotos 테이블에 저장하여 나중에 사용 가능activity와 애플리케이션의 세부 정보를 기기의 연락처 애플리케이션 및 연락처 provider에 제공하는 XML 파일을 조합하여 구현.동기화 어댑터를 등록하여 사용자가 동기화 어댑터에서 관리하는 연락처를 볼 때 콜백을 수신하려면 다음 단계를 따라야 합니다.
res/xml/ 디렉터리에 contacts.xml 파일 생성. 이미 해당 파일이 있다면 해당 절차 생략 가능<ContactsAccountType xmlns:android="http://schemas.android.com/apk/res/android"> 요소를 추가. 해당 요소가 이미 존재한다면 해당 절차 생략 가능service를 등록하려면 viewContactNotifyService="serviceclass" 속성을 요소에 추가. 여기서 serviceclass는 기기의 연락처 애플리케이션에서 인텐트를 받을 service의 전체 클래스 이름. 콜백을 받는 service의 경우 인텐트를 받을 수 있도록 IntentService를 상속한 클래스 사용 필요. 수신되는 인텐트의 데이터에는 사용자가 클릭한 원시 연락처의 콘텐츠 URI가 포함. 콜백 받는 service에서 동기화 어댑터에 바인딩한 후 동기화 어댑터를 호출하여 원시 연락처의 데이터 업데이트 가능사용자가 스트림 항목이나 사진, 또는 그 두 가지를 모두 클릭할 때 호출할 activity를 등록하는 방법은 다음과 같습니다
디렉터리에contacts.xml``` 파일 생성. 이미 해당 파일이 있다면 해당 절차 생략 가능<ContactsAccountType xmlns:android="http://schemas.android.com/apk/res/android">요소 추가. 해당 요소가 이미 존재한다면 해당 절차 생략 가능activity를 등록하려면 viewStreamItemActivity="activityclass" 속성 요소에 추가. 여기서 activityclass은 기기의 연락처 애플리케이션에서 인텐트를 받을 activity의 전체 클래스 이름 수신되는 인텐트에는 사용자가 클릭한 항목 또는 사진의 콘텐츠 URI가 들어 있습니다. 텍스트 항목과 사진에 각기 별도의 activity를 적용하려면, 두 속성을 모두 같은 파일에서 명시해야 합니다.
사용자는 소셜 네트워킹 사이트에 연락처를 초대하기 위해 기기의 연락처 애플리케이션에서 나가지 않아도 됩니다. 대신 기기의 연락처 앱이 activity에 연락처를 초대하는 인텐트를 보내도록 할 수 있습니다. 설정 방법은 다음과 같습니다
res/xml/ 디렉터리에 contacts.xml 파일 생성. 이미 해당 파일이 있다면 해당 절차 생략 가능<ContactsAccountType xmlns:android="http://schemas.android.com/apk/res/android"> 요소 추가. 해당 요소가 이미 존재한다면 해당 절차 생략 가능inviteContactActivity="activityclass",inviteContactActionLabel="@string/invite_action_label" 속성 추가. activityclass 값은 인텐트를 받는 activity의 전체 클래스 이름. invite_action_label 값은 기기의 연락처 애플리케이션의 연결 추가 메뉴에 표시되는 텍스트 문자열. ContactsSource는 ContactsAccountType에서 deprecated 된 태그 이름(사용 비추천).contacts.xml 파일에는 동기화 어댑터 및 애플리케이션과 연락처 애플리케이션 및 연락처 provider의 상호작용을 제어하는 XML 요소가 포함되어 있습니다.
<ContactsAccountType> 요소<ContactsAccountType
xmlns:android="http://schemas.android.com/apk/res/android"
inviteContactActivity="activity_name"
inviteContactActionLabel="invite_command_text"
viewContactNotifyService="view_notify_service"
viewGroupActivity="group_view_activity"
viewGroupActionLabel="group_action_text"
viewStreamItemActivity="viewstream_activity_name"
viewStreamItemPhotoActivity="viewphotostream_activity_name">
<ContactsAccountType> 요소는 애플리케이션과 연락처 애플리케이션의 상호작용을 제어res/xml/contacts.xml<ContactsDataKind><ContactsAccountType>의 속성에는 속성 접두사 android:가 필요하지 않음inviteContactActivityactivity의 전체 클래스 이름inviteContactActionLabelinviteContactActivity에 지정된 activity에 관해 표시되는 텍스트 문자열. 예를 들어 'Follow in my network'라는 문자열을 사용. viewContactNotifyServiceservice의 전체 클래스 이름. viewGroupActivityactivity의 전체 클래스 이름activity의 UI가 표시viewGroupActionLabelviewStreamItemActivityactivity의 전체 클래스 이름viewStreamItemPhotoActivityactivity의 전체 클래스 이름<ContactsDataKind> 요소<ContactsDataKind
android:mimeType="MIMEtype"
android:icon="icon_resources"
android:summaryColumn="column_name"
android:detailColumn="column_name">
<ContactsDataKind> 요소는 연락처 애플리케이션의 UI에서 애플리케이션의 맞춤 데이터 행 표시를 제어<ContactsAccountType><ContactsAccountType>의 각 <ContactsDataKind> 하위 요소는 동기화 어댑터가 ContactsContract.Data 테이블에 추가하는 특정 타입의 사용자 정의 데이터 행을 나타냄<ContactsDataKind> 요소를 하나씩 추가해야 함. 만약 표시할 필요가 없는 사용자 정의 데이터 행이 있으면 해당 요소를 추가하지 않아도 됨.android:mimeTypeContactsContract.Data 테이블에 있는 사용자 정의 데이터 행 타입에 대해 정의한 사용자 정의 MIME. 예를 들어, 값이 vnd.android.cursor.item/vnd.example.locationstatus라면 이는 연락처의 마지막으로 알려진 위치를 기록하는 데이터 행을 위한 사용자 정의 MIME 타입.android:iconandroid:summaryColumnandroid:detailColumn연락처 provider는 선택적으로 관련된 연락처 집합을 그룹 데이터로 라벨을 지정할 수 있습니다. 특정 사용자 계정과 연결된 서버에서 그룹을 유지하려는 경우 해당 계정 계정 유형의 동기화 어댑터는 그룹 데이터를 연락처 provider와 서버 간에 전송해야 합니다. 사용자가 서버에 새 연락처를 추가하고 이 연락처를 새 그룹에 넣으면 동기화 어댑터는 반드시 새 그룹을 ContactsContract.Groups 테이블에 추가해야 합니다. 원시 연락처가 속한 그룹은 ContactsContract.CommonDataKinds.GroupMembership MIME 유형을 사용하여 ContactsContract.Data 테이블에 저장됩니다.
만약 서버에서 연락처 provider로 원시 연락처 데이터를 추가하는 동기화 어댑터를 설계하고 있는데 그룹을 사용하지 않는다면, provider에 데이터가 표시되도록 명시해야 합니다. 그룹 단위로 연락처가 관리되기 때문에, 어떤 그룹에도 속하지 않는 연락처는 UI에 표시되지 않도록 설계되었기 때문입니다. 사용자가 기기에 계정을 추가할 때 실행되는 코드에서 연락처 provider가 계정에 추가하는 ContactsContract.Settings 행을 업데이트해야 합니다. 이 행에서 Settings.UNGROUPED_VISIBLE 열의 값을 1로 설정합니다. 이렇게 하면 연락처 provider는 그룹을 사용하지 않더라도 연락처 데이터를 항상 표시합니다.
ContactsContract.Data 테이블은 사진을 MIME 유형이 Photo.CONTENT_ITEM_TYPE인 행으로 저장합니다. 행의 CONTACT_ID 열은 행이 속한 원시 연락처의 _ID 열과 연결됩니다.
ContactsContract.Contacts.Photo 클래스는 연락처 대표 사진의 사진 정보가 포함된 ContactsContract.Contacts의 하위 테이블을 정의합니다. 연락처에는 여러 계정별 연락처(원시 연락처) 데이터 집합으로 구성되어 여러 장의 사진이 연결될 수 있습니다. 이때 대표 사진은 그 중 대표로 쓰이는 사진입니다. 일반적으로 연락처 목록이나 상세 화면에 표시됩니다. 즉, 연락처 대표 사진이란 연락처 기본 원시 연락처의 기본 사진을 말합니다.
마찬가지로 ContactsContract.RawContacts.DisplayPhoto 클래스는 원시 연락처 대표 사진의 사진 정보가 포함된 ContactsContract.RawContacts의 하위 테이블을 정의합니다.
원시 연락처의 기본 썸네일을 검색하는 편의 클래스는 없지만 ContactsContract.Data 테이블에 쿼리를 전송하여 원시 연락처의 _ID, Photo.CONTENT_ITEM_TYPE, IS_PRIMARY 열을 조건으로 선택하면 원시 연락처의 대표 사진 행을 찾을 수 있습니다. 한 사람의 소셜 스트림 데이터에도 사진이 포함되어 있을 수 있습니다
ContactsContract.Contacts.Photo에서 사진은 두 가지 방식으로 저장될 수 있습니다. 기본으로 photo는 데이터 행에 직접 저장된 썸네일 크기의 이미지이고, display photo는 (존재한다면) 파일로 저장된 더 큰 버전의 사진입니다. 또한 ContactsContract.Contacts.openContactPhotoInputStream(ContentResolver, Uri, boolean) 편의 메서드를 사용하여 썸네일 크기이든 전체 크기이든 원본 사진 데이터를 가져올 수 있습니다. ContactsContract.Contacts.CONTENT_URI나 ContactsContract.Contacts.CONTENT_LOOKUP_URI와 모두 사용할 수 있습니다.
//썸네일 사이즈 사진
public InputStream openPhoto(long contactId) {
Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
Uri photoUri = Uri.withAppendedPath(contactUri, Contacts.Photo.CONTENT_DIRECTORY);
Cursor cursor = getContentResolver().query(photoUri,
new String[] {Contacts.Photo.PHOTO}, null, null, null);
if (cursor == null) {
return null;
}
try {
if (cursor.moveToFirst()) {
byte[] data = cursor.getBlob(0);
if (data != null) {
return new ByteArrayInputStream(data);
}
}
} finally {
cursor.close();
}
return null;
}
//더 큰 사이즈 사진
public InputStream openDisplayPhoto(long contactId) {
Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
Uri displayPhotoUri = Uri.withAppendedPath(contactUri, Contacts.Photo.DISPLAY_PHOTO);
try {
AssetFileDescriptor fd =
getContentResolver().openAssetFileDescriptor(displayPhotoUri, "r");
return fd.createInputStream();
} catch (IOException e) {
return null;
}
}
ContactsContract.RawContacts.DisplayPhoto에 접근하려면, 원시 연락처 URI에 CONTENT_DIRECTORY를 추가하면 됩니다. 결과 URI는 이미지 파일을 나타내며, ContentResolver.openAssetFileDescriptor를 사용하여 다뤄야 합니다. 쓰기 모드로도 사진을 열 수 있습니다. 호출자는 asset 파일을 열고 원시 연락처와 연관된 기본 사진을 생성하거나 교체할 수 있으며, 전체 크기 사진 데이터를 파일에 기록하면 됩니다. 파일을 닫으면, 이미지가 파싱되고 필요 시 전체 크기 표시 사진과 썸네일 크기에 맞게 축소되어 저장됩니다.
public void writeDisplayPhoto(long rawContactId, byte[] photo) {
Uri rawContactPhotoUri = Uri.withAppendedPath(
ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
RawContacts.DisplayPhoto.CONTENT_DIRECTORY);
try {
AssetFileDescriptor fd =
getContentResolver().openAssetFileDescriptor(rawContactPhotoUri, "rw");
OutputStream os = fd.createOutputStream();
os.write(photo);
os.close();
fd.close();
} catch (IOException e) {
// Handle error cases.
}
}
이전 CONTENT PROVIDER 포스트
<contentProvider_1(개념)>
<contentProvider_2(생성 과정)>
<contentProvider_3(예제)>
<contentProvider_4(calendar 개념)>
<contentProvider_5(calendar 예제)>
<contentProvider_6(비동기 방식)>