ContentProvider で Stream を使って大きなデータをやりとりしよう

ContentResolver から openInputStream/openOutputStream を開くことが出来、これを利用すれば ContentProvider 経由で IPC に乗らない (だいたい 50KB 以上) データをやり取りすることが出来ます。

どういう風に ContentProvider を実装すれば Stream のやりとりが出来るようになるのか。僕は Android アプリのチュートリアルページから次のような部分を見付けました。

テーブルに投入するには大きすぎるようなバイトデータ ( 大きなビットマップファイルのような ) を公開する場合は、実際にそこにある content: URI を含め、クライアントにそのデータを公開するフィールドにすべきです。これはクライアントがアクセスするデータファイルを与えるフィールドになります。レコードには別のフィールドを持つことができ、その名前は "_data" で、このファイルに対するデバイス上での正確なファイルパスをリストします。このフィールドはクライアントが読み込むことを意図してあるわけではなく、 ContentResolver のためにあります。クライアントは、アイテムに対する URI を保持したこのユーザから見えるフィールドを使って、ContentResolver.openInputStream() を呼び出します。ContentResolver はこのレコードに対する"_data" フィールドを要求し、クライアントよりも高い許可を持っていることにより、そのファイルに直接アクセスすることができ、このファイル読み込み用ラッパをクライアントに返却します。

要約すると、「Stream を開きたい場合は "_data" というカラムを作って実パス放り込めば良い」ということになると思います。

なんだか、とっても簡単そうですね、と思って安易に作ると動きません。これは罠なのです。

openInputStream で URI を呼び出すと、 ContentProvider 側では openFile というメソッドが呼び出されます。

public ParcelFileDescriptor openFile (Uri uri, String mode)

しかし、このメソッドでは、 FileNotFoundException を投げるようにプラットフォーム側では実装されている。
そう、「"_data" に実パスを放り込む」なんて安易な方法は使えず、 openFile をオーバーライドして実装する必要があるのです。

まとめ

こうすれば良い。

@Override
public ParcelFileDescriptor openFile (Uri uri, String mode) {
    return openFileHelper(uri, mode);
}

"_data" を使って読み込む機構は、実は openFileHelper というメソッドで実装されていて、 openFile から明示的に繋ぎ込む必要がある。

openFileHelper を繋ぐ必要があるとか気付かねえよ。