Androidの電話帳を住所で検索し電話番号を取得する

nakamuraが2015/02/06 18:44:31に投稿

Androidの電話帳を住所で検索し電話番号を取得する方法

Androidの電話帳について

Androidでは電話帳はSQLiteを使って管理していて、
/data/data/com.android.providers.contacts/databases/contacts2.db
に保存されています。Android Emulatorなどを使っていれば

> adb pull /data/data/com.android.providers.contacts/databases/contacts2.db

とすれば実際のファイルの内容を確認できます。
しかし通常のアプリケーションは直接このファイルを参照することはできないので、
Content Provider経由でデータを扱います。

Content Providerとは

Content Providerとは電話帳や、ユーザ辞書などのような複数のアプリケーションから参照されるデータを管理するためのコンポーネントです。
Activity, Service, Broadcast Receiverとともに4大要素と呼ばれています。

Content Providerの使い方

まずContext#getContentResolver()でContent Resolverを取得します。
ContextはActivityのスーパークラスなので、Activtyのメソッドからは以下のように呼べます。

    final ContentResolver resolver = getContentResolver();

つぎに以下のメソッドContentResolver#query()でContent Providerからデータを取得します。

     Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)

このメソッドは大体SQLでいうSELECT文のようなことをします。
引数はそれぞれ

SELECT
    <projection>
FROM <uriで指定されたテーブルやビュー>
WHERE <selection>
ORDER <sortOrder>

のように使われます。selectionには例えば、

address LIKE ?

のように「?」でパラメータを指定できます。このときselectionArgsが{ "%foo%" }だと、

address LIKE '%foo%'

というふうに展開されます。直接selectionに検索キーワードを書いてしまうとSQLインジェクションの恐れがあるので、selectionArgsを使います。

返り値のCursorにSQL実行して得られた結果が入っています。

Contact Provider

電話帳を管理しているContent ProviderはContact Providerです。
Contact Providerを使って検索するのに必要なURIやprojectionに指定する列名などは
android.provider.ContactsContractクラスで定義されています。

名前で検索して電話番号を取得

電話番号を取得するには、URIにContactsContract.CommonDataKinds.Phone.CONTENT_URIを指定します。
電話番号を表す列名はPhone.NUMBERです。また名前を表す列名はPhone.DISPLAY_NAME_PRIMARYです。
これらを使えば、例えば名前の一部にnameを含む人の電話番号は以下のようにして検索できます。

final String[] proj = { Phone.NUMBER };
final String selection = Phone.DISPLAY_NAME_PRIMARY + " LIKE ?";
final String[] args = { "%" + name + "%" };

final ContentResolver resolver = getContentResolver();
final Cursor cursor = resolver.query(
    Phone.CONTENT_URI, proj, selection, args, null
);

while (cursor.moveToNext()) {
    String phoneNumber = cursor.getString(0); // 0はproj配列におけるNUMBERの位置
    ....
}
cursor.close();

上のような検索をすると、(実際とは違いますが)大体以下のようなSQLが実行されます。

SELECT DISTINCT 電話番号
FROM data
WHERE display_name LIKE '%hoge%'

Android Emulatorでは以下のコマンドを実行すると、実際どのようなSQLが実行されているかlogcatから確認することができます。

> adb shell setprop log.tag.SQLiteStatements VERBOSE
> adb stop
> sdb start

名前で検索して住所を取得

これも同じようにできます。今度はURIにはContactsContract.CommonDataKinds.StructuredPostal.CONTENT_URIを使い、
列名にはStructuredPostal.FORMATTED_ADDRESSを使います。
名前の列名はStructuredPostal.DISPLAY_NAME_PRIMARYです。

final String[] proj = { StructuredPostal.FORMATTED_ADDRESS };
final String selection = StructuredPostal.DISPLAY_NAME_PRIMARY + " LIKE ?";
final String[] args = { "%" + name + "%" };

final ContentResolver resolver = getContentResolver();
final Cursor cursor = resolver.query(
    StructuredPostal.CONTENT_URI, proj, selection, args, null
);

while (cursor.moveToNext()) {
    String address = cursor.getString(0);
    ....
}
cursor.close();

住所で電話番号を検索

間違った方法

上の2つの例から、住所から電話番号を検索するには、以下のようにしたらよいように思えます。

