App Component:Content Provider

投稿日:  更新日:

アプリを構成する最上位の構成要素がアプリケーションコンポーネント(App component)です。

Content Providerはアプリケーションコンポーネントの1つです。アプリケーションが管理するデータを公開(共有)します。

Content Providerについてまとめます。

スポンサーリンク

Content Providerとは

アプリは自分が管理するデータや関数に対して、他のアプリからのアクセスを許しません。

図にあるように、アプリAはアプリBからのアクセスを拒否します。

これは、アプリBが悪意のあるアプリだった場合に、アプリAのデータが改竄(かいざん)されたり、漏洩(ろうえい)して悪用されたりすることを防ぐためです。

このアクセスを拒否する仕組みをサンドボックス(Sandbox:砂場)と言います。

サンドボックスとプロバイダー(1)

逆に、データや関数を他のアプリと共有した方が有益な場合も存在します。

例えば、アドレス帳アプリです。

アドレス帳アプリが管理するアドレスDB(データベース)は、通話アプリで電話をかける相手を選択する時に役立ちます。

データを共有する例

共有ができれば有益なのに、サンドボックスがあるため出来ません。

ここで、登場するのがContent Providerです。

Content Providerはサンドボックスの壁を越えて、アプリが管理するデータを公開する環境を提供します。

図で言うと、アプリBはアプリAのProviderを経由してデータにアクセスできるようになります。

サンドボックスとプロバイダー(2)

ちなみに、データを公開(提供)する側をProviderといい、取得する側をResolverといいます。

スポンサーリンク

Content Provider雛形の作成

Android StudioでContent Providerの自動作成が可能です。

Providerの雛形
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の呼び出し

起動したいProviderの指定(Resolver側)

Contento Providerは接続したいProviderをURIで指定します。

このURIはProviderの識別名に加えて、アクセス対象のデータを指し示すパス(テーブル名とID)も付加できます。

URI全体のフォーマットは次の通りです。

フォーマット:
  content://<authority>[/<data>][/<num>]
パラメータ概要
prefix(scheme)content://”content://”に固定
authorityパッケージ名+Provider名Providerを特定する固有名
pathdataデータの種類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を経由する方法を取りました。

Content Providerの呼び出し例

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
スポンサーリンク

関連記事:

アプリを構成する最上位の構成要素がアプリケーションコンポーネント(App component)です。 アプリケーションコンポーネントの概要をまとめます。 ...
「インテントの解決」はシステム内でアプリケーションコンポーネントを選択する処理のことです。 この選択する処理の流れが複雑で、ちょっと癖がありあます。 ここでは「インテントの解決」の処理の流れについてまとめます。 ...
スポンサーリンク