Android2.0(API level5)부터 멀티어카운트와 다른 데이터소스로부터 다루고 통합하는 개선된 Contacts API를 제공한다. 여러소스들로부터 오버랩되는 데이터를 다루기 위해 contacts
content provider는 유사한 contacts들을 모은다 그리고 사용자에게 하나의 엔티티로 제공한다. 이 문서는 contacts를 다루기 위한 새로운 방법에 대해 기술한다.
새로운 Contacts API는 android.provider.ContactsContract와 이와 관련된 클래스들로 정의된다. 예전 API도 여전히 지원되나 사용이 권장되지는 않는다. 이전 API로 작성된 애플리케이션을 가지고 있다면 Considerations for legacy apps문서를 참조하라.
새로운 API와 예전 API를 모두 사용하는 예제를 참조하고 싶다면 Business Card sample
application 을 참조하라.
Data structure of Contacts
새로운 API에서 data는 3개의 primary table로 나뉜다. Contacts, raw
contacts그리고 data로 구성되며 이는 여러 개의 contacts소스로부터 정보를 다루고 저장하기 좋은 구조로 만들어져 있다.
Data는 raw contact와 연관된 모든 종류의 데이터가 저장된 일반적인 테이블이다. 각 row는 해당종류의 데이터가 저장된다(ex: name,
photo, email address, phone number, group membership). 각 row는 또 데이터의 종류를 파악하기 위한 MIME type으로 tag된다. Column은 그것이 가지고 있는 타입이 각 row에 저장되어있는 데이터의 종류에 따라 결정된다. 예) 특정 row의 data종류가
Phone.CONTENT_ITEM_TYPE이라면, 첫번째 column은 전화번호를 가지게 되고, data종류가 Email.CONTENT_ITEM_TYPE이라면 column에는 email주소가 저장된다.
ContactsContract.CommonDataKinds클랙스는 contacts data에 해당하는 MIME type들에 대응되는 서브클래스들을 제공한다. 필요하다면 data row에 해당하는 새로운 MIME type을 정의하여 사용이 가능하다. 더 많은 정보를 원하면 android.provider.ContactsContract.Data 를 보라.
RawContacts table의 하나의 row는 Data의 집합과 인물에 대한 설명과 하나의 contacts source와 연결된 정보를 표현한다. 예를 들면, 하나의 row는 Google이나 Exchange 계정 또는 facebook 친구와 연관된 정보를 정의한다. 더 많은 정보는 ContactsContract.RawContacts를 살펴보라.
Contacts테이블내의 하나의 row는 동일한 인물(또는 엔티티)을 설명하는 하나이상의 합쳐진 RawContacts를 표현한다.
위에서 언급한대로 Contact content
provider는 자동으로 Raw Contacts를 합쳐서 하나의 Contact 엔트리에 넣는다. 취합로직이 Contacts의 row내의 엔트리들을 다루기 때문에 그 엔트리들은 읽을수 있지만 수정되어선 안된다. 아래 Aggregation of contacts
섹션을 보면 어떻게 취합이 이루어지는지에 대한 더 많은 정보를 얻을 수 있다.
이 것은 통합되어 제공되므로 사용자에게 contacts가 보여질 때 하부의 다양한 소스로부터 contact들의 뷰가 취합되어진다. 이는 Contacts level에서 이루어진다.(When
displaying contacts to users, applications should typically operate on the
Contacts level, since it provides a unified, aggregated view of contacts from
various underlying sources)
Example: Inserting a Phone Number
전화번호를 새로운 API를 사용해서 추가하려면, 전화번호를 추가할 Raw Contact의 ID가 필요할것이다. 그 때 Data row를 생성할 필요가 생긴다 :
import android.provider.ContactsContract.CommonDataKinds.Phone;
...
ContentValues values = new ContentValues();
values.put(Phone.RAW_CONTACT_ID, rawContactId);
values.put(Phone.NUMBER, phoneNumber);
values.put(Phone.TYPE, Phone.TYPE_MOBILE);
Uri uri = getContentResolver().insert(Phone.CONTENT_URI, values);
Aggregation of contacts(contacts의 취합)
사용자가 여러 개의 소스로부터 contacts를 동기화할 때, 일부 contact들은 동일인물이나 엔티티에 해당하는 경우가 있다. 예를 들어 “Bob Parr”는 사용자의 동료이거나 친구일수 있다. 따라서 사용자는 그의 회사 이메일계정과 개인계정정보 두가지를 모두 contact 정보로 갖기를 원할것이다. 사용자에게 단순화된 뷰를 제공하려면 시스템은 겹쳐지는 contact들을 배치하고, 하나로 합치고 취합한다.
시스템은 디폴트로 자동으로 contact들을 취합한다. 하지만 필요한 경우 당신의 애플리케이션은 시스템이 취합하는 방법에 관여하거나 모두함께 취합되지 않도록 할수 있다.
Automatic aggregation
Raw contact가 추가되거나 수정되면, 시스템은 취합되어질 매치되는(겹쳐지는)raw contact들을 찾는다. 하나도 매치되는 raw contact들을 찾지 못할 수도 있다. 이 때는 새로운 raw contact가 생성될것이다. 단 하나가 검색되었다면 두개의 raw contact를 갖는 하나의 Contact가 새로 생성된다. 그리고 여러 개의 raw contact가 검색되었다면 그 중 가장 매치가 잘되는 raw contact를 선택하게 된다.
2개의 raw contact가 아래의 요구 중 적어도 하나이상 일치하면 매치된것으로 간주된다.
- 이름이 같은경우
- 이름이 동일한 단어로 순서만 다른 경우(ex, “Bob Parr”과 “Parr,
Bob”)
- 공통된 short name을 갖는 경우(ex, “Bob Parr”과 “Robert
Parr”)
- First name과
last name중 하나만 갖고 있으며 다른 raw contact와 동일한 경우, 이 룰은 좀 느슨한편이다. 그래서 두개이 raw contact가 폰번호, 이메일주소 또는 애칭을 공유하는 경우에만 적용된다.(ex, Hellen [“elastigirl]”=Helen Parr[“elastigirl”])
- 둘중 하나가 name정보가 모두 없는 상태에서 전화번호, 이메일주소, 또는 애칭을 공유하고 있는 경우(ex, Bob Parr [incredible@android.com] = incredible@android.com)
이름을 비교할 시 시스템은 대소문자와 음성구분마크(diacritical
mark)(Hélène=Helene)는 구분하지 않는다. 전화번호를 구분할 때 “*”, “#”, “(“, “), “ “등 특수문자는 비교하지 않는다. 또한 두개의 번호가 국가코드만 다르고 나머지는 동일하다면 두가지를 동일한 번호로 인식한다(일본 국가코드만 제외됨)
자동 취합은 영속적이지 않으며 하나의 구성하는 raw contact의 변경이 새로운 취합을 생성하기도하고 존재하는 취합을 중단시킬 수 있다.(Automatic
aggregation is not permanent; any change of a constituent raw contact may
create a new aggregate or break up an existing one.)
Explicit aggregation(명시적 취합)
어떤 경우에는 시스템의 자동 취합기능이 당신의 애플리케이션이나 sync adapter의 요구사항에 부합되지 않는경우가 있을 수 있다. 그래서 명시적으로 취합을 제어할 수 있는 두가지 종류의 API set이 있다. Aggregation
mode(취합모드)는 자동취합행위자체를 제어할수 있으며 aggregation
exception은 자동화된 취합자체를 오버라이드할수 있다.
Aggregation
modes(취합모드)
당신을 raw contact의 각각에 대해 aggregation
mode를 설정할 수 있다. 이렇게 하기 위해서 모드상수값을 RawContact의 row에 column으로 추가한다. 모드상수는 아래와 같이 정의되어 있다:
·
AGGREGATION_MODE_DEFAULT — 일반모드, 자동취합기능 동작.
·
AGGREGATION_MODE_DISABLED — 자동취합기능 꺼짐. 이 raw contact는 취합되지 않는다.
·
AGGREGATION_MODE_SUSPENDED — 자동취합기능이 비활성화된다. 취합모드가 suspene로 변경될 때 이미 취합중인 상태였다면, 취합중인 내용은 계속 유지된다.
Aggregation
exceptions(취합 예외)
두개의 raw contact를 무조건 합치거나 떨어뜨려 놓으려면,
ContactsContract.AggregationException(contact2.db에 agg_exceptions테이블명으로 있슴)테이블에 row를 하나 추가할 수 있다. 여기에 추가된 예외는 모든 자동 취합룰에 대해 오버라이드된다.
Loookup URI
새로운 Contacts API는 contact를 검색하기 위해 lookup key의 개념을 소개한다. 당신의 애플리케이션이 contact의 참조를 관리할 필요가 있다면 전통적인 row id대신에 lookup key를 사용해야 한다. 당신은 contact로부터 lookup key를 가져올 수 있다. 이는 ContactsContract.Contacts 테이블에 하나의 칼럼(contact2.db의 경우 “lookup”필드명으로 있슴)으로 있다. Lookup key를 가져오면 아래의 방법으로 UIR를 구성할 수 있다.
Uri lookupUri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey)
그리고 아래와 같이 전통적인 방법처럼 URI를 사용할 수 있다.
Cursor c = getContentResolver().query(lookupUri, new String[]{Contacts.DISPLAY_NAME}, ...);
try {
c.moveToFirst();
String displayName = c.getString(0);
} finally {
c.close();
}
이렇게 복잡해진 이유는 contact row ID는 근본적으로 volatile하기 때문이다. 당신의 애플리케이션이 아주 긴 ID를 가진 contact를 저장했다고 해보자, 그러면 사용자는 수동으로 해당 contact를 다른 contact에 join한다. 이제 두 개로 사용되는 하나의 contact가 있다. 그리고 긴 ID로 저장된 contact는 아무데도 없다. <- 이게 먼소린지???
The reason for this complication is that regular contact
row IDs are inherently volatile. Let's say your app stored a long ID of a
contact. Then the user goes and manually joins the contact with some other
contact. Now there is a single contact where there used to be two, and the
stored long contact ID points nowhere.
Lookup key는 이런 케이스의 해결책을 제시한다. 키값은 raw contacts들의 server-side id값들을 이어붙인 문자열이다. 당신의 애플리케이션에서는 임의 contact를 찾기 위해 이 문자열을 사용할 수 있다. 이는 해당 raw contact의 취합여부에 상관없이 사용이 가능하다.
만일 당신의 애플리케이션은 성능을 중시한다면 lookup과 long ID를 모두 저장할 수 있으며 ID값을 벗어난 lookup URI값을 아래와 같이 만들어낼 수 있다.If performance
is a concern for your application, you might want to store both the lookup and
the long ID of a contact and construct a lookup URI out of both IDs, as shown
here:
Uri lookupUri = getLookupUri(contactId, lookupKey)
URI에 두개의 ID가 모두 표현되어 있다면, 시스템은 long ID를 먼저 사용한다. 그렇게 하면 아주 빠른 쿼리가 가능하다. Contact가 검색되지 않거나 검색된 정보가 잘못된 lookup key를 가지고 있으면, content
provider는 lookup key를 파싱하고 생성된 raw contact들을 추적(?)한다. 만일 애플리케이션이 contacts를 bulk-process 한다면, ID를 모두 다루어야 하고 반대로 사용자 액션당 하나의 contact에 대한 작업을 한다면, long ID를 저장할 필요가 없을 것이다.
안드로이드는 스스로 contact를 참조할 일(shortcut,
Quuick Contact, contact 뷰 또는 수정중인 경우)이 생기면 lookup URI를 사용한다.
왜 contact를 단순히 보고 있는중에 contact ID가 변경 되는것인가? 그것은 백그라운드로 동기화가 진행중이거나 화면에 보여지는 중에 다른 항목과 취합될수 있기 때문이다.
정리하면 contact를 참조할 필요가 생기면 무조건 lookup URI를 사용할것을 추천하는 바이다.
Considerations for legacy applications(이전 애플리케이션에 대한 고려)
이전 API를 사용하는 애플리케이션이 있다면 새로운 API로 업그레이드를 고려해야 한다. 네 가지 방법이 있다:
·
걍 내버려 두고 Contacts의 호환성모드에 기대는 것.
·
업그레이드를 진행하는 것, 그리고 Android2.0 이전 플랫폼에 대한 지원을 중단하는 것.
·
이전 버전을 계속 유지한 상태에서 새 버전을 작성하는 것
·
배포된 platform에 맞는 올바른 API를 사용한 어플을 만드는 것
·
Using compatibility mode
호환성 모드는 가장 쉬운 방법이다. 그리고 public API만을 사용하는 한 안드로이드 2.0에서도 동작해야 한다. Non-public API를 사용하는 예제는 내부 쿼리들에서 명시적인 테이블명을 사용하고 컬럼을 사용한다. 그것은 Contacts class에 public상수로 정의되어 있지 않다.
이런 애플리케이션이 현재도 사용중임에도 불구하고 이렇게 계속 내버려두고 싶지은 않을것이다. 왜냐하면 오직 하나의 계정에만 접근이 가능하기 때문이다. 그것도 처음 지정된 구글계정에만. 만일 사용자가 다른 계정을 열거나 다른 구글계정으로 변경한다면 이런 애플리케이션들은 그 계정에 접근할 수가 없게 된다.
Upgrading to the new API and dropping support for older platforms
애플리케이션이 Android 2.0보다 오래된 플랫폼을 더 이상 지원하지 않는다면 아래와 같은 방법으로 업그레이드 가능하다.
l 새 API로 Contacts의 모든 사용처를 변경한다. 모두 마친 후에는 애플리케이션 빌드중에 발생하는 모든 사용되지 않음 경고는 무시한다. 새 애플리케이션은 다중계정과 Android 2.0의 다른 특성들의 장점을 모두 지원하게 될것이다.
l Manifest파일에 <uses-s아> 엘리먼트에 android:minSdkVersion속성을 업데이트한다. 새 API를 사용하려면 API level ‘5’이상을 설정해야 한다.
Maintaining two applications
Android 2.0이전과 이후 버전을 모두 지원하는 애플리케이션을 만들고자 한다면, 다음과 같은 작업을 해야 한다.
l 기존 어플 복제
l 이전 어플 수정
n 어플 적재시 SDK버전을 확인해야 함. 버전정보는 android.os.Build.VERSION.SDK. 값을 확인하면 됨.
n 버전이 맞지 않으면 마켓으로 새로운 어플을 설치할 수 있도록 팝업을 띄워주는 것이 좋다.(see Using Intents to Launch Market).
l 새 어플 수정
n API 호출부분 모두 수정
n AndroidManifest.xml수정
u 애플리케이션이름과 패키지이름을 새로 부여하라. 현재 안드로이드 마켓은 동일 이름/패키지로 두개의 어플리케이션 설치를 지원하지 않는다.
u android:minSdkVersion
수정
l 두개의 어플을 모두 마켓에 발표한다. 하나는 업그레이드버전이고 하나는 새로운 버전이다. 간략하게 차이점에 대한 설명을 달아야 한다.
이 방법은 두가지 단점이 있다.
u 새 어플은 이전 어플의 데이터를 읽을수 없다. 어플의 데이터는 동일한 패키지에 해당하는 코드에서만 접근 가능하다.
u 업그레이드 과정이 아주 사용자에게 투박하다. 일부 사용자는 두개를 다 사용하거나 모두 삭제할수도 있다.
Supporting the old and new APIs in the same application
이것은 아주 사소한 트릭이지만 노력에 비해 결과는 훌룡하다. 하나의 패키지에 모든 플랫폼에서 동작하게 할 수 있다.
기존 어플의 모든 Contacts를 사용하는 코드를 모아서 하나의 클래스로 만들어라. 예를 들면 아래와 같이 하면된다.
ContactAccessor라는 abstract class를 만들고 이를 확장하여 old API를 억세스하는 class와 new API를 억세스하는 class를 만든다. 그러고 SdkVersion에 따라 적절한 class를 생성하여 미리정의된 interface method를 호출하여 사용하도록 한다.
기존 코드는 아래와 같다.
protected void pickContact() {
startActivityForResult(new Intent(Intent.ACTION_PICK, People.CONTENT_URI), 0);
}
위 코드를 아래와 같이 변경하여 사용할 수 있도록 한다.
private final ContactAccessorOldApi
mContactAccessor = new ContactAccessorOldApi();
void pickContact() {
startActivityForResult(mContactAccessor.getContactPickerIntent(), 0);
}
ContactAccessorOldApi의
getContactPickerIntent()이다.
public Intent
getContactPickerIntent() {
return new Intent(Intent.ACTION_PICK, People.CONTENT_URI);
}
이제 ContactAccessor를 정의하고 이를 확장하여 Old API용과 New API용 클래스를 정의한다.
public abstract class ContactAccessor {
public abstract Intent
getContactPickerIntent();
...
}
public class ContactAccessorNewApi extends ContactAccessor {
@Override
public Intent
getContactPickerIntent() {
return new Intent(Intent.ACTION_PICK, Contacts.CONTENT_URI);
}
...
}
ContactAccessor에서 SdkVersion에 따라 분기하여 적절한 class를 리턴하는 singletone 함수를 만든다.
private static ContactAccessor sInstance;
public static ContactAccessor getInstance() {
if (sInstance == null) {
String className;
int sdkVersion = Integer.parseInt(Build.VERSION.SDK);
if (sdkVersion < Build.VERSION_CODES.ECLAIR) {
className = "ContactAccessorOldApi";
} else {
className = "ContactAccessorNewApi";
}
try {
Class<? extends ContactAccessor> clazz =
Class.forName(ContactAccessor.class.getPackage() + "." + className)
.asSubclass(ContactAccessor.class);
sInstance = clazz.newInstance();
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
return sInstance;
}
이제 아래와 같이 ContactAccessor를 생성하여 SdkVersion에 상관없이 사용할 수 있는 객체를 완성하였다.
private final ContactAccessor
mContactAccessor = ContactAccessor.getInstance();