final String[] proj = { Phone.NUMBER };
final String selection = StructuredPostal.FORMATTED_ADDRESS + " LIKE ?";
final String[] args = { "%" + address + "%" };

final ContentResolver resolver = getContentResolver();
final Cursor cursor = resolver.query(
    Phone.CONTENT_URI, proj, selection, args, null
);

while (cursor.moveToNext()) {
    String phoneNumber = cursor.getString(0); // 0はproj配列におけるNUMBERの位置
    ....
}

しかしこれでは正しく検索はできません。住所ではなく電話番号の部分一致で検索してしまいます。

検索できた方法

調べた限りでは一度のqueryメソッド呼び出しではできないようです。
しかたないので住所で検索して、その人のCONTACT_IDを取り出し、そのIDでもう一度電話番号を検索します。

final String[] proj = { StructuredPostal.CONTACT_ID };
final String selection = StructuredPostal.FORMATTED_ADDRESS + " LIKE ?";
final String[] args = { "%" + address + "%" };

final ContentResolver resolver = getContentResolver();
final Cursor cursor = resolver.query(
    proj, selection, args, null
);

String[] phone_proj = { Phone.NUMBER };
String phone_selection = Phone.CONTACT_ID + "= ?";
while (cursor.moveToNext()) {
    String contactId = cursor.getLong(0);
    String[] phone_args = { String.valueOf(contactId) };
    Cursor cur = resolver.query(Phone.CONTENT_URI, phone_proj, phone_selection, phone_args, null);
    while (cur.moveToNext()) {
        String phoneNumber = cur.getString(0);
        ...
    }
    cur.close();
}
cursor.close();

なぜできない??

電話帳のデータ構造

調べる前は電話帳はこんなデータ構造なのだろうと想像していました。

ID 名前 電話番号 住所 メールアドレス
1 佐藤一郎 00-1111-1111 大阪市北区梅田1-1-1 sato@example.com
2 高橋次郎 00-2222-2222 東京都新宿区新宿2-2-2 taka@example.com

このようににデータを持っているなら、住所から電話番号が検索できないのは不可解です。

しかし、実際のデータ構造はまったく違います。

raw_contactsテーブル

id display_name
1 佐藤一郎
2 高橋次郎

dataテーブル

id mime_type raw_contact_id data1 data2 data3 ... data15
1 名前 1 佐藤一郎 一郎 佐藤
2 電話番号 1 00-1111-1111 携帯
3 住所 1 大阪市北区梅田1-1-1 自宅
4 メールアドレス 1 sato@example.com 自宅
5 名前 2 高橋次郎 次郎 高橋
6 電話番号 2 00-2222-2222 自宅
7 住所 2 東京都新宿区新宿2-2-2 自宅
8 メールアドレス 2 taka@example.com 自宅

data2~data15にはデータの種類に応じて、ふりがなや姓、自宅・職場などのデータが入っています。

確かにこういう風にしておけば、将来保存するフィールドが増えてもMIME TYPE変えて保存できるし、柔軟性は高そうです。でも検索しずらい…。

住所から電話番号を検索する間違った方法で書いた、

final String[] proj = { Phone.NUMBER };
final String selection = StructuredPostal.FORMATTED_ADDRESS + " LIKE ?";
final String[] args = { "%" + address + "%" };

で検索すると、

SELECT
    data1
FROM data
INNER JOIN raw_contacts ON data.raw_contact_id = raw_contacts.id
WHERE
    data1 LIKE '%address%' AND
    mimetype = 電話番号;

のようなSQLが実行されます。Phone.NUMBERもStructuredPostal.FORMATTED_ADDRESSもdata1だからです。

SQLで住所から電話番号を検索しようとするなら、

SELECT
    address.data1,
    phone.data1
FROM data address
INNER JOIN data AS phone
ON address.contact_id = phone.contact_id
WHERE
    address.mimetype = 住所 AND
    phone.mimetype = 電話番号 AND
    address.data1 LIKE '%address%'
;

みたいにする必要があります。でもどうやらこういう複雑なSQLは使えないようです。

それじゃあ、なんで名前だけは電話番号や住所と一緒に取得できるのかというと、
dataテーブルからSELECTするときにはraw_contactsテーブルをJOINしていて、
名前はraw_contacts.display_name列から取れるからです。