アプリを構成する最上位の構成要素がアプリケーションコンポーネント(App component)です。
Content Providerはアプリケーションコンポーネントの1つです。アプリケーションが管理するデータを公開(共有)します。
Content Providerについてまとめます。
Content Providerとは
アプリは自分が管理するデータや関数に対して、他のアプリからのアクセスを許しません。
図にあるように、アプリAはアプリBからのアクセスを拒否します。
これは、アプリBが悪意のあるアプリだった場合に、アプリAのデータが改竄(かいざん)されたり、漏洩(ろうえい)して悪用されたりすることを防ぐためです。
このアクセスを拒否する仕組みをサンドボックス(Sandbox:砂場)と言います。
逆に、データや関数を他のアプリと共有した方が有益な場合も存在します。
例えば、アドレス帳アプリです。
アドレス帳アプリが管理するアドレスDB(データベース)は、通話アプリで電話をかける相手を選択する時に役立ちます。
共有ができれば有益なのに、サンドボックスがあるため出来ません。
ここで、登場するのがContent Providerです。
Content Providerはサンドボックスの壁を越えて、アプリが管理するデータを公開する環境を提供します。
図で言うと、アプリBはアプリAのProviderを経由してデータにアクセスできるようになります。
ちなみに、データを公開(提供)する側をProviderといい、取得する側をResolverといいます。
Content Provider雛形の作成
Android StudioでContent Providerの自動作成が可能です。
自動作成は次のことを行います。
(1)ContentProviderを継承した***Providerクラスを作成
(2)AndroidManifest.xmlへ***Providerを記述
※***:任意な名前
なお、作成時に指定するURI AuthoritiesはContent Providerを特定するための識別名です。固有な名前である必要があります。
ですので、「パッケージ名+プロバイダ名」とするのが普通です。
class KeywordProvider : ContentProvider() { override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int { // クライアントの要求に答えてDBのデータを削除 } override fun getType(uri: Uri): String? { // DBから取得されるデータのMIMEタイプを返す } override fun insert(uri: Uri, values: ContentValues?): Uri? { // クライアントの要求に答えてDBへデータを追加 } override fun onCreate(): Boolean { // Providerを初期化、開始時に一度だけ実行 } override fun query( uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String? ): Cursor? { // クライアントの要求に答えてDBからデータを取得 } override fun update( uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>? ): Int { // クライアントの要求に答えてDBのデータを更新 } }
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.example.myapp"> <application ...> <provider android:name=".KeywordProvider" android:authorities="com.example.myapp.keyword" android:enabled="true" android:exported="true"> </provider> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
Android Studioの自動作成を使わないのであれば、(1)~(2)を手作業で行ってください。
Content Providerの実装
実際のProviderはContentProvider抽象クラスを継承して各々の抽象関数を実装しなければなりません。
抽象関数は表のような処理を担います。
抽象関数 | 実装内容 | 戻り値 |
---|---|---|
onCreate( ) | Providerを初期化 開始時に一度だけ実行 | Boolean true:初期化成功 false:初期化失敗 |
insert( uri, ... ) | DBへデータを追加 | Uri 追加されたデータのUri |
delete( uri, ... ) | DBのデータを削除 | Int 削除されたデータ数 |
update( uri, ... ) | DBのデータを更新 | Int 変更されたデータ数 |
query( uri, ... ) | DBのデータを要求(取得) | Cursor |
getType( uri, ... ) | データのMIMEタイプを返す | String MIMEタイプ |
関数名を見れば分かるように、DBへアクセスする関数が並んでいます。
Andoridはデータを管理する仕組みに、標準でDB(データベース、SQLite)を用いることになっています。ですので、Providerが公開するデータはDBです。ただし、あくまで「標準」であって、強制ではありあません。
コンポーネントの連携
端末内でコンポーネント(ここではContent Providerのこと)は連携して動作します。「連携」とはコンポーネントが互いに呼び出し合いながら協調して動くという意味です。
この「呼び出し」はアプリ内のコンポーネントに限りません。アプリ外も呼び出しの対象になります。
「連携」を行うことで、アプリの機能が容易に拡充できます。
※詳細は「アプリケーションコンポーネント(App component)」を参照
呼び出しの許可・拒否(Provider側)
コンポーネントの呼び出しを許可・拒否する仕組みがあります。
セキュリティ確保のため、またはアプリ自身の都合のため、呼び出されたくないコンポーネントがあるからです。
許可・拒否はマニフェストに記述するEnabledとExported属性で行います。
属性の基本的な動作は表の通りです。
Enabled | インスタンス化 | Exported | アプリ内から 呼出し | アプリ外から 呼出し |
---|---|---|---|---|
true | ○ (記述なしの場合) | true | ○ | ○ |
false | ○ | ×(※c) | ||
false | × | true | × (インスタンス化できないので全てにおいて拒否) |
|
false | ||||
※○:許可、×:拒否 ※c:SecurityExceptionにより強制停止 |
Exported=falseのProviderを無理やり呼び出そうとすると、SecurityExceptionになるので注意してください。
java.lang.SecurityException: Permission Denial: opening provider 起動先コンポーネント from ProcessRecord{起動元コンポーネント} (pid=xxx, uid=起動元アプリのUID) that is not exported from UID 起動先コンポーネントのUID
呼び出しの関数(Resolver側)
Providerを呼び出す関数はResolverが持っています。
戻り値 | 関数 | タイプ | アプリ内から 呼び出し | アプリ外から 呼び出し |
---|---|---|---|---|
Uri 追加データのURI | ContentResolver #insert( uri, ... } | URI | ||
Int 削除データの数 | ContentResolver #delete( uri, ... } |
|||
Int 更新データの数 | ContentResolver #update( uri, ... } |
|||
Cursor 取得データのCursor | ContentResolver #query( uri, ... } |
|||
String データのMIMEタイプ | ContentResolver #getType( uri } |
|||
※○:可能、×:不可能 ※Enabled/Exportedにより呼び出し許可の場合 |
Providerで実装した関数と同じ使い方である点に注目してください。
内部的に行われているのは、AndroidのBinderと呼ばれるプロセス間通信(IPC:Inter Process Communication)で、サンドボックスを超えてリモート関数コールが行われる仕組みになっています。
これにより、Resolverの関数を実行する事は、Providerの関数を実行する事と、等価です。
起動したいProviderの指定(Resolver側)
Contento Providerは接続したいProviderをURIで指定します。
このURIはProviderの識別名に加えて、アクセス対象のデータを指し示すパス(テーブル名とID)も付加できます。
URI全体のフォーマットは次の通りです。
フォーマット: content://<authority>[/<data>][/<num>]
パラメータ | 値 | 概要 | |
---|---|---|---|
prefix(scheme) | content:// | ”content://”に固定 | |
authority | パッケージ名+Provider名 | Providerを特定する固有名 | |
path | data | データの種類 | DBのテーブル名 文字列ワイルドカード「*」が利用可能 |
num | データの番号 | DBの_id 数値ワイルドカード「#」が利用可能 |
例: content://com.example.myapp.keyword/connect ⇒keyword Providerが管理するDBのconnectテーブル content://com.example.myapp.keyword/connect/1 ⇒keyword Providerが管理するDBのconnectテーブルのID=1レコード content://com.example.myapp.keyword/connect/3 ⇒keyword Providerが管理するDBのconnectテーブルのID=3レコード content://com.example.myapp.keyword/connect/# ⇒keyword Providerが管理するDBのconnectテーブルの全レコード content://com.example.myapp.keyword/connect/sample1 ⇒keyword Providerが管理するDBのsample1テーブル content://com.example.myapp.keyword/connect/sample2 ⇒keyword Providerが管理するDBのsample2テーブル content://com.example.myapp.keyword/connect/* ⇒keyword Providerが管理するDBのsample1とsample2テーブル
アクセス対象データの判別(Provider側)
Providerの関数の引数Uriは、Resolverで指定したURIがそのまま送られてきます。
ProviderはURIの構成を解釈してアクセス対象のデータを判別し、データに見合った処理を選択します。
このURIの構成を解釈する時、プロバイダAPIのUriMatcherクラスが有効です。
UriMatcherは指定したパターンにマッチしたURIへ整数値をマッピングします。これを使えば、マッピングされた整数値を分岐条件にして処理を選択できます。
private const val CURSOR_DIR = 1 private const val CURSOR_ITEM = 2 class XXXProvider : ContentProvider() { companion object { const val AUTHORITY = "com.example.myapp.keyword" const val DB_TABLE = "connect" } private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply { // "content://com.example.myapp.keyword/connect"にマッチ⇒CURSOR_DIRを返す addURI(AUTHORITY, DB_TABLE, CURSOR_DIR) // "content://com.example.myapp.keyword/connect/1"にマッチ⇒CURSOR_ITENを返す // "content://com.example.myapp.keyword/connect/3"にマッチ⇒CURSOR_ITENを返す addURI(AUTHORITY, "${DB_TABLE}/#", CURSOR_ITEM) } override fun query( uri: Uri, ... ): Cursor? { return when(uriMatcher.match(uri)) { // 目的の処理を振り分け CURSOR_DIR -> { // テーブルに対する処理 // } CURSOR_ITEM -> { // レコードに対する処理 // } else -> { null} } } }
呼び出し例
Aアプリが管理するデータ(Keyword DB)に対して、Aアプリ(アプリ内)からアクセスする場合と、Bアプリ(アプリ外)からアクセスする場合の例です。
アプリ内からであれば直接的にデータへアクセス出来ますが、今回はProvider+Resolverを経由する方法を取りました。
Provider(データの公開)
アプリの管理するデータはKeywordDB(SQLite DB)です。
“key”と対応する”word”のペアをレコードに持つ単純なデータベースです。連想配列(ハッシュ配列)と変わりません。
class KeywordDB (private val context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) { companion object { const val DB_NAME = "Keyword.db" const val DB_TABLE = "connect" const val DB_VERSION = 1 } override fun onCreate(db: SQLiteDatabase) { val _buffer = StringBuffer() _buffer.append("create table ") _buffer.append(DB_TABLE) _buffer.append(" (") _buffer.append("_id integer primary key autoincrement,") _buffer.append("key text not null,") _buffer.append("word text not null") _buffer.append(")") db.execSQL(_buffer.toString()) } override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { db.execSQL("drop table if exists %s".format(DB_TABLE)) onCreate(db) } }
ProviderKeywordがデータの公開を行うProviderクラスです。ContentProvider抽象クラスを継承して作ります。
KeywordDBのデータベースの追加・削除・更新・取得処理がProviderクラスの抽象関数に実装されています。
これらの関数がResolverからの呼び出しに応じて実行(リモート関数コール)されることになります。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.myapp_a"> <application ...> <provider android:name=".KeywordProvider" android:authorities="com.example.myapp_a.keyword" android:enabled="true" android:exported="true"> </provider> <activity> android:name=".MainActivity" ... </activity> </application> </manifest>
import com.example.myapp_a.KeywordDB.Companion.DB_TABLE private const val CURSOR_DIR = 1 private const val CURSOR_ITEM = 2 class KeywordProvider : ContentProvider() { companion object { const val AUTHORITY = "com.example.myapp_a.keyword" const val URI = "content://${AUTHORITY}/${DB_TABLE}" } private lateinit var db: SQLiteDatabase private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply { addURI(AUTHORITY, DB_TABLE, CURSOR_DIR) addURI(AUTHORITY, "${DB_TABLE}/#", CURSOR_ITEM) } override fun onCreate(): Boolean { db = KeywordDB(context!!).writableDatabase return true } override fun insert(uri: Uri, values: ContentValues?): Uri? { // 追加 val _retUri = when(uriMatcher.match(uri)) { CURSOR_DIR -> { val _id = db.insert(DB_TABLE, null, values) ContentUris.withAppendedId(uri, _id) } CURSOR_ITEM -> null else -> null } return _retUri } override fun delete( // 削除 uri: Uri, selection: String?, selectionArgs: Array<String>? ): Int { val _num = when(uriMatcher.match(uri)) { CURSOR_DIR -> { db.delete(DB_TABLE, selection, selectionArgs) } CURSOR_ITEM -> { val _id = uri.getLastPathSegment() db.delete(DB_TABLE, "_ID = ?", arrayOf(_id)) } else -> 0 } return _num } override fun update( // 更新 uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>? ): Int { val _num = when(uriMatcher.match(uri)) { CURSOR_DIR -> { db.update(DB_TABLE, values, selection, selectionArgs) } CURSOR_ITEM -> { val _id = uri.getLastPathSegment() db.update(DB_TABLE, values, "_ID = ?", arrayOf(_id)) } else -> 0 } return _num } override fun query( // 取得 uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String? ): Cursor? { val _cursor = when(uriMatcher.match(uri)) { CURSOR_DIR -> { db.query(DB_TABLE, projection, selection, selectionArgs, null, null, sortOrder) } CURSOR_ITEM -> { val _id = uri.getLastPathSegment() db.query(DB_TABLE, projection, "_ID = ?", arrayOf(_id), null, null, sortOrder) } else -> { null} } return _cursor } override fun getType(uri: Uri): String? { return when(uriMatcher.match(uri)) { CURSOR_DIR -> "vnd.android.cursor.dir/vnd.${AUTHORITY}.${DB_TABLE}" CURSOR_ITEM -> "vnd.android.cursor.item/vnd.${AUTHORITY}.${DB_TABLE}" else -> null } } }
Resolver(データの取得)
起動したいProviderのURIを引数に指定し、Resolverの呼び出し関数を実行すれば、目的のProviderが起動します。
Aアプリ(アプリ内)からアクセスする場合も、Bアプリからアクセスする場合も、呼び出し関数の記述は変わりません。ですので、例はBアプリからアクセスする場合のみを紹介しています。
query:DBのデータを要求(取得)
private const val URI_B = "content://com.example.myapp_b.keyword/connect" ... { val _recAll = this@MainActivity.contentResolver.query( // 取得 Uri.parse(URI_B), arrayOf("_id", "key", "word"), null, null, null ) ... }
B Query Cursor = _id:1,key:ひらけ,word:ごま(B) _id:2,key:あした,word:天気になあれ(B) _id:3,key:三回まわって,word:ワン!(B)
private const val URI_B = "content://com.example.myapp_b.keyword/connect" ... { val _id = 2 val _recAll = contentResolver.query( Uri.parse("${URI_B}/${_id}"), arrayOf("_id", "key", "word"), null, null, null ) ... }
B Query Cursor = _id:2,key:あした,word:天気になあれ(B)
insert:DBへデータを追加
private const val URI_B = "content://com.example.myapp_b.keyword/connect" ... { val _rstUri = this@MainActivity.contentResolver.insert( // 追加 Uri.parse(URI_B), ContentValues().apply { put("key", "へのへの") put("word", "もへじ") } ) ... }
B Insert Uri = content://com.example.myapp_b.keyword/connect/4 B Query Cursor = _id:1,key:ひらけ,word:ごま(B) _id:2,key:あした,word:天気になあれ(B) _id:3,key:三回まわって,word:ワン!(B) _id:4,key:へのへの,word:もへじ
delete:DBのデータを削除
private const val URI_B = "content://com.example.myapp_b.keyword/connect" ... { val _num = this@MainActivity.contentResolver.delete( // 削除 Uri.parse(URI_B), "key = ?", arrayOf("あした") ) ... }
private const val URI_B = "content://com.example.myapp_b.keyword/connect" ... { val _id = 2 val _num = this@MainActivity.contentResolver.delete( // 削除 Uri.parse("${URI_B}/${_id}"), null, null ) ... }
B Delete Num = 1 B Query Cursor = _id:1,key:ひらけ,word:ごま(B) _id:3,key:三回まわって,word:ワン!(B) _id:4,key:へのへの,word:もへじ
update:DBのデータを更新
private const val URI_B = "content://com.example.myapp_b.keyword/connect" ... { val _num = this@MainActivity.contentResolver.update( // 更新 Uri.parse(URI_B), ContentValues().apply { put("key", "ちちん") put("word", "ぷいぷい") }, "key = ?", arrayOf("ひらけ") ) ... }
private const val URI_B = "content://com.example.myapp_b.keyword/connect" ... { val _id = 1 val _num = this@MainActivity.contentResolver.update( // 更新 Uri.parse("${URI_B}/${_id}"), ContentValues().apply { put("key", "ちちん") put("word", "ぷいぷい") }, null, null ) ... }
B Update Num = 1 B Query Cursor = _id:1,key:ちちん,word:ぷいぷい _id:3,key:三回まわって,word:ワン!(B) _id:4,key:へのへの,word:もへじ
getType:データのMIMEタイプを返す
private const val URI_B = "content://com.example.myapp_b.keyword/connect" ... { val _type = this@MainActivity.contentResolver.getType(Uri.parse(URI_B)) ... }
B Type = vnd.android.cursor.dir/vnd.com.example.myapp_b.keyword.connect
private const val URI_B = "content://com.example.myapp_b.keyword/connect" ... { val _id = 1 val _type = this@MainActivity.contentResolver.getType(Uri.parse("${URI_B}/${_id}")) ... }
B Type = vnd.android.cursor.item/vnd.com.example.myapp_b.keyword.connect
関連記事: