プログラマーのメモ書き

伊勢在住のプログラマーが気になることを気ままにメモったブログです

【Android】 SQLite と CursorLoader の使い方(2/3): Content Provider の設定

Cursor Loader はContent Provider を前提にしているので、Content Provider を定義します。

 

Content Provider の設定

愚鈍人:コンテンツプロバイダ を参考に、コンテンツプロバイダを定義します。

SQLiteOpenHelper (の派生クラス)の定義は(1/3)の記事と同じです。コンテンツプロバイダの定義は下記の通りになります。

package com.mori_soft.android.sqlitesamplecp;

import java.util.HashMap;

import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;

public class SampleTblProvider extends ContentProvider {

    private SampleTblOpenHelper mHelper;
    private static final UriMatcher mUriMatcher;

    private static final int PERSONS = 1;
    private static final int PERSON_ITEM = 2;

    private static HashMap mProjectionMap;

    static {
        // UriMatcherの定義
        mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        mUriMatcher.addURI(SampleTblContract.AUTHORITY, SampleTblContract.PATH, PERSONS);
        mUriMatcher.addURI(SampleTblContract.AUTHORITY, SampleTblContract.PATH + "/#", PERSON_ITEM);

        // ProjectionMapの定義
        // テーブルに_IDがないので、rowidを使う
        mProjectionMap = new HashMap();
        mProjectionMap.put(SampleTblContract._ID, "rowid as " + SampleTblContract._ID);
        mProjectionMap.put(SampleTblContract.NAME, SampleTblContract.NAME);
        mProjectionMap.put(SampleTblContract.AGE, SampleTblContract.AGE);
    }

    @Override
    public boolean onCreate() {
        mHelper = new SampleTblOpenHelper(this.getContext());
        return false;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {
        
        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        
        switch (mUriMatcher.match(uri)){
        case PERSONS:
            qb.setTables(SampleTblOpenHelper.TBL_NAME);
            qb.setProjectionMap(mProjectionMap);
            break;
        case PERSON_ITEM:
            qb.setTables(SampleTblOpenHelper.TBL_NAME);
            qb.setProjectionMap(mProjectionMap);
            qb.appendWhere(SampleTblContract._ID + "=" + uri.getPathSegments().get(1));
            break;
     default:
            throw new IllegalArgumentException("Unknown URI" + uri);
        }

        SQLiteDatabase db = mHelper.getReadableDatabase();
        Cursor c = qb.query(db, projection, selection, selectionArgs, null,  null, sortOrder);
        
        // 更新対象uriの登録
        c.setNotificationUri(getContext().getContentResolver(), uri);
        
        return c;
    }
        
    @Override
    public String getType(Uri uri) {
        switch (mUriMatcher.match(uri)) {
        case PERSONS:
            return SampleTblContract.CONTENT_TYPE;
        case PERSON_ITEM:
            return SampleTblContract.CONTENT_ITEM_TYPE;
         default:
                throw new IllegalArgumentException("Unknown URI " + uri);
        }
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        
        // insertはpersons の形式で呼ばれる(persons/#ではない)
        if (mUriMatcher.match(uri) != PERSONS) {
            throw new IllegalArgumentException("Unknown URI " + uri);
        }

        if (values.containsKey(SampleTblContract.NAME) == false) {
            values.put(SampleTblContract.NAME, "匿名希望");
        }

        SQLiteDatabase db = mHelper.getWritableDatabase();
        long rowid = db.insert(SampleTblOpenHelper.TBL_NAME, null,  values);
        
        if (rowid > 0) {
            Uri ret = ContentUris.withAppendedId(SampleTblContract.CONTENT_URI, rowid);
            
            // 更新通知
            getContext().getContentResolver().notifyChange(ret, null);
            return ret;
        }
        
        throw new SQLException("Failed to insert row into " + uri);
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        
        SQLiteDatabase db = mHelper.getWritableDatabase();
        
        int count;
        switch (mUriMatcher.match(uri)) {
        case PERSONS:
            count = db.delete(SampleTblOpenHelper.TBL_NAME, selection, selectionArgs);
            break;
        case PERSON_ITEM:
            String id = uri.getPathSegments().get(1);
            count = db.delete(SampleTblOpenHelper.TBL_NAME,
                    //SampleTblContract._ID + "=" + id +
                    "rowid" + "=" + id +
                    (!TextUtils.isEmpty(selection) ? " AND (" + selection + ")" : ""),
                    selectionArgs);
            break;
     default:
            throw new IllegalArgumentException("Unknown URI " + uri);            
        }

        // 更新通知
        getContext().getContentResolver().notifyChange(uri, null);
        
        return count;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        
        SQLiteDatabase db = mHelper.getWritableDatabase();
        
        int count;
        switch (mUriMatcher.match(uri)) {
        case PERSONS:
            count = db.update(SampleTblOpenHelper.TBL_NAME, values, selection, selectionArgs);
            break;
        case PERSON_ITEM:
            String id = uri.getPathSegments().get(1);
            count = db.update(SampleTblOpenHelper.TBL_NAME,
                    values,
                    //SampleTblContract._ID + "=" + id +
                    "rowid"  + "=" + id +
                    (!TextUtils.isEmpty(selection) ? " AND (" + selection + ")" : ""),
                    selectionArgs);
            break;
     default:
            throw new IllegalArgumentException("Unknown URI " + uri);            
        }

        // 更新通知
        getContext().getContentResolver().notifyChange(uri, null);
        
        return count;
    }

}

今回使ったSampleTblOpenHelperの定義では、_id列を作っていないので、SQLiteQueryBuilderへProjectionMapを定義して、_id として指定されたものを、rowid as _id のように別名にして呼び出しています。また、update/delete で_idを指定する箇所も_idではなく、rowidを使っています。

rowidについてはこちらなども参照

Content Provider が外部(実際はContent Resolver)に対してどのようなURIでアクセス可能かを示す"契約"を表すContractクラスを作っておきます。Contractクラスは独立したクラスである必要はなく、必要な情報がContentProviderの派生クラス等で定義されていてもOKです。

package com.mori_soft.android.sqlitesamplecp;

import android.content.ContentResolver;
import android.net.Uri;
import android.provider.BaseColumns;

public final class SampleTblContract implements BaseColumns {
    
    public static final String AUTHORITY = "com.mori_soft.android.sqlitesamplecp.sampletblprovider";
    public static final String PATH = "persons";
    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH);

    private static final String PROVIDER_SPECIFIC_NAME = AUTHORITY;
    private static final String PROVIDER_SPECIFIC_TYPE = PATH;
    public static final String CONTENT_TYPE = ContentResolver.CURSOR_DIR_BASE_TYPE + "/" + "vnd." + PROVIDER_SPECIFIC_NAME + "." + PROVIDER_SPECIFIC_TYPE;
    public static final String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/" + "vnd." + PROVIDER_SPECIFIC_NAME + "." + PROVIDER_SPECIFIC_TYPE;

    public static final String NAME = "name";
    public static final String AGE = "age";
}

MIME type(上記Contractクラスの、CONTENT_TYPEとCONTENT_ITEM_TYPE)の定義の仕方については、Googleのドキュメントが詳しいです。

Activityは下記のようにしました。

package com.mori_soft.android.sqlitesamplecp;

import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.app.ListActivity;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.ContentObserver;
import android.database.Cursor;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.widget.ListAdapter;
import android.widget.ListView;

public class MainActivity extends ListActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        
        Uri uri = SampleTblContract.CONTENT_URI;
        final Cursor c = managedQuery(uri, null, null, null, null); // 内部でContentResolerで処理する
        
        ListAdapter adapter = new TestSimpleCursorAdapter(
                this,
                android.R.layout.two_line_list_item,
                c,
                new String[] {SampleTblContract.NAME, SampleTblContract.AGE},
                new int[] {android.R.id.text1, android.R.id.text2});
                //CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);

        // Bind to our new adapter.
        setListAdapter(adapter);

        // 更新の確認
        c.registerContentObserver(new ContentObserver(new Handler()) {

            @Override
            public void onChange(boolean selfChange) {
                // TODO Auto-generated method stub
                super.onChange(selfChange);
                
                Log.d("ContentObserver", "ContentObserver#onChange called:" + selfChange);
                //c.requery(); // 表示更新の意味なら不要(そもそもdeprecatedだけどね)
            }
        });
    }

    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        super.onListItemClick(l, v, position, id);
        
        // アイテムにタッチすると、そのアイテムを削除する
        /*
       Uri uri = ContentUris.withAppendedId(SampleTblContract.CONTENT_URI, id);
       getContentResolver().delete(uri, null, null);
       */
        
        // アイテムにタッチすると、年齢が増える
        Uri uri = ContentUris.withAppendedId(SampleTblContract.CONTENT_URI, id);
        ContentValues cv = new ContentValues();
        cv.put(SampleTblContract.AGE, "99");
        getContentResolver().update(uri, cv, null, null);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_main, menu);
        return true;
    }
}

SimpleCursorAdapterをそのまま使ってもよいのですが、後述するようにContent Provider でsetNotificationUriを設定した際の動作を確認(更新通知のテスト)するため、派生クラスを使うようにしています。また、更新通知の確認のため、registerContentObserverにより通知を受け取るオブジェクトも指定しています。

このSimpleCursorAdapterの派生クラスは下記のようにを定義しました。

package com.mori_soft.android.sqlitesamplecp;

import android.content.Context;
import android.database.Cursor;
import android.util.Log;
import android.widget.SimpleCursorAdapter;


public class TestSimpleCursorAdapter extends SimpleCursorAdapter {

    @Deprecated
    public TestSimpleCursorAdapter(Context context, int layout, Cursor c,
            String[] from, int[] to) {
        super(context, layout, c, from, to);
    }
    
/*
   public TestSimpleCursorAdapter(Context context, int layout, Cursor c,
           String[] from, int[] to, int flags) {
       //super(context, layout, c, from, to, flags);
       super(context, layout, c, from, to, flags);
       // TODO Auto-generated constructor stub
   }
*/
    
    @Override
    protected void onContentChanged() {
        // TODO Auto-generated method stub
        super.onContentChanged();
        
        Log.d("TestSimpleCursorAdapter", "onContextChanged called");
    }

}

onContentChangedが呼ばれたら、ログを出力するだけです。また、派生元クラスのコンストラクタは、deprecatedになっているほうのSimpleCursorAdapterのコンストラクタを呼んでいます。

Content Provider を定義したので、AndroidManifest.xml に次の記述を追加します。

        <provider
            android:name="SampleTblProvider" 
            android:authorities="com.mori_soft.android.sqlitesamplecp.sampletblprovider"
            android:exported="false">
        </provider>

ここで、exported="false"としているのは、他のアプリケーションに対してContent Providerを公開しないという設定です。

レイアウトはListViewのみを持ち、(1/3)の記事と大きく変わらずです。

 

実行

上記のように定義して、実際に実行してみます。問題がなければ、ListViewに名前と年齢が表示されます。

 

実験

次に、SampleTblProvider#queryメソッドで、Cursor#setNotificationUri を設定した効果を見てみます。この状態でアイテムにタッチすると、そのアイテムの年齢が99歳になります。画面も自動で更新されています。

画面更新が自動で行われるのは、今回使ったSimpleCursorAdapterのコンストラクタ(既に、deprecatedになっている) が、自動でqueryを発行するためのようです。この時、UIスレッドで処理が行われるのでdeprecatedになったようです。

詳しくは『Googleのドキュメント』や『Android:CursorAdapterコンストラクタの一部非推奨化』などをご覧ください。

 

更新通知はどうなっているかを見るために、アイテムを選択した際のログを見ると、

となっています。

これより、TestSimpleCursorAdapter (SimpleCursorAdapterの派生クラス)の onContentChange が呼ばれることがわかります。これは、ContentResolver#notifyChange のAPIドキュメントにあるように、CursorAdapter にはデフォルトで通知がいくということに対応していることがわかります。また、今回のサンプルでは、Activity 内で Cursor#registerContentObserver を呼出し、ContentObserver も登録しているので、これにも通知が行くことがわかります。

また、SampleTblProvider#query メソッドで、Cursor#setNotificationUri をコメントアウトして実行すると、この場合は画面が更新されません。しかし、SQLiteのDBファイルをDDMSで取得して、eclipseのDBViewerで見ると、年齢は確かに変更されていることがわかります。

(参考サイト)

 

このようにContent Provider を定義し、SimpleCursorAdapterを用いると、自動での画面更新が行われるのがわかります。この流儀はAndroid2.3までの古いやり方ですかね?今はAndroid 3.0以前が対象でもSupport Packageを取り入れ、CursorLoaderを使って、UIスレッドではqueryを発行せず、別スレッドで処理するのが推奨のようです。

なので実際のアプリでは上記のような方法は使わないほうが良いと思います。