EclipseによるAndroidのproviderの作り方とサンプルプログラム

入門 Android 2 プログラミング”を読んで結構参考になった。個人的にはプロバイダやサービスの仕組みに興味があったので、本書を参考に自前でゼロから組んでみた。先ずはプロバイダを作成してみたのだが、Eclipseでの簡単設定方法とサンプルプログラムについてメモしておく。

自分でゼロから作ってみて気が付いたのだけど、“入門 Android 2 プログラミング”にあるサンプルはもっと本格的なアプリケーションから不要なところを削除して書かれたようだ。そのために不要なコードが残っていたり、簡単なコードもわざわざメソッドにデレゲーションしているので、返って読み難い部分もあった。そういった部分もまとめて、ほぼ骨組みだけのコードで組んでみた。

Eclipseによるproviderの構築

前提として次のようなアプリケーションを作成する。

  • プロジェクト名: Provider_Skeleton_1
  • パッケージ名: com.example.android.provider_skeleton_1
  • アプリケーション名: ProductBrowser1
  • プロバイダ名: ProductDbProvider1

スマートフォン等の“製品”のデータベースから製品の各種情報を提供するプロバイダを想定している。ここでは製品の“製品名”、“重さ”、“備考(メーカー名等)”を検索できるようにする。

1:プロジェクトの作成
Eclipseのメニューから[File]−[New]−[Android Project]を選択して新規プロジェクト作成のダイアログを開き、次のようにプロジェクト名、アプリケーション名などを入力する。

[Finish]をクリックして終了。

2:マニフェストにプロジェクトを追加する
Eclipseの左側にある[Project]タブから、今作成したプロジェクトの中にある“AndroidManifest.xml”を選択する。マニフェストの設定画面(右側)の下の方にアクティビティなどを追加できる部分(Application Nodes)があるので、そこの[Add...]をクリックする。

すると何をエレメントを追加するのか聞かれるので“Provider”を選択して[OK]をクリックする。

マニフェストの設定画面で、今追加したプロバイダを選択すると、その右側にプロバイダに関する情報を入力する部分が表示される。その中で*印のついた必須項目の“Name”と“Authorities”を入力する(Authoritiesは下の方に隠れているのでスライドバーを下げれば表示される)。Nameには “ProductDbProvider1”、Autoritiesには“com.example.android.provider_skeleton_1.ProductDbProvider1”と入力する。アクティビティやプロバイダなどの名前はパッケージ名を省略する場合は頭にドットを付けて“.ProductDbProvider1”と表現するのが正式(qualified名)かと思うが、何故かここではドットを含んだ文字列を受け付けてくれない。qualified名でなくてもプログラムはちゃんと動くので問題ないが、気になるのであれば、次に説明するClass設定画面を終わらせた後、改めてNameをqualified名にすることができる。下の例ではドット付きのqualified名を入力しているので小さな赤い×印が出ている。(わざわざ警告を出す位なのでバグではないと思うのだが、何でこういった仕様なのかわからない。)

この2つを入力したら“Name”というタイトルをクリックする。(下線のついた“Name”というタイトルがリンクになっている。)すると、Class設定ダイアログで現れる。(上のマニフェスト画面でNameとAuthoritiesを入力せずに行きなりNameタイトルのリンクをクリックしてClass設定ダイアログで入力する方法もある。)

“Inherited abstract methods”をチェックして[Finish]をクリックする。すると新規に作成するプロバイダのソースコード(この場合、ProductDbProvider1.java)のスケルトンが作成され、自動的にエディタ画面へ移る。

あとは、必要なソースコードを追加して完成させる(ソースコードについては後述)。
なお、このプログラムにはres/layoutの下にrow.xmlというファイルが必要となる。row.xmlには“表”の“行”の定義が入っている。まず、次のようにパッケージエクスプローラで“layput”をマウスで右クリックしてコンテキストメニューを出し、[New]−[File]を選択する。

するとNew Fileダイアログが開くので、File Name欄に row.xml を入力し[Finish]をクリックする。

からのrow.xmlが作られエディタ画面が開く。
全てのファイルの設定が終わると、次のようなファイル群となる。

完成したらRunで“Android Application”を選択して実行する。


providerの超シンプル・サンプルプログラム

次の3つのファイルを変更する。

  • ProductDbProvider1.java
  • ProductBrowser1.java
  • main.xml

(AndroidManifest.xmlは上の作業ですでに変更しているので保存だけしておく。)
そして次の1つファイルを追加する。

以上、4つのファイルの設定だけ。

ProductDbProvider1.java

プロバイダの本体。先ずはソース全体を表示しておく:

package com.example.android.provider_skeleton_1;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
//import android.provider.BaseColumns;
import android.text.TextUtils;
import android.util.Log;

public class ProductDbProvider1 extends ContentProvider {
	
	private static final String	AUTHORITY = 
		"com.example.android.provider_skeleton_1.ProductDbProvider1";
	private static final String	DB_NAME = "products.db";
	private static final int	DB_VERSION = 1;
	private static final String TABLE_NAME = "products";
	// ベースURIかインスタンスURIかの区別をするための定数。初期化子で使う。
	private static final int	BASE_URI = 1;
	private static final int	INSTANCE_URI = 2;
	private static UriMatcher	MATCHER;
	
	// コンテンツに関する定義(データベースの列名など)を宣言するクラス。
	// BaseColumnsをインプリしなくても"_id"カラムの定義をしておけば大丈夫。
	// SimpleCursorAdapterクラスで暗黙に"_id"カラムがあることを前提としているの"_id"の設定が必要。
	// public static final class Product implements BaseColumns {
	public static final class Product {
		public static final Uri	CONTENT_URI = 
			Uri.parse("content://" + AUTHORITY + "/" + TABLE_NAME);
		public static final String	_ID = "_id";
		public static final String	NAME = "name";
		public static final String	WEIGHT = "weight";
		public static final String	REMARKS = "remarks";
		public static final String	DEFAULT_SORT_ORDER = _ID + " ASC";
	}
	
	// MATCHER の初期化
	static {
		MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
		MATCHER.addURI(AUTHORITY, "products", BASE_URI);
		MATCHER.addURI(AUTHORITY, "products/#", INSTANCE_URI);
	}
	
	// SQLiteOpenHelperクラスを使ってデータベースを初期化&オープン
	private class ProductDbOpenHelper extends SQLiteOpenHelper {

		// コンストラクタ。 提供するコンテンツ(データベース)は決まっているので定数でビルトイン。
		public ProductDbOpenHelper(Context context) {
			super(context, DB_NAME, null, DB_VERSION);
		}
		
		// データベースの初期化。必要であればデータベースを作成し、ダミーデータを登録する。
		@Override
		public void onCreate(SQLiteDatabase db) {
			Cursor c = 
				db.rawQuery(
						"SELECT name FROM sqlite_master" +
							" WHERE type='table' AND name='"+ TABLE_NAME + "'", 
						null
						);
			
			if (c.getCount() == 0)
				try {
					// テーブルの作成
					db.execSQL("CREATE TABLE " + TABLE_NAME 
							+ " ( " + Product._ID + " INTEGER PRIMARY KEY AUTOINCREMENT"
							+ ", " + Product.NAME + " TEXT"
							+ ", " + Product.WEIGHT + " INTEGER"
							+ ", " + Product.REMARKS + " TEXT"
							+ " )"
							);
					
					// ダミーデータの定義
					final String[]	P_NAME = {
						"Xperia",	"Desire X06HT","IS01",	"IS02",		"LYNX SH-10B",	"Galaxy S",
						"HT-03A",	"SC-01B",	"X02T",		"X05HT",	"T-01B",	"Droid X",
						"Bold 9700","iPhine 4",	"aaaaa",	"bbbbb",	"ccccc",	"ddddd",
						"eeeee",	"fffff",	"ggggg",	"hhhhh",	"jjjjj"
					};
					final int[]	P_WEIGHT = {
						139,		135,		227,		123,		230,		119,
						123,		130,		129,		165,		160,		155,
						122,		137,		111,		222,		333,		444,
						555,		666,		777,		888,		999
					};
					final String[]	P_REMARKS = {
						"Sony Ericsson","HTC",	"Sharp",	"Toshiba",	"Sharp",	"SAMSUNG",
						"HTC",		"SAMSUNG",	"Toshiba",	"HTC",		"Toshiba",	"Motorola",
						"RIM",		"Apple",	"AAAAA",	"BBBBB",	"CCCCC",	"DDDDD",
						"EEEEE",	"FFFFF",	"GGGGG",	"HHHHH",	"JJJJJ"
					};
					
					// ダミーデータの設定
					ContentValues cv = new ContentValues();
					for (int i = 0; i < P_NAME.length; i++) {
						cv.put(Product.NAME, P_NAME[i]);
						cv.put(Product.WEIGHT, P_WEIGHT[i]);
						cv.put(Product.REMARKS, P_REMARKS[i]);
						// NullColumnHackにはNAMEを割り当てる。
						// 実際にはNAMEはNULLではないので今回は不要だが。
						db.insert(TABLE_NAME, Product.NAME, cv);
					}
				} finally {
					// Nothing to do
				}
			c.close();
		}
		
		@Override
		public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

			Log.w(
					this.getClass().getName().toString(), 
					"Upgrading Database, which will destroy all old data");
			db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
			onCreate(db);
		}
	}
	
	// プロバイダの本体
	private SQLiteDatabase db;

	@Override
	public int delete(Uri uri, String selection, String[] selectionArgs) {
		// TODO Auto-generated method stub
		return 0;
	}

	@Override
	public String getType(Uri uri) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public Uri insert(Uri uri, ContentValues values) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public boolean onCreate() {
		
		db = (new ProductDbOpenHelper(getContext())).getWritableDatabase();
		
		return (db == null) ? false : true;
	}

	@Override
	public Cursor query(Uri uri, String[] projection, String selection,
			String[] selectionArgs, String sortOrder) {
		
		SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
		
		qb.setTables(TABLE_NAME);
		
		if (MATCHER.match(uri) == INSTANCE_URI)
			qb.appendWhere(Product._ID + " = " + uri.getPathSegments().get(1));
		
		String sort;
		if (TextUtils.isEmpty(sortOrder))
			sort = Product.DEFAULT_SORT_ORDER;
		else
			sort = sortOrder;
		
		Cursor cursor = qb.query(db, projection, selection, selectionArgs, null, null, sort);
		cursor.setNotificationUri(getContext().getContentResolver(), uri);
		
		return cursor;
	}

	@Override
	public int update(Uri uri, ContentValues values, String selection,
			String[] selectionArgs) {
		// TODO Auto-generated method stub
		return 0;
	}

}

このサンプルでは、データベースの生成、テーブルの生成とダミーデータの追加(onCreate)、そしてデータの検索(query)だけを作る。(追加(insert)、更新(update)、削除(delete)などについては実装しない。あくまでもプロバイダの実装方法についての骨組みだけ示すため。)

onCreateでは“SQLiteOpenHelper”をベースとした“ProductDbOpenHelper”クラスでデータベースの初期化をしている。このプログラムの殆どはProductDbOpenHelperはである。(逆に言えば、プロバイダの本質的な部分はそれ以外の僅かな部分。)

さて、上のコードの要点を説明しておく。先ずは定数などの定義から:

private static final String	AUTHORITY = 
	"com.example.android.provider_skeleton_1.ProductDbProvider1";
private static final String	DB_NAME = "products.db";
private static final int	DB_VERSION = 1;
private static final String TABLE_NAME = "products";
// ベースURIかインスタンスURIかの区別をするための定数。初期化子で使う。
private static final int	BASE_URI = 1;
private static final int	INSTANCE_URI = 2;
private static UriMatcher	MATCHER;

これらはこのプロバイダ内部だけで使うprivate final。“AUTHORITY”はマニファストで記述したものと同じ文字列であることを確認する。
“BASE_URI”と“INSTANCE_URI”は、他の例等を見ると“データ名”と“データ名_ID”となっている。しかし、これは与えられたURIがベースURI(つまりID無し)であるかインスタンスURI(つまりID付き)であるかを区別するための抽象表現なので、上記のような定数名にした。その方がプログラムも読みやすいと思う。
“MATCHER”はクラスなのでこの後、static初期化子で初期化している。
ここで、参考書などでは次のようにHashMapを使ったProjection Map(column Map)を宣言している。

private static HashMap<String, String>	PROJECTION_MAP;

しかし、Projection Mapは(Androidのホームページ(setProjectionMap)を見ると)複数のテーブルでJOINを実施する際に、呼び出したアクティビティが指定したカラム名に曖昧さがないように、より正確なカラム名マッピングするためとある。テーブルを一つしか持たない単純なサンプルでは必要ないので割愛した。

次はコンテンツ(データベース)の定義:

// コンテンツのプロバティ(データベースの列)に関する定義を宣言するクラス
// BaseColumnsをインプリしなくても"_id"カラムの定義をしておけば大丈夫。
// SimpleCursorAdapterクラスで暗黙に"_id"カラムがあることを前提としているの"_id"の設定が必要
// public static final class Product implements BaseColumns {
public static final class Product {
	public static final Uri	CONTENT_URI = 
		Uri.parse("content://" + AUTHORITY + "/" + TABLE_NAME);
	public static final String	_ID = "_id";
	public static final String	NAME = "name";
	public static final String	WEIGHT = "weight";
	public static final String	REMARKS = "remarks";
	public static final String	DEFAULT_SORT_ORDER = _ID + " ASC";
}

ここではデータベース(もしくはプロバイダが提供するデータ)の構造等をクラスとして定義している。(このサンプルでは“製品”の情報なので“Product”という名前にした。)このクラスについては2点ほど検討事項があった。一つは、このクラスをプロバイダ(つまりProductDbProvider1)の内部で定義するのか、もう一つは“BaseColumns”をインプリメントするのか、という点である。

プロバイダ内部で定義するかどうかは、このデータが他の多くのアプリケーションから利用されるかどうかによるだろう。ここではプロバイダにローカルな位置づけでプロバイダ内部で定義した。「初めてのGoogle Androidプログラミング」ではプロバイダとは別のクラスとして定義している。

次にBaseColumnsをインプリメントするかどうかだが。Android流のデータベースの利用を前提とすれば使った方が良いのだろう。Androidの内部で使われている数々のデータベース(連絡先等)はこのBaseColumnsをインプリしている。BaseColumnsをインプリするとデータベースのカラム(列)名として“_id”が有ることが前提となる。ここにはユニークな値が書き込まれ一般にPRIMARY KEYとして利用される。一方で、製品データベースの様なものではJANコードの様なものをPRIMARY KEYにしたいので不要になる。その場合はAndroid環境に依存した“_id”が存在することを前提とするのは頂けない。BaseColumnsをインプリしたくないところだ。
上の例ではBaseColumnsをインプリしていないケースでコンパイルしているが、こうしたこともできるよ、というサンプル。実際にはメインActivityで使っているSimpleCursorAdapterクラス等が暗黙にデータベースに"_id"カラムがあることを前提して実装されているらしく、BaseColumnsを使わなくても結局は“_id”を定義しなければならない。なんかちょっとナンダカナァって感じ。所詮は携帯の中のアプリケーションなので、郷に入れば郷に従え、なのかも知れない。

// MATCHER, PROJECTION_MAP の初期化
static {
	MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
	MATCHER.addURI(AUTHORITY, "products", BASE_URI);
	MATCHER.addURI(AUTHORITY, "products/#", INSTANCE_URI);
}

ここはお約束みたいなところ。URIが“products”で終わる場合はベースURI、“products スラッシュ 番号”で終わる場合はインスタンスURIと宣言している。

次に出てくるのはデータベースの初期化等で利用するSQLiteOpenHelperのサブクラスProductDbOpenHelperの定義。実際のアプリケーションでは、結構複雑な処理をしなければならないが、ここではデータベースの作成とダミーデータの設定といったスタブコードになっている(つまり手を抜いているということ)。なので、特にコメントは無い。

ProductDbOpenHelperの後がデータベース操作の本体部分。ここではプロバイダの構成方法を示すだけなので、“onCreate”と“query”しか実装していない。onCreateでは上で定義したProductDbOpenHelperを呼び出してデータベースをオープン(&初回にのみ初期化)しているだけ。queryは:

@Override
public Cursor query(Uri uri, String[] projection, String selection,
		String[] selectionArgs, String sortOrder) {
	
	SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
	
	qb.setTables(TABLE_NAME);
	
	if (MATCHER.match(uri) == INSTANCE_URI)
		qb.appendWhere(Product._ID + " = " + uri.getPathSegments().get(1));
	
	String sort;
	if (TextUtils.isEmpty(sortOrder))
		sort = Product.DEFAULT_SORT_ORDER;
	else
		sort = sortOrder;
	
	Cursor cursor = qb.query(db, projection, selection, selectionArgs, null, null, sort);
	cursor.setNotificationUri(getContext().getContentResolver(), uri);
	
	return cursor;
}

これもシンプル。

  1. これから検索するテーブルをしていする。
  2. 与えられたUriインスタンスURIであれば、特定の行だけを検索するようにWHERE句を作っている。ベースURIであれば何もしない(=テーブル全体を検索対象とする)。getPathSegmentsはURIのパス部分(UriのAuthorityの右側)だけを抽出する。この例では第0要素が“products”で、もしインスタンスIDがあれば第1要素がその番号となる。なのでget(1)でインスタンス番号を取得できる。
  3. ソート条件の設定。引数が空であれば、標準のソート条件を設定し、空でなければ、そのまま引数を使う。
  4. 検索を実行する。結果はCursorクラスで返ってくる。
  5. データ変更のためのNotificationを発行する。(が、検索(query)に必要なのだろうか?)

以上でプロバイダProductDbProvider1の構築は終了。今回は使っていないのでgetTypeの実装も省略している。

ProductBrowser1.java

このアプリケーションのメイン・アクティビティ。上のプロバイダを使ってデータベースを検索して結果を表示するだけ。

package com.example.android.provider_skeleton_1;

import android.app.ListActivity;
import android.database.Cursor;
import android.os.Bundle;
import android.widget.ListAdapter;
import android.widget.SimpleCursorAdapter;

public class ProductBrowser1 extends ListActivity {
	
	private static final String[] PROJECTION =
		new String[] {
			ProductDbProvider1.Product._ID,
			ProductDbProvider1.Product.NAME,
			ProductDbProvider1.Product.WEIGHT,
			ProductDbProvider1.Product.REMARKS
	};
	private Cursor cursor;
	
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		
		cursor = 
			managedQuery(ProductDbProvider1.Product.CONTENT_URI, PROJECTION, null, null, null);
		
		ListAdapter adapter =
			new SimpleCursorAdapter(
					this, 
					R.layout.row, 
					cursor, 
					new String[] {
							ProductDbProvider1.Product._ID,
							ProductDbProvider1.Product.NAME,
							ProductDbProvider1.Product.WEIGHT,
							ProductDbProvider1.Product.REMARKS
					}, 
					new int[] {
							R.id.c_id,
							R.id.c_name,
							R.id.c_weight,
							R.id.c_remarks
					});
		setContentView(R.layout.main);
		setListAdapter(adapter);
	}
	
	@Override
	protected void onDestroy() {
		super.onDestroy();
		cursor.close();
	}

}

これも単純。ベースUriを指定してmanagedQueryでプロバイダを呼び出し、結果をCursorで受け取る。それをListAdapterに渡してAndroid画面に表示しているだけ。

このサンプルはプロバイダの構築が目的なので画面は凝っていない(表示データの見出し行もない)。mail.xmlは次のような内容にした:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="vertical"
	android:layout_width="fill_parent"
	android:layout_height="fill_parent"
	>
	<ListView
		android:id="@android:id/list"
		android:layout_width="fill_parent"
		android:layout_height="fill_parent"
		/>
</LinearLayout>

単にデータベースの検索結果を表示するListViewがあるだけ。

ListViewに表示するリストの各行はrow.xmlで定義している。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="horizontal"
	android:layout_width="fill_parent"
	android:layout_height="fill_parent"
	>
	<TextView
		android:id="@+id/c_id"
		android:layout_width="24sp" 
		android:layout_height="wrap_content"
		android:gravity="right"
		android:paddingLeft="2sp"
		android:paddingRight="2sp"
		/>
	<TextView
		android:layout_width="1px" 
		android:layout_height="wrap_content"
		android:background="#7f7f7f"
		/>
	<TextView
		android:id="@+id/c_name"
		android:layout_width="100sp" 
		android:layout_height="wrap_content"
		android:singleLine="true"
		android:paddingLeft="2sp"
		android:paddingRight="2sp"
		/>
	<TextView
		android:layout_width="1px" 
		android:layout_height="wrap_content"
		android:background="#7f7f7f"
		/>
	<TextView
		android:id="@+id/c_weight"
		android:layout_width="50sp" 
		android:layout_height="wrap_content" 
		android:gravity="right"
		android:paddingLeft="2sp"
		android:paddingRight="2sp"
		/>
	<TextView
		android:layout_width="1px" 
		android:layout_height="wrap_content"
		android:background="#7f7f7f"
		/>
	<TextView
		android:id="@+id/c_remarks"
		android:layout_width="match_parent" 
		android:layout_height="wrap_content" 
		android:singleLine="true"
		android:paddingLeft="2sp"
		android:paddingRight="2sp"
		/>
</LinearLayout>

だらだらと長くなったが、見やすいように途中に罫線代わりのTextViewを挿入したため(罫線(各Viewの枠)の表示方法ってあったような気もするのだけど、思い出せなかった)。