ContentProvider利用側がテーブルや主キーを選り好み

こんにちは。

Androidアプリ同士は**何の施しもしていなければ**、1台の端末に同居してインストールされていても、お互いの持つデータにアクセスできません。

アプリ甲の持つデータを、アプリ乙がアクセスするには、色々と策はありますが、そのうちの一つにContentProviderがあります。ContentProviderは自アプリのデータアクセスを提供する仕組みです。ここでいう「データ」というのは、得てしてDBに格納されているデータのことを指します。

それでは、アプリ甲がDBで持つデータを、アプリ乙がアクセスできるようにするには、まず以下のような体制にします。

- アプリ甲は、ContentProviderを実装する。
- アプリ乙は、ContentResolverでアクセスする。

今回は、アプリ乙の方で、アクセスしたいアプリ甲の持つDBのテーブルを選り好みしたり、全件検索でぜんぶ寄こせ！だけでなく、主キーによる一件検索までしてみます。

# ContentProviderでデータアクセスを提供する側のアプリ甲

2つのテーブルを用意しました。いわゆる「部署と従業員」です。[主キー列名は例によって例のごとく「_id」です。](https://developer.android.com/reference/android/provider/BaseColumns.html#_ID)

detpテーブル

| _id       |name     |
|:------|:----------|
| 1 | 鎌倉本社 |
| 2 | 室町支社 |
| 3 | 江戸営業所 |

employeeテーブル

| _id | name |  dept_id  |
|:----|:---------------:|:-----|
| 101 |  源 頼朝 | 1 |
| 102 |  源 頼家 | 1 |
| 103 |  源 実朝 | 2 |
| 201 | 足利 尊氏 | 2 |

従業員はあと数名いるのですが、ここに書くのが面倒くさいので、忖度してください。

```java:MyContentProvider.java
package jp.co.casareal.providerside;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.provider.BaseColumns;
import android.util.Log;

import java.util.List;

public class MyContentProvider extends ContentProvider {
    /**
     * URIのauthority
     */
    private static final String AUTHORITY = "jp.co.casareal.providerside.provider";

    /**
     * 既定のURIパターンで絞り込む為のMatcher
     */
    private static final UriMatcher uriMatcher;
    static {
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI(AUTHORITY, "dept", 1);
        uriMatcher.addURI(AUTHORITY, "dept/#", 2);
        uriMatcher.addURI(AUTHORITY, "employee", 3);
        uriMatcher.addURI(AUTHORITY, "employee/#", 4);
    }

    /**
     * SQLiteOpenHelperのインスタンス.
     */
    private SQLiteOpenHelper openHelper;

    @Override
    public boolean onCreate() {
        openHelper = new DbOpenHelper(getContext());
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        SQLiteDatabase database = openHelper.getReadableDatabase();
        Cursor cursor = null;
        switch (uriMatcher.match(uri)) {
            case 1:
                Log.v(getClass().getName(), "case1:部署全件検索");
                cursor = database.query("dept", projection, selection, selectionArgs, null, null, sortOrder);
                break;
            case 2:
                Log.v(getClass().getName(), "case2:部署主キー検索");
                cursor = database.query("dept", projection, appendSelection(uri, selection),
                        appendSelectionArgs(uri, selectionArgs), null, null, sortOrder);
                break;
            case 3:
                // 略
                break;
            case 4:
                // 略
                break;
            default:
                throw new UnsupportedOperationException("URIが間違っています。");
        }
        return cursor;
    }

    /**
     * URIで検索条件があった場合、selectionにそれを連結して返す
     *
     * @param uri       URI
     * @param selection 絞り込み条件
     * @return 条件が連結されたselection
     */
    private String appendSelection(Uri uri, String selection) {
        List<String> pathSegments = uri.getPathSegments();
        if (pathSegments.size() == 1) {
            return selection;
        }
        return BaseColumns._ID + "=?" + (selection == null ? "" : " AND name=" + selection);
    }

    /**
     * URIで検索条件があった場合、selectionArgsにそれを連結して返す
     *
     * @param uri           URI
     * @param selectionArgs 絞り込み条件の引数
     * @return 条件が連結されたselectionArgs
     */
    private String[] appendSelectionArgs(Uri uri, String[] selectionArgs) {
        List<String> pathSegments = uri.getPathSegments();
        if (pathSegments.size() == 1) {
            return selectionArgs;
        }
        if (selectionArgs == null || selectionArgs.length == 0) {
            return new String[]{pathSegments.get(1)};
        }
        String[] returnArgs = new String[selectionArgs.length + 1];
        returnArgs[0] = pathSegments.get(1);
        System.arraycopy(selectionArgs, 0, returnArgs, 1, selectionArgs.length);
        return returnArgs;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public String getType(Uri uri) {
        return null;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        throw new UnsupportedOperationException("Not yet implemented");
    }
}
```

このContentProviderサブクラス、いろいろとコーディングするのが面倒くさかったので略してます、忖度してください。

## ポイント①UriMatcherを用意セヨ

上記のプログラムはクラス丸ごとなので、ポイントを掻い摘んでいきます。

UriMatcherはフィールドで宣言しましょう。かつ、`static { }`で初期化もしておきましょう。

```java:UriMatcherを用意しておく
String AUTHORITY = "jp.co.casareal.providerside.provider";
private static final UriMatcher uriMatcher;
static {
    uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    uriMatcher.addURI(AUTHORITY, "dept", 1);
    uriMatcher.addURI(AUTHORITY, "dept/#", 2);
    uriMatcher.addURI(AUTHORITY, "employee", 3);
    uriMatcher.addURI(AUTHORITY, "employee/#", 4);
}
```

感覚的に例えると、UriMatcherは「ケースごとのURIの列挙」というかんじ。私個人としては、enumで定義させてくれよー:tired_face:と思うのですが、addURIメソッドで追加していくスタイルです。

addURIメソッドの第2引数で、テーブル名や`#`を設定しておきます。

addURIメソッドの第3引数は任意で採番していいIDです。あとでこれを頼りにして使います。

## ポイント②URIのケース分けで応答セヨ

上記のMyContentProviderクラスの`query`メソッド内を見てください。

```java:switch文でケース振り分け
switch (uriMatcher.match(uri)) {
    case 1:
        // 部署全件検索
        cursor = database.query("dept", projection, selection,
        selectionArgs, null, null, sortOrder);
        break;
    case 2:
        // 部署主キー検索
        cursor = database.query("dept", projection, appendSelection(uri, selection),appendSelectionArgs(uri, selectionArgs), null, null, sortOrder);
        break;
}
```

UriMatcher#addURIメソッドの第3引数でswitchのcase振り分けとなります。

- "content://jp.co.casareal.providerside.provider/**dept**"なら`case 1:`
- "content://jp.co.casareal.providerside.provider/**dept/1**"なら`case 2:`
- "content://jp.co.casareal.providerside.provider/**employee**"なら`case 3:`
- "content://jp.co.casareal.providerside.provider/**employee/201**"なら`case 4:`

## ポイント③UriMatcherは、しょせんケースの振り分けだけだ！

ここが注意です。UriMatcherは、しょせんケースの振り分けのための橋渡しの役割しかしません。
何が言いたいのかというと、**主キーによる検索なのか、部署名や従業員名の曖昧検索なのか、などはUriMatcherの責務に非ず**なのです。

例えば、

```java:このURIは何を意味しているのか？
"content://jp.co.casareal.providerside.provider/employee/101/103/朝"
```

このURI、忖度するなら、「_idが100から103の間で（BETWEEN）、nameに'朝'が含まれている（LIKE）従業員」なのかなー、と思われます。

ともあれ、URIの書式はアプリ甲と乙の両者で約束が通っていればソレデイイのであって、その忖度アルゴリズムは開発者にお任せです。
だから、上記のMyContentProviderクラスでは、appendSelectionメソッドやappendSelectionArgsメソッドを自力で書き上げました。

## ポイント④そうそう、マニフェスト

作ったContentProviderサブクラスは、ちゃんとマニフェストに記載しないとネ。:point_up:

```xml:AndroidManifest.xml
<provider
    android:name=".MyContentProvider"
    android:authorities="jp.co.casareal.providerside.provider"
    android:exported="true"></provider>
```

# ContentResolverでデータアクセスする側のアプリ乙

```java:約束はちゃんと守ってね
String findAllDeptsUri = "content://jp.co.casareal.providerside.provider/dept";
String findDeptByIdUri = "content://jp.co.casareal.providerside.provider/dept/2";
// どちらかのURIでアクセスしてみてください
Uri uri = Uri.parse(findDeptByIdUri);
// ContentResolverでアクセス
ContentResolver contentResolver = getContentResolver();
cursor = contentResolver.query(uri, null, null, null, null);
```

そういえば、[ContentResolver](https://developer.android.com/reference/android/content/ContentResolver.html)のqueryメソッドには、テーブル名を渡す引数がないんですよね。

以上です。