ContentProviderを使って別のアプリ間でデータをやりとりする

2023/02/06追記: 以下の方法は別のアプリ間でデータをやりとりできますが、それはapkの署名が同じである場合に限るようです。
異なる署名間のアプリでは、この方法だけではContentProviderのデータを読み取ることができませんでした。
セキュリティ的にはそりゃそうだよね〜という感じです。

データを提供する側のアプリ: ContentProviderを実装
データを読み取る側のアプリ: ContentResolverのAPIを呼び出す

## データを提供する側

```xml
<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    >

    <!-- 自分のContentProviderにアクセスさせるための権限を宣言する -->
    <permission android:name="com.example.contentproviertest.READ_DATA" />

    <application>
        <!-- 自分のContentProviderの宣言を追加 -->
        <provider
            android:name=".MyContentProvider"
            android:authorities="com.example.contentprovidertest.myprovider"
            android:enabled="true"
            android:exported="true"
            android:readPermission="com.example.contentproviertest.READ_DATA"
            />
    </application>
</manifest>
```

### ポイント

- ContentProvider実装クラスをAndroidManifest.xmlに宣言する
- `<provider>`
    - name: クラス名(`.`で始めた場合はパッケージ名からの相対名)
    - authorities: オーソリティ名。他のアプリと被ってはいけない。パッケージ名のように、ドメイン名の逆順とかで付ける
    - readPermission: `<permission>`で宣言した名前と揃える

ContentProviderの実装は以下のような感じになります。

```kotlin:com.example.contentprovidertest.MyContentProvider.kt
class MyContentProvider : ContentProvider() {
    override fun onCreate(): Boolean {
        return true
    }

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
        return 0 // TODO 未実装
    }

    override fun getType(uri: Uri): String? {
        return null // TODO 未実装(nullのままでもデータの取得には問題ない)
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        return null // TODO 未実装(外からデータを読み取るだけならこのままでも問題ない)
    }

    override fun update(
        uri: Uri,
        values: ContentValues?,
        selection: String?,
        selectionArgs: Array<String>?
    ): Int {
        return 0 // TODO 未実装(外からデータを読み取るだけならこのままでも問題ない)
    }

    override fun query(
        uri: Uri,
        projection: Array<String>?,
        selection: String?,
        selectionArgs: Array<String>?,
        sortOrder: String?
    ): Cursor {
        // TODO なんらかの方法でデータを取得し、Cursor型として返却する。
        // データがSQLiteテーブルにあるなら引数Cursorをそのまま返す。
        // データがSQLiteテーブルではないなら、MatrixCursorを使って擬似的なテーブルのような構造でデータを返す。
        // ここでは固定の値を返すサンプル
        return MatrixCursor(
            arrayOf("name", "address"),
        ).apply {
            addRow(arrayOf("Taro", "Tokyo"))
            addRow(arrayOf("Yuka", "Okinawa"))
        }
    }
}
```

このContentProviderにデータをクエリすると、以下のようなテーブル(のような構造)が返却されるはずです。

name | address
---|---
Taro | Tokyo
Yuka | Okinawa

## データを読み取る側

ContentProviderのデータを利用する側のAndroidManifestに、「このContentProviderを使いますよ」という宣言を入れる。

```xml
<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- ContentProvider側の <permission> と同じ名前を <uses-permission> で宣言 -->
    <uses-permission android:name="com.example.contentproviertest.READ_DATA" />

    <queries>
        <!-- ContentProvider側の <provider> の authorities と同じものを指定 -->
        <provider android:authorities="com.example.contentprovidertest.myprovider" />
    </queries>

    <application>
        <!-- 以下、省略 -->
    </application>
</manifest>
```

### ポイント

- ContentProvider側で指定している`<permission>`を`<uses-permission>`に書く
- ContentProvider側で指定している`<provider>`の`android:authorities`を`<queries>`内の`<provider android:authorities>`に書く

AndroidManifestの定義がきちんとできていれば、別のアプリからContentProviderのデータをクエリできます。その際、`content://${authorities}`という形式のURIを使用して`ContentResolver.query()`メソッドを呼びます。

```kotlin
val authority = "com.example.contentprovidertest.myprovider"
val uri = "content://$authority"
context.contentResolver.query(Uri.parse(uri), null, null, null, null)?.use { cursor ->
  val nameIndex = cursor.getColumnIndex("name")
  val addressIndex = cursor.getColumnIndex("address")
  while (cursor.moveToNext()) {
    val name = cursor.getString(nameIndex)
    val address = cursor.getString(addressIndex)
  }
}
```

---

以上、ContentProviderを使ったアプリ間のデータ共有方法のすごくシンプルな実装を紹介しました。
最低限の説明のために非常にシンプルな呼び出し方を使用しましたが、実際にはデータを識別するためにURIの末尾にパスを指定したり、`query()`メソッドの残りの引数に欲しいカラム名や条件を絞り込むためのクエリ文字列を入れたりすることが可能です。