ContentProviderでアプリケーションのデータを共有する その1



今回は題名にある通り、ContentProviderを使ったデータの共有方法について説明してみたいと思います。


アプリケーションのデータを保存する手段としては、SQLiteを利用するのが一般的だと思いますが、場合によってはその蓄積したデータを他のアプリケーションからも利用したいと思うことが多々あると思います。
そんな場合はContentProviderをアプリに実装することで、そのデータを外部に公開することが可能です。
ContentProviderの詳細な説明については、他サイトを参考にして頂ければ良いと思います。

ContentProviderを使って、不特定多数のアプリにデータを公開する

図で説明すると、次のような感じになります。

例として、プロジェクトAとプロジェクトBがあるとします。
プロジェクトAのデータに対して、プロジェクトBがプロジェクトAのContentProviderを介して、
データベースを操作するイメージです。


それでは上記の通りデータがやり取りされていることを確認するために、Eclipseから新たにプロジェクトを2つ立ち上げてください。
プロジェクト名は、プロジェクトAとプロジェクトBとします。
プロジェクトAでは次のようなクラスを実装します。

  • Activityクラス
  • データベース操作クラス
  • クエリー定義クラス
  • ContentProviderを拡張したクラス
  • ArrayAdapterを拡張したクラス(リストビューのカスタマイズ)


Activityクラス Hello
アプリの起動と同時にデータベースを作成し、データを一件インサート後、
その直後にInsertしたデータをSelectして、リストビューに表示します。

package hoge.hoge.hatena.A;

import android.app.Activity;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.widget.ListView;

/**
 * Hello Activity
 * 概要:インサート処理とセレクト処理行い
 * リストビューにセレクトした内容を詰め込む
 * @author korsakov
 *
 */
public class Hello extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        //インサート処理
        DBHelper mDb = new DBHelper(this);
        SQLiteDatabase db = mDb.getWritableDatabase();
        try {
            
            db.beginTransaction();
            HelloTable q = new HelloTable();
            q.mMs = "Hello A";
            db.execSQL(q.getInsertQuery(), q.getInsertBindItem());
            db.setTransactionSuccessful();
        } finally {
            
            db.endTransaction();
            db.close();
        }
        
        //セレクト処理
        db = mDb.getReadableDatabase();
        HelloTable []bean = null;
        try {
            
            db.beginTransaction();
            HelloTable q = new HelloTable();
            
            //セレクトした結果のカーソルオブジェクトを変換する
            bean = q.makeData(db.rawQuery(q.getSelectQuery(), null));
            
            db.endTransaction();
        } finally {
            
            db.close();
        }
        
        ListView list = (ListView)this.findViewById(R.id.ListView01);
        list.setAdapter(new HelloListAdapter(this, 0, bean));
    }
}

データベース操作クラス DBHelper
SQLiteOpenHelperを拡張したクラスです。
このクラスを使うポイントとしては予めデータベース名を定義しておき、onCreateメソッドが走った時にクエリー発行して必要なテーブルの作成を行うことです。

データベース名は HELLO_DATABASE としておきます

package hoge.hoge.hatena.A;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class DBHelper extends SQLiteOpenHelper {

    private static final String DB_NAME = "HELLO_DATABASE";
    private static final int DB_VERSION = 1;
    
    public DBHelper(Context context) {
        
        //データベース名と、データベースバージョンを設定
        super(context, DB_NAME, null, DB_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        
        // 初回DB作成時は、こちらのメソッドが実行される
        
        db.beginTransaction();
        try {
            
            HelloTable q = new HelloTable();
            db.execSQL(q.getCreateQuery());
            db.setTransactionSuccessful();
            
        } finally {
            
            //トランザクションの終了
            db.endTransaction();
        }
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // TODO Auto-generated method stub

    }

}

クエリー定義クラス HelloTable
AndroidSQLiteの使い方を紹介しているサイトは幾つも有りますが、各サイトで紹介されている方法の多くはクエリーを発行するクラス内で、直接クエリーの文字列リテラルをソースに記述するやり方が紹介されています。
そのやり方だとtypoする事もあるし、自分的には何となく纏まりがない思うので、定義するテーブルごとにクラスを作成する事にしました。
次のようなコードよりも・・・

String table = “foo, bar”;
String[] columns = {“foo.a”, “bar.b”};
String selection = “foo.a = bar.a”;
Cursor c = db.query(table, columns, selection, null, null, null, null);

このほうがスマートだと思う。

HelloTable []bean = null;
HelloTable q = new HelloTable();
bean = q.makeData(db.rawQuery(q.getSelectQuery(), null));

package hoge.hoge.hatena.A;

import android.database.Cursor;

public class HelloTable {
    
    private static final String CREATE =
        "create table hello(_id integer primary key autoincrement not null, ms text not null)";
    private static final String DROP = "drop table hello";
    private static final String INSERT = "insert into hello(ms) values ( ? )";
    private static final String UPDATE = "update hello set ms = ? where _id = ?";
    private static final String DELETE = "delete from hello";
    private static final String SELECT = "select * from hello";
    
    public int mId;     //Column 1
    public String mMs;  //Column 2
    
    public String getCreateQuery() { return CREATE; }
    public String getInsertQuery() { return INSERT; }
    public String[] getInsertBindItem() { return new String[]{mMs}; }
    public String getSelectQuery() { return SELECT; }
    
    public void setData(Cursor c) {
        // TODO Auto-generated method stub
        
        mId = c.getInt(0);
        mMs = c.getString(1);
    }
    
    public HelloTable[] makeData(Cursor c) {
        
        //最初のデータを参照する
        c.moveToFirst();
        int cnt = c.getCount();
        if (cnt == 0) {
            
            c.close();
            return null;
        }
        
        HelloTable []q = new HelloTable[cnt];
        for (int i = 0; i < cnt; i++) {
            q[i] = new HelloTable();
            q[i].setData(c);
            c.moveToNext();
        }
        
        c.close();
        
        return q;
    }
}

ContentProviderクラス HelloProvider
ContentProviderを拡張したクラスです。AUTHORITYはプロジェクトAの完全なデータベース名になります。
この記述はAndroidManifestファイルにも同様に記述します。

package hoge.hoge.hatena.A;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.util.Log;

public class HelloProvider extends ContentProvider {

    private DBHelper mDb;
    public static final String AUTHORITY = "hoge.hoge.hatena.A.HELLO_DATABASE";
    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/");
    private static final String TAG = "HelloProvider";
    
    //ディレクトリのMIMEタイプ
    private static final String CONTENT_TYPE = "vnd.android.cursor.dir/" + AUTHORITY;
    
    @Override
    public int delete(Uri arg0, String arg1, String[] arg2) {
        // TODO Auto-generated method stub
        return 0;
    }

    @Override
    public String getType(Uri arg0) {
        // TODO Auto-generated method stub
        
        //ディレクトリのみアクセス
        return CONTENT_TYPE;
    }

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

    @Override
    public boolean onCreate() {
        
        Log.d(TAG, "onCreate");
        mDb = new DBHelper(getContext());
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {
        
        Log.d(TAG, "query");
        Log.d(TAG, uri.toString());
        
        HelloTable hello = new HelloTable();
        final SQLiteDatabase db = mDb.getReadableDatabase();
        Cursor c = null;
        c = db.rawQuery(hello.getSelectQuery(), null);
        if (c == null) {
            Log.d(TAG, "Cursor is null");
        }
        
        c.setNotificationUri(getContext().getContentResolver(), uri);
        
        return c;
    }

    @Override
    public int update(Uri arg0, ContentValues arg1, String arg2, String[] arg3) {
        // TODO Auto-generated method stub
        return 0;
    }

}

ArrayAdapterクラス HelloListAdapter
ListViewをカスタマイズする為に作成しました。各行のレイアウトについてはInflaterを使ってXMLレイアウトから読み込むのが定石のようですが、Inflaterは結構遅いらしいので今回は使っていません。

package hoge.hoge.hatena.A;

import android.content.Context;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.TableRow;
import android.widget.TextView;

public class HelloListAdapter extends ArrayAdapter<HelloTable> {

    private Context context;
    
    public HelloListAdapter(Context context, int textViewResourceId, HelloTable []items) {
        
        super(context, textViewResourceId);
        this.context = context;
        for (HelloTable h : items) {
            this.add(h);
        }
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        
        LinearLayout mLayout = null;
        TextView mText1 = null;
        TextView mText2 = null;
        View view = convertView;
        final HelloTable h = this.getItem(position);
        
        if (view == null) {
            
            //リストビューのレイアウトを作成
            //inflaterは遅いので・・・
            mLayout = new LinearLayout(context);
            mLayout.setId(1);
            TableRow.LayoutParams textParams = new TableRow.LayoutParams(
                    LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
            textParams.weight = 1;
            
            mText1 = new TextView(context);
            mText1.setLayoutParams(textParams);
            mText1.setGravity(Gravity.CENTER);
            mText1.setPadding(4, 2, 2, 2);
            mText1.setTextSize(20);
            mText1.setId(2);
            
            mText2 = new TextView(context);
            mText2.setLayoutParams(textParams);
            mText2.setGravity(Gravity.CENTER);
            mText1.setPadding(2, 2, 2, 4);
            mText2.setTextSize(20);
            mText2.setId(3);
            
            mLayout.addView(mText1);
            mLayout.addView(mText2);
            view = mLayout;
        } else {
            mLayout = (LinearLayout)view.findViewById(1);
            mText1 = (TextView)view.findViewById(2);
            mText2 = (TextView)view.findViewById(3);
        }
        
        if (mText1 != null) {
            mText1.setText("id " + h.mId);
        }
        if (mText2 != null) {
            mText2.setText(h.mMs);
        }
        
        return view;
    }
}

AndroidManifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
        android:versionCode="1"
        android:versionName="1.0"
        package="hoge.hoge.hatena.A">
    
    <application android:icon="@drawable/icon"
        android:label="@string/app_name"
        android:debuggable="true">
        
        <activity
            android:name=".Hello"
            android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    
        <provider
            android:name=".HelloProvider"
            android:authorities="hoge.hoge.hatena.A.HELLO_DATABASE">
        </provider>
    </application>
    <uses-sdk android:minSdkVersion="4" />
</manifest> 

main.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="@+id/ListView01"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent">
    </ListView>
</LinearLayout>

上記のコードをビルドして実行すると、次のうような画面になると思います。

続いて、プロジェクトAのデータベースを読み取るべく、プロジェクトBを作成します。
プロジェクトBでは、次のような機能を実装します。

ArryaAdapterクラスと、クエリー定義クラスはプロジェクトAのクラスをコピーして使います。

Activityクラス Hello

package hoge.hoge.hatena.B;

import android.app.Activity;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.ListView;

public class Hello extends Activity {
    /** Called when the activity is first created. */
    
    private String TAG = "Hello B";
    
    //アプリAのパッケージ名 + データベース名
    private static final String CONTENT =
        "content://hoge.hoge.hatena.A.HELLO_DATABASE/";
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Cursor c = null;
        ListView list = (ListView)this.findViewById(R.id.ListView01);
        
        //アプリAのContentProviderを使って、データを取得
        getIntent().setData(Uri.parse(CONTENT));
        c = managedQuery(getIntent().getData(), null, null,null,null);
        if (c != null) {
            
            HelloTable q = new HelloTable();
            HelloTable []bean = q.makeData(c);
            
            if (bean != null) {
                
                list.setAdapter(new HelloListAdapter(this, 0, bean));
            }
        } else {
            
            //データが存在しない場合
            Log.d(TAG, "result 0items");
            ArrayAdapter<String> ad = new ArrayAdapter<String>(
                    this, android.R.layout.simple_list_item_1);
            ad.add("No Data");
            list.setAdapter(ad);
        }
    }
}

AndroidManifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="hoge.hoge.hatena.B"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name" android:debuggable="true">
        <activity android:name=".Hello"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>
    <uses-sdk android:minSdkVersion="4" />
</manifest> 

main.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="@+id/ListView01"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent">
    </ListView>
</LinearLayout>

プロジェクトBを実行すると、次のようにプロジェクトAのデータベースの中身を一覧表示します。

随分とソースコードが長くなってしまいましたが、ContentProviderを使う上で参考になれば幸いです。

次回は、ContentProviderを使って特定のアプリケーションにのみ、データベースを公開する方法を紹介したいと思います。