2012/08/30

Android:CancellationSignalで処理を中断する

●はじめに

JellyBeanで"処理のキャンセル要求"を表現するCancellationSignalクラスが追加されました。

下記のようなシーンで利用されています。
  • ローダの処理を中断する。 
  • データベースへのクエリ要求を中断する。
「前者はLoaderのcancelLoadと違うのか?」って疑問を持つ方もいると思います。

ローダについては、CancellationSignalの仕組みを取り込む形で内部設計が変更されました。
この変更より、ローダ利用側がしないといけないことは特にありません。

一般的に利用されるシーンとしては、後者の"データベースへのクエリ要求の中断"でしょう。
例えば、下記のような利用が考えられます。
  • クエリを発行したものの、結果返却される前にアプリ終了したのでクエリをキャンセルしたい。
  • クエリを発行したものの、(ロック解放待ち等により)一定時間応答がないのでクエリをキャンセルしたい。
これまでは、一度発行したクエリは投げっぱなしで、中断する術がありませんでした。

(ContentProvider側をゴリゴリカスタムすればできそうですけど...)
JellyBean以降、CancellationSignalが導入されたことでこれが可能となりました。

●CancellationSignal概念

CancellationSignalは"処理の中断要求"を表現するクラスです。
このクラスは、キャンセル状態の管理とリスナーへのコールバックを持つシンプルなものです。
CancellationSignalはあくまでもキャンセル"要求"です。
このキャンセル要求を受け入れるかどうかは、キャンセルされる処理側が判断します。

これは、AsyncTaskのそれと変わりありません。
AsyncTaskもキャンセルメソッドを持っていますが、本当にキャンセルされるかどうかは実装依存です。

独自のローダやContentProviderにキャンセル機能を実現したい場合でも同じです。
JB以前に作成したContentProviderにキャンセル要求を投げても上手く動きません。
キャンセル機能を実現したいならそれを組み込む必要があります。

データベースのクエリ要求では、少なくとも次のポイントでキャンセルできるよう実装されています。
このケースでは少なくとも次のポイントでキャンセル要求を確認しています。
  • ContentResolver/ContentProviderClientへのクエリ要求時 
  • データベースへの接続前と接続待ち中
  • クエリの実行前と実行中 
  • トランザクションの開始時と終了時
注意点として、標準で用意されているContentProviderでもキャンセル機能をサポートしていないものが存在します。
例えば通話履歴データを提供するCallLogProviderはこれをサポートしていません。
このようなContentProviderにもCancellationSignalを渡すことは可能ですが、結果無視されます。

●"キャンセル要求があるか?"

「キャンセル要求があるか?」を判断するには2通りの方法があります。
CancellationSignal.isCanceled()throwIfCanceled()です。
isCanceled()はキャンセル要求の有無(true/false)を返します。
throwIfCanceled()はキャンセルされている場合にandroid.os.OperationCanceledException(実行時例外)をスローします。

キャンセル要求をチェック/実行する側はthrowIfCanceled()を使用するケースが多いようです。
処理をキャンセルしたい場合は呼び出しもとにOperationCanceledExceptionをそのままスローします。
キャンセル要求を投げる側はisCanceled()を使うことがほとんどでしょう。

●CancellationSignalによるキャンセル要求をサポートする

ここでは、独自ContentProviderにCancellationSignalによるキャンセルをサポートさせてみます。
実装は非常に簡単。

まず、ContentProviderを継承したクラスで下記のqueryメソッドをオーバライドします。
ContentProvider.query (Uri, String[], String, String[], String, CancellationSignal)

public class MyContentProvider extends ContentProvider {
    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder,
            CancellationSignal cancellationSignal) {
        ... 略 ...
    }
}

続いて、SQLiteDatabaseインスタンスに対して下記メソッド経由でCancellationSignalインスタンスを渡します。
SQLiteDatabase.query (boolean, String, String[], String, String[], String , String, String, String, CancellationSignal)

@Override
public Cursor query(Uri uri, String[] projection, String selection,
        String[] selectionArgs, String sortOrder,
        CancellationSignal cancellationSignal) {
    SQLiteDatabase db = mDataBaseHelper.getReadableDatabase();
    Cursor c = db.query(false, "test", projection,
            selection, selectionArgs, null, null, sortOrder, null,
            cancellationSignal);
    ... 略 ...
}

ContentProviderの呼出し元はContentResolverあるはContentProviderClient経由でCancellationSignalインスタンスを渡します。

・ContentResolver.query (Uri, String[], String, String[], String, CancellationSignal)
・ContentProviderClient.query (Uri, String[], String, String[], String, CancellationSignal)
mCancelSignal = new CancellationSignal();
Cursor cursor = getContentResolver().query(
        Uri.parse("content://yuki.mycontentprovider"),
        new String[] { "_id" }, null, null, null, mCancelSignal);
これで、SQLiteデータベースへのクエリ要求はキャンセル可能なものとして扱われます。
クエリ要求をキャンセルしたい場合は渡したCancellationSignalのcancelメソッドを呼び出します。
mCancelSignal.cancel();
キャンセル要求が受け入れられて処理中断されるとOperationCanceledException例外がスローされるのでこれをキャッチしましょう。
try {
    Cursor cursor = getContentResolver().query(
            Uri.parse("content://yuki.mycontentprovider"),
            new String[] { "_id" }, null, null, null, mCancelSignal);
} catch (OperationCanceledException ex) {
    // do something.
}
キャンセル要求のサポートは以上です。

ちなみにですが、AsyncTaskLoaderはCancellationSignalによるキャンセルをサポートしています。
キャンセル可能な範囲はバックグラウンド処理部分。
ちょっと気になるコメントがあったので引用します。
http://tools.oesf.biz/android-4.1.1_r1.0/xref/frameworks/base/core/java/android/content/AsyncTaskLoader.java#71

} catch (OperationCanceledException ex) {
    if (!isCancelled()) {
        // onLoadInBackground threw a canceled exception spuriously.
        // This is problematic because it means that the LoaderManager did not
        // cancel the Loader itself and still expects to receive a result.
        // Additionally, the Loader's own state will not have been updated to
        // reflect the fact that the task was being canceled.
        // So we treat this case as an unhandled exception.
        throw ex;
    }
CancellationSignalによるキャンセルが実行されたのに、Loaderとしてはキャンセル状態にない場合。
AsyncTaskLoaderはこれを異常として例外を再スローするようですね。

●キャンセル処理をサポートしていないContentProviderにCancellationSignalを渡したら...

クエリのキャンセルをサポートしていない or 古いContentProviderでもCancellationSignalを渡すことは可能です。
しかし、渡しても即破棄されるのでキャンセル要求は無視されます。

http://tools.oesf.biz/android-4.1.1_r1.0/xref/frameworks/base/core/java/android/content/ContentProvider.java#649
public Cursor query(Uri uri, String[] projection,
        String selection, String[] selectionArgs, String sortOrder,
        CancellationSignal cancellationSignal) {
    return query(uri, projection, selection, selectionArgs, sortOrder);
}


●お試し実装で...

以下はCancellationSignalの効果を確かめようと思って実施した内容です。
蛇足ではありますが、一部落とし穴があったので記載します。

キャンセル要求が本当に通るかを検証すべく、
Exclusiveロックされたデータベースにクエリを投げて、ロック解除待ち状態のクエリをキャンセルしてみようと思いました。

で、手軽にロックを掛けたかったのでsqlite3コマンドから直接
sqlite3> BEGIN EXCLUSIVE TRANSACTION;
としたのですが、これが誤り...

sqlite3コマンドから直接データベースを操作すると、期待通りの動作となりませんでした。
# ずっとロック解放待ちのままハマってしまう...

ちゃんとSQLiteDatabaseのbeginTransactionでロックを取得すれば上手くいきました。

----------
-------
---
以降はCancellationSignal調査中のメモ書き。あまり役に立たないネタです。
(といいつつ、キャンセル処理を自力で実装したい場合は参考になります)
「どうやってSQLiteデータベースはクエリを中断しているのか?」を追いました。

おおまかなシーケンスは下記(一部省略)。



下記はデータベースにクエリを発行した際のシーケンスを順に追っています。

1. SQLiteDatabaseに対してqueryを発行
android.database.sqlite.SQLiteDatabase.query(boolean, String, String[], String, String[], String, String, String, String, CancellationSignal)

2. query発行処理...
android.database.sqlite.SQLiteDatabase.queryWithFactory(CursorFactory, boolean, String, String[], String, String[], String, String, String, String, CancellationSignal)

3. SQLiteDirectCursorDriverのインスタンス化
android.database.sqlite.SQLiteDirectCursorDriver.SQLiteDirectCursorDriver(SQLiteDatabase, String, String, CancellationSignal)
SQLiteDirectCursorDriverはCancellationSignalをキャッシュする。
public SQLiteDirectCursorDriver(SQLiteDatabase db, String sql, String editTable,
        CancellationSignal cancellationSignal) {
    mDatabase = db;
    mEditTable = editTable;
    mSql = sql;
    mCancellationSignal = cancellationSignal;
}

4. SQLiteQueryとSQLiteCursorの生成
android.database.sqlite.SQLiteDirectCursorDriver.query(CursorFactory, String[])
public Cursor query(CursorFactory factory, String[] selectionArgs) {
    final SQLiteQuery query = new SQLiteQuery(mDatabase, mSql, mCancellationSignal);
    ... 略 ...
        cursor = new SQLiteCursor(this, mEditTable, query);
    ... 略 ...
}

5. SQLiteQueryはCancellationSignalをキャッシュする
android.database.sqlite.SQLiteQuery.SQLiteQuery(SQLiteDatabase, String, CancellationSignal)
SQLiteQuery(SQLiteDatabase db, String query, CancellationSignal cancellationSignal) {
    super(db, query, null, cancellationSignal);
    mCancellationSignal = cancellationSignal;
}

6. SQLiteQueryは親クラスSQLiteProgramのコンストラクタを呼び出す
android.database.sqlite.SQLiteProgram.SQLiteProgram(SQLiteDatabase, String, Object[], CancellationSignal)
クエリ操作では、SQLiteSessionの準備を行う。
int n = DatabaseUtils.getSqlStatementType(mSql);
switch (n) {
    default:
        ... 略 ...
        db.getThreadSession().prepare(mSql,
                db.getThreadDefaultConnectionFlags(assumeReadOnly),
                cancellationSignalForPrepare, info);
        ... 略 ...
        break;
}

7. データベースコネクションの取得を開始
android.database.sqlite.SQLiteSession.prepare(String, int, CancellationSignal, SQLiteStatementInfo)
prepare処理前にキャンセル確認。
public void prepare(String sql, int connectionFlags, CancellationSignal cancellationSignal,
        SQLiteStatementInfo outStatementInfo) {
    ... 略 ...
    if (cancellationSignal != null) {
        cancellationSignal.throwIfCanceled();
    }
    ... 略 ...
}
さらに、SQLiteConnectionを取得
acquireConnection(sql, connectionFlags, cancellationSignal); // might throw
try {
    mConnection.prepare(sql, outStatementInfo); // might throw
} finally {
    releaseConnection(); // might throw
}

8. コネクションプールにSQLiteConnection取得要求
android.database.sqlite.SQLiteConnectionPool.acquireConnection(String, int, CancellationSignal)

9. コネクションプールからSQLiteConnection取得できるまでウェイト
android.database.sqlite.SQLiteConnectionPool.waitForConnection(String, int, CancellationSignal)
ウェイト前にキャンセル確認
private SQLiteConnection waitForConnection(String sql, int connectionFlags,
        CancellationSignal cancellationSignal) {
        ... 略 ...
        // Abort if canceled.
        if (cancellationSignal != null) {
            cancellationSignal.throwIfCanceled();
        }
        ... 略 ...
}
ウェイト時にもキャンセルできるようにキャンセルリスナー前準備
cancellationSignal.setOnCancelListener(new CancellationSignal.OnCancelListener() {
@Override
public void onCancel() {
        synchronized (mLock) {
            if (waiter.mNonce == nonce) {
                cancelConnectionWaiterLocked(waiter);
            }
        }
});
ウェイト開始...
for (;;) {
    ... 略 ...
    // Wait to be unparked (may already have happened), a timeout, or interruption.
    LockSupport.parkNanos(this, busyTimeoutMillis * 1000000L);
    ... 略 ...
}
ウェイトが満了したらキャンセルリスナー解除
try {
    ... 略 ....
} finally {
    // Remove the cancellation listener.
    if (cancellationSignal != null) {
        cancellationSignal.setOnCancelListener(null);
    }
}

10. コネクションが取得できたらCursorWindowの準備
android.database.sqlite.SQLiteConnection.executeForCursorWindow(String, Object[], CursorWindow, int, int, boolean, CancellationSignal)
ネイティブへの問い合わせがあるので、ネイティブ実行中でもキャンセルできるよう前準備
try {
    final PreparedStatement statement = acquirePreparedStatement(sql);
        ... 略 ...
        attachCancellationSignal(cancellationSignal);
        try {
            final long result = nativeExecuteForCursorWindow(...);
            ... 略 ...
        } finally {
            detachCancellationSignal(cancellationSignal);
        }
        ... 略 ...
} catch (RuntimeException ex) {
                ... 略 ...

11. ネイティブ処理のキャンセルシグナルをアタッチ
android.database.sqlite.SQLiteConnection.attachCancellationSignal(CancellationSignal)
if (cancellationSignal != null) {
    cancellationSignal.throwIfCanceled();
    ... 略 ...
        // Reset cancellation flag before executing the statement.
        nativeResetCancel(mConnectionPtr, true /*cancelable*/);

        // After this point, onCancel() may be called concurrently.
        cancellationSignal.setOnCancelListener(this);
    ... 略 ...
}

ちなみに、SQLiteConnectionはCancellationSignal.OnCancelListenerをimplementsしている。
// CancellationSignal.OnCancelListener callback.
// This method may be called on a different thread than the executing statement.
// However, it will only be called between calls to attachCancellationSignal and
// detachCancellationSignal, while a statement is executing.  We can safely assume
// that the SQLite connection is still alive.
@Override
public void onCancel() {
    nativeCancel(mConnectionPtr);
}

12. キャンセルシグナルのデタッチ
android.database.sqlite.SQLiteConnection.detachCancellationSignal(CancellationSignal)
private void detachCancellationSignal(CancellationSignal cancellationSignal) {
    ... 略 ...
            // After this point, onCancel() cannot be called concurrently.
            cancellationSignal.setOnCancelListener(null);

            // Reset cancellation flag after executing the statement.
            nativeResetCancel(mConnectionPtr, false /*cancelable*/);
    ... 略 ...
}

13. SQLiteCursorはCancellationSignalをキャッシュしたSQLiteQueryをキャッシュする
android.database.sqlite.SQLiteCursor.SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)

これで、生成されたCursorがSQLiteDatabase.query()の戻り値になります。
他にもトランザクションの開始・終了時なんかにもキャンセル要求のチェックが入ります。

以上です。
2012/08/25

Android:ロングスクリーンと判断される基準

スクリーンがlongかnotlongかの判断は下記の式で決定されるようです。

  isLong = ((画面サイズの長い方*3)/5) >= (画面サイズの短い方-1))

例えば、800px×1280pxの端末の場合下記の判定式に置き換えられます。

  (1280*3)/5 >= 800-1

結果、notlongな端末と判断されます。


ただし、実際にはデコレーション領域を除いたサイズが参照されます。
ナビゲーションバー領域のサイズが減算されてた値が使用されます。
Nexus7だと、ナビゲーションバーの位置が画面の向きに依存します。

 port : 基本サイズ(x=800px, y=1280px), 参照サイズ(x=800px, y=1205px)


land : 基本サイズ(x=1280px, y=800px), 参照サイズ(x=1280px, y=736px)


該当するソースコードは下記です。

・com.android.server.wm.WindowManagerService.reduceConfigLayout()
// Is this a long screen?
if (((longSize*3)/5) >= (shortSize-1)) {
    // Anything wider than WVGA (5:3) is considering to be long.
    screenLayoutLong = true;
} else {
    screenLayoutLong = false;
}

"long, notlongは画面アスペクト比に依存するもので、画面の向きには依存しない"とDevelopersSiteに記載があります。
http://developer.android.com/guide/topics/resources/providing-resources.html
This is based purely on the aspect ratio of the screen (a "long" screen is wider). This is not related to the screen orientation.
でも、Nexus7のようなナビゲーションバーが画面の向きで変化する端末だと、
場合によってはこの前提が崩れてしまうのでは...

以上です。
2012/08/09

Android:JellyBeanで変更・非推奨となったメソッド・定数


JellyBeanで変更・非推奨となったメソッド・定数をいくつか抜粋。


●コンテンツの変更通知 -ContentObserver-

今までは監視したいURIの数だけObserverを用意する必要がありました。
JB以降は変更されたコンテンツURIも同時にObserverへ通知されるので、Observerを複数用意する必要がなくなります。

android.database.ContentObserver.dispatchChange(boolean)
非推奨となりました。
代わりにandroid.database.ContentObserver.dispatchChange(boolean, Uri)を使いましょう。


android.database.ContentObserver.dispatchChange(boolean, Uri)
新規追加のメソッドです。
引数Uriは変更のあったコンテンツのURIを指定します。


android.database.ContentObserver.onChange(boolean, Uri)
新規追加のメソッドです。
監視しているコンテンツに変更があった場合に呼ばれます。
引数Uriは変更のあったコンテンツのURIになります(nullの場合は"不明"を意味します)

JBより古い環境で互換性を維持したい場合、onChange(boolean)をオーバーライドする必要があります。
// Implement the onChange(boolean) method to delegate the change notification to
// the onChange(boolean, Uri) method to ensure correct operation on older versions
// of the framework that did not have the onChange(boolean, Uri) method.
@Override
public void onChange(boolean selfChange) {
    onChange(selfChange, null);
}

// Implement the onChange(boolean, Uri) method to take advantage of the new Uri argument.
@Override
public void onChange(boolean selfChange, Uri uri) {
    // Handle change.
}


●データベース関連

メンテがちらほら。
acquireUnstableContentProviderClientは何気に影響範囲の広い修正です。
「Android:JellyBean以降のContentResolver.queryの挙動」

android.database.Cursor.deactivate()
非推奨となりました。
requery()が既に非推奨化されている(Honeycombからだったかな?)ので、これも非推奨です。
requery()の非推奨化時の忘れ物かな?


android.database.sqlite.SQLiteOpenHelper.setWriteAheadLoggingEnabled(boolean)
新規追加のメソッドです。
WALの有効/無効指定です。
引数booleanがtrueならenableWriteAheadLogging()、falseならdisableWriteAheadLogging()が実行されます。


android.content.ContentResolver.acquireUnstableContentProviderClient(Uri)
android.content.ContentResolver.acquireUnstableContentProviderClient(String)
新規追加のメソッドです。
信用できないContentProviderに対するクライアント(ContentProviderClient)を取得します。
過去の記事でも取り上げました。
「Android:高速化。ContentResolver?ContentProviderClient?」



●クエリ/リクエストのキャンセル -CancellationSignal-

コンテンツへのクエリがキャンセルできるようになりました。
これにより、コンテンツをロードするLoaderとそのサブクラスも同時に拡張されています。

また、"オペレーションのキャンセル"という抽象的な例外クラスOperationCanceledExceptionが追加されました。
クエリやローダをキャンセルした際にスローされるOperationCanceledExceptionにも注目です。

android.content.ContentResolver.query(Uri, String[], String, String[], String, CancellationSignal) 
新規追加のメソッドです。
cancellationSignalが指定できるようになり、クエリを中断することができます。
cancellationSignalについては追々調査。


android.content.AsyncTaskLoader.cancelLoadInBackground()
新規追加のメソッドです。
ローダの処理をキャンセルします。
cancelLoadInBackgroundはloadInBackground()処理が走っていない、あるいは既に終了している場合N.O.Pとするのが通例です。


android.content.AsyncTaskLoader.isLoadInBackgroundCanceled()
新規追加メソッドです。
ローダがキャンセル処理を受け付けたかどうかを確認します。


android.content.Loader.registerOnLoadCanceledListener(OnLoadCanceledListener<D>)
新規追加メソッドです。
ロード処理のキャンセルをlistenするリスナーを登録します。


android.content.Loader.unregisterOnLoadCanceledListener(OnLoadCanceledListener<D>)
新規追加メソッドです。
ロード処理のキャンセルをlistenするリスナー登録を解除します。


android.content.Loader.cancelLoad()
新規追加メソッドです。
ロード処理のキャンセルを試みます。


android.content.Loader.deliverCancellation()
新規追加メソッドです。
OnLoadCanceledListenerへのキャンセル通知を行います。


android.content.Loader.onCancelLoad()
新規追加メソッドです。
Loaderのサブクラスは、このメソッドをオーバーライドしてロード処理のキャンセル要求(cancelLoad)に応えます。



●その他

android.content.res.Configuration.ORIENTATION_SQUARE 
非推奨となりました。
⇒過去の記事で取り上げています「画面の向き:ORIENTATION_SQUARE


android.view.ActionProvider.onCreateActionView () 
非推奨となりました。
代わりにandroid.view.ActionProvider.onCreateActionView(MenuItem)を使いましょう。
JBより古い環境をサポートしたい場合は、このメソッドをオーバーライドして、有効なActionViewを返します。


android.view.ActionProvider.onCreateActionView(MenuItem)
新規追加のメソッドです。
JB以降はこのメソッドでActionViewを生成することが推奨されます。

以上です。

2012/08/06

Android:JellyBean以降のContentResolver.queryの挙動


事の発端は下記の投稿。
Issue 35610:  Why does getContentResover().query() acquire IContentProvider twice but leave the stable one only?  

JellyBeanで追加されたacquireUnstableProviderメソッド。
この変更がContentResolverやContentProviderClientにも波及している。

特にContentResolverのqueryやopenAssetFileDescriptorの変更は興味深い。
ContentResolver.java (r4.1.1)

まず、unstableなプロバイダを取得してクエリを発行。
qCursor = unstableProvider.query(uri, projection,
        selection, selectionArgs, sortOrder, remoteCancellationSignal);
取得したqCursorはCursorWrapperInnerでラップして、stableなプロバイダと紐付けさせている。
CursorWrapperInner wrapper = new CursorWrapperInner(qCursor,
        stableProvider != null ? stableProvider : acquireProvider(uri));
最後に、要らなくなったunstableなプロバイダをリリース。
} finally {
    if (unstableProvider != null) {
        releaseUnstableProvider(unstableProvider);
    }
なぜこんな処理をしているのか?
Issue 35610を投稿した方も色々疑問に思っているみたいですが、、、

プロバイダ側プロセスの死亡で道連れ死する期間を小さく抑えているのか?
はたまた、パフォーマンス的な問題なのか?

はっきりした答えがありませんが、
JellyBeanからContentResolver.queryの挙動が若干変わったことは覚えておいたほうが良さそうです。

以上です。
2012/08/03

Android:ContentProviderOperation.BuilderのAPI


情報源はDevelopersサイトのjavadoc。
ContentProviderOperation.Builder

●クラス概要

ContentProviderOperationを構築するビルダクラスです。
ContentProviderOperation.Builderのインスタンスは、newInsert/newUpdate/newDelete/newAssertQueryメソッドを呼び出して生成します。
newInsertで生成されたビルダはInsertタイプ、newUpdateで生成されたビルダはUpdateタイプといった具合になります。

withXXXメソッドではビルダにパラメータを追加するのに使います。
それぞれのメソッドは、ContentProviderOperation.Builderタイプが許可されているかをチェックします。
例えば、InsertタイプのビルダではwithSelection()を使用することはできません。

build()を呼ぶことで、ContentProviderOperationが構築・生成されます。


●public ContentProviderOperation build () 

ContentProviderOperation.BuilderからContentProviderOperationを生成します。


●public ContentProviderOperation.Builder withExpectedCount (int count) 

これを設定した場合、オペレーションによって影響をうける行数と引数countが一致しないとOperationApplicationExceptionをスローします。
このメソッドはupdate/delete/assertタイプのビルダで使用可能です。


●public ContentProviderOperation.Builder withSelection (String selection, String[] selectionArgs) 

selectionに'?'が含まれている場合、selectionArgsの対応するIndexの値で置換されます。
selectionArgsのいずれかは、withSelectionBackReference(int, int)によって後方参照引数で上書きされる可能性があります。
このメソッドはupdate/delete/assertタイプのビルダで使用可能です。


●public ContentProviderOperation.Builder withSelectionBackReference (int selectionArgIndex, int previousResult)

selectionArgsの後方参照値を指定します。
withSelection(String, String[])によって指定されたselectionArgIndexにある任意の値を上書きます。
このメソッドはupdate/delete/assertタイプのビルダで使用可能です。


●public ContentProviderOperation.Builder withValue (String key, Object value) 

この値はwithValueBackReference(String, int)で指定された値で上書きされる可能性があります。
このメソッドはinsert/update/assertタイプのビルダで使用可能です。


●public ContentProviderOperation.Builder withValueBackReference (String key, int previousResult) 

withValues(ContentValues)で指定された値よりも優先されます。
このメソッドはinsert/update/assertタイプのビルダで使用可能です。


●public ContentProviderOperation.Builder withValueBackReferences (ContentValues backReferences)

key値はカラム名、vlaue値はpreviousResultのインデックス値。
withValues(ContentValues)で指定された値よりも優先されます。
このメソッドはupdate/delete/assertタイプのビルダで使用可能です。


●public ContentProviderOperation.Builder withValues (ContentValues values) 

これはnullになる可能性があります。
この値はwithValueBackReference(String, int)で指定された値で上書きされる可能性があります。
あるいは、これより後に呼ばれるwithValues(ContentValues), withValue(String, Object)によっても上書きされます。
このメソッドはupdate/delete/assertタイプのビルダで使用可能です。

以上です。
2012/08/02

Android:ContentProviderOperationで後方参照を活用する


ContentProviderOperationには後方参照の機能が備わっています。
ここでの後方参照とは"クエリセット中における、前回の結果を参照する機能"です。

後方参照を使用すると、クエリ結果を別クエリの条件式で使用することができるようになります。
例えば下記のようなケースです。
  • クエリ1:page_numberが2のレコードを追加
  • クエリ2:クエリ1のレコードを親に持つ子レコードを追加
SQLに置き換えてみると下記のような感じでしょうか...
  • クエリ1:INSERT INTO tbl_A (name, page_number, parent_id) VALUES ('parent', 2, NULL);
  • クエリ2:INSERT INTO tbl_B (name, parent_id) VALUES ('child', 777 /* クエリ1で追加したレコードの_id値 */ );
クエリ2がクエリ1で追加したレコードの_idを必要としていることがわかると思います。

クエリ1の_id値はInsert文を発行してみないとわかりません。
こういう場合に、前回の結果を参照できる"後方参照"の使用を検討します。

ContentProviderOperation.Builderには後方参照を設定するためのメソッドが用意されています。
  • ContentProviderOperation.Builder.withSelectionBackReference(int, int) 
  • ContentProviderOperation.Builder.withValueBackReference(String, int)
  • ContentProviderOperation.Builder.withValueBackReference(ContentValues)

各メソッドの詳細は後述します。

実際にwithSelectionBackReferenceメソッドを使って後方参照してみます。
例えば、下記のようにオペレーションを構築したとします。
operations.add(ContentProviderOperation.newInsert(uri)
        .withValue("name", "test1").build());
operations.add(ContentProviderOperation.newUpdate(uri)
        .withValue("name", "test2").build());
operations.add(ContentProviderOperation.newInsert(uri)
        .withValue("name", "test3").build());
operations.add(ContentProviderOperation.newInsert(uri)
        .withValue("name", "test4").build());
operations.add(ContentProviderOperation.newUpdate(uri)
        .withValue("name", "test5")
        .withSelection("_id=?", new String[1])
        .withSelectionBackReference(0, 2)
        .build());
5つめのオペレーション構築で後方参照指定していますね。
selection、つまり抽出条件を"_id=?"としています。

withSelectionBackReferenceの第一引数はselectionArgIndex。
selectionで'?'パラメータとして渡されたインデックスを指定し、これを置換します。
何に置換されるかは第二引数のpreviousResultで決定されます。

previousResultには「何番目の結果を参照するか?」を指定します。
この場合は1~4つめまでのオペレーションで得られた結果のインデックス(0~3)を指定します。
previousResultに0を指定すれば1つめのオペレーションの結果が、1を指定すれば2つめのオペレーションの結果が得られます。
previousResultが指定する参照先は、applyBatchの戻り値でもあるContentProviderResultの配列、つまり結果セットそのものです。

参照された結果セット(ContentProviderResult配列)はどのような値を返すのか?後方参照でどのような値に置換されるのか?
これは、参照するContentProviderResultの種類(Insert or Update or Delete)によって変化します。
また、プロバイダのinsert/updateやdeleteがどのような戻り値を返すのかにも依存します。
通常、Insertは追加されたレコードのURI、Update/Deleteは影響を受けたレコード数が戻り値となりますね。
以降は、そのように実装されている前提で話を進めます。

後方参照の対象となる結果がInsertオペレーションによるものの場合。
後方参照の結果、Insertで追加されたレコードのID値が取得できます。

後方参照の対象となる結果がUpdate/Deleteオペレーションによるものの場合。
後方参照の結果、Update/Deleteで変更されたレコードの数が取得できます。

ContentProviderResultはInsertオペレーションによる結果はuriを保持し、Update/Deleteオペレーションによる結果はcountを保持すると説明しました。
しかし、後方参照ではInsertの結果をuriではなく、そのuriに含まれている(であろう)id値、つまり追加されたレコードの_id値が取得できます。
これは、後方参照用にContentProviderResultのuriを解析して、末尾の数字(つまり_id)を抽出しているためです。

実際に後方参照値を取り出すソースコードを見てみましょう。

・android.content.ContentProviderOperation.backRefToValue(ContentProviderResult[], int, Integer)
/**
 * Return the string representation of the requested back reference.
 * @param backRefs an array of results
 * @param numBackRefs the number of items in the backRefs array that are valid
 * @param backRefIndex which backRef to be used
 * @throws ArrayIndexOutOfBoundsException thrown if the backRefIndex is larger than
 * the numBackRefs
 * @return the string representation of the requested back reference.
 */
private long backRefToValue(ContentProviderResult[] backRefs, int numBackRefs,
        Integer backRefIndex) {
    if (backRefIndex >= numBackRefs) {
        Log.e(TAG, this.toString());
        throw new ArrayIndexOutOfBoundsException("asked for back ref " + backRefIndex
                + " but there are only " + numBackRefs + " back refs");
    }
    ContentProviderResult backRef = backRefs[backRefIndex];
    long backRefValue;
    if (backRef.uri != null) {
        backRefValue = ContentUris.parseId(backRef.uri);
    } else {
        backRefValue = backRef.count;
    }
    return backRefValue;
}
if (backRef.uri != null) つまりInsertオペレーションによる結果(ContentProviderResult)である場合、uriから_idを抽出しています。
そうでない(つまりUpdate/Deleteオペレーションによる結果である)場合、countを返す仕様です。

ここで、もう一度オペレーション構築のコードに戻ります。
今度はコメントをつけてみました。
operations.add(ContentProviderOperation.newInsert(uri)  // previousResult 0番目
        .withValue("name", "test1").build());
operations.add(ContentProviderOperation.newUpdate(uri)  // previousResult 1番目
        .withValue("name", "test2").build());
operations.add(ContentProviderOperation.newInsert(uri)  // previousResult 2番目
        .withValue("name", "test3").build());
operations.add(ContentProviderOperation.newInsert(uri)  // previousResult 3番目
        .withValue("name", "test4").build());
operations.add(ContentProviderOperation.newUpdate(uri)  // previousResult 4番目
        .withValue("name", "test5")
        .withSelection("_id=?", new String[1])          // selectionArgIndex=0は_idに対する?
        .withSelectionBackReference(0, 2)               // selectionArgIndex=0, previousResult=2
5番目のオペレーションは後方参照の結果、下記のようなUpdate文になるでしょう。
WHEREで指定される_id値は、3つめのオペレーションで追加されたレコードIDの結果次第で変化します。
UPDATE test_table SET name = 'test5' WHERE _id = 777 /* 3つめのオペレーションで追加されたレコードID */ ;

以上が後方参照の基本的な動作です。

残る下記2つのメソッドについても基本的には同じ考え方です。
これらは、Insert/Updateで追加or更新したい値を指定するwithValue()の後方参照版です。
  • ContentProviderOperation.Builder.withValueBackReference(String, int)
  • ContentProviderOperation.Builder.withValueBackReference(ContentValues)

withValueBackReference(String, int)の2つ目の引数が前述のpreviousResultにあたります。
1つ目の引数は更新対象のカラム名です。

withValueBackReference(ContentValues)は、追加・更新したい値が複数カラムある場合に使います。
ContentValuesのkeyとvalueはwithValueBackReference(String, int)のそれと同じです。
ContentValuesのkeyが更新対象のカラム名、valuesが前述のpreviousResultにあたります。

下記はそのサンプル。
// nameの値がpreviousResult 2番目の結果で更新される。
ContentValues v = new ContentValues();
v.put("name", 2);
operations.add(ContentProviderOperation.newInsert(uri)
        .withValue("name", "test995")
        .withValueBackReferences(v).build());
後方参照は少しややこしいですが、
  • previousResultは結果セットの何番目を参照するかを指定する
  • 後方参照で得られる値はInsertの場合uri末尾の数字(_id値)、Update/Deleteは影響を受けたレコード数になる
ことを覚えておけば理解しやすいと思います。

以上です。
2012/08/01

Android:ContentProviderOperationとapplyBatch


データベースへのクエリをまとめて発行したい場合はapplyBatchの使用を検討します。

●applyBatchのメリット

・ContentResolver経由で1つずつクエリ実行するより高速
これは、ContentResolver.applyBatch内部でContentProviderClientを使用するためです。

引用:android.content.ContentResolver.applyBatch(String, ArrayList<ContentProviderOperation>)
public ContentProviderResult[] applyBatch(String authority,
        ArrayList<ContentProviderOperation> operations)
        throws RemoteException, OperationApplicationException {
    ContentProviderClient provider = acquireContentProviderClient(authority);
    if (provider == null) {
        throw new IllegalArgumentException("Unknown authority " + authority);
    }
    try {
        return provider.applyBatch(operations);
    } finally {
        provider.release();
    }
}
ContentProviderClientによる高速化については過去の記事で取り上げています。
高速化。ContentResolver?ContentProviderClient?

・トランザクション制御が容易
デフォルトではapplyBatchのアトミック性は保証されません。
これを保証するにはContentProviderのapplyBatchをオーバーライドしてトランザクション処理を追加します。
bulkInsertとapplyBatchのアトミック性を保証する


●applyBatchのデメリット

・クエリの組み立てが若干複雑
クエリの組み立て方に独特なルールがあります。
また、後方参照を使用しなければならないケースでは可読性が下がります。
といっても、理解すれば問題ないレベルです。

・クエリが途中で失敗した場合の対処
まず、applyBatchがアトミック性を保証していないということ。
さらに、クエリが途中で失敗すると、それ以降のクエリは実行されないということ。
この2点を念頭においてapplyBatchを使用する必要があります。

applyBatchのメリット・デメリットを列挙したところで、本稿ではapplyBatchの概要と、これを使用するためのContentProviderOperationについて記載します。


●概要

applyBatchはクエリ群をContentProviderOperationのリストとして受け取ります。
ContentProviderOperationはinsert/update/deleteクエリを表現するクラスです。
クエリは、ContentProviderOperationに設定した値をもとに生成・発行されます。
ContentResolver.applyBatch  (String, ArrayList<ContentProviderOperation>)
ContentProviderOperation


●ContentProviderOperation.Builder

ContentProviderOperationはContentProviderOperation.Builderクラスから生成します。
ContentProviderOperation.Builder

ContentProviderOperationを構築する簡単なサンプルは下記です。
// Insert文のオペレーションを生成
ContentProviderOperation.newInsert(uri).withValue("name", "test1").build()

// Update文のオペレーションを生成
ContentProviderOperation.newUpdate(uri).withValue("name", "test2").build()

// Delete文のオペレーションを生成
ContentProviderOperation.newDelete(uri).build()
ContentProviderOperation.Builderは用途にあわせてInsert用、Update用、Delete用が用意されています。
ビルダは"どの種類のビルダなのか"を明示してインスタンスを生成します。
  • newInsert(uri) : Insert用ビルダを生成
  • newUpdate(uri) : Update用ビルダを生成
  • newDelete(uri) : Delete用ビルダを生成


●ContentProviderOperationの構築

ContentProviderOperation.Builderには、ContentProviderOperationを構築するためのメソッドが用意されています。
各メソッドはInsert文に特化したものや、Update/Delete文に特化したものがあります。
例えばInsert用ビルダでは、Update/Deleteに特化したwithSelection(...)メソッドは使用できません。
各ビルダ種別毎の使用可能/不可能メソッドの一覧はContentProviderOperation.Builderのjavadocで確認できます。
もし、使用不可能なメソッドを呼び出すと例外が投げられます。
# Insert用ビルダがwithSelectionメソッドを呼び出した場合
java.lang.IllegalArgumentException: 
        only updates, deletes, and asserts can have selections

主なメソッドを下記に列挙します。
InsertやUpdateクエリで追加値・更新値を指定するには下記のメソッドを使用します。
  • ContentProviderOperation.Builder.withValues(ContentValues) 
  • ContentProviderOperation.Builder.withValueBackReference(String, int)
  • ContentProviderOperation.Builder.withValueBackReference(ContentValues)
これらはDelete用ビルダでは使用できないメソッド群です。

***BackReferenceは後方参照です。これについては次回投稿します。
withValuesメソッドを使用した簡単なサンプルは下記です。
// nameにtest1を持つレコードを追加するInsert文
ContentProviderOperation.newInsert(uri).withValue("name", "test1").build();
ContentProviderOperationで表現されるクエリは条件(selection)の指定が可能です。
条件指定には下記のメソッドを使用します。
  • ContentProviderOperation.Builder.withSelection(String, String[]) 
  • ContentProviderOperation.Builder.withSelectionBackReference(int, int) 
これらはInsert用ビルダでは使用できないメソッド群です。

****BackReferenceは後方参照です。これについては次回投稿します。
withSelectionメソッドを使用した簡単なサンプルは下記です。
// _idが3のレコードのnameを"test5"に更新するUpdate文
ContentProviderOperation.newUpdate(uri).withValue("name", "test5")
    .withSelection("_id=?", new String[]{"3"}).build();

●ContentProviderOperationの結果オブジェクト

applyBatchは複数のクエリ(ContentProviderOperation)を実行します。
そのため、複数のクエリ結果をContentProviderResultの配列で返します。

ContentProviderResultはuriとcountを持つシンプルなクラスです。
Insertクエリの結果はuriに、Update/Deleteの結果はcountに格納されます。
ContentProviderResult

applyBatchで実行される各クエリの結果を参照したい場合はContentProviderResultを参照します。


●applyBatchの実行

applyBatchを実行する簡単なコードは下記です。
ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
Uri uri = Uri
        .parse("content://yuki.contentprovider.mycontentprovider");
operations.add(ContentProviderOperation.newInsert(uri)
        .withValue("name", "test1").build());
operations.add(ContentProviderOperation.newUpdate(uri)
        .withValue("name", "test2").build());
try {
    getContentResolver().applyBatch(
            "yuki.contentprovider.mycontentprovider", operations);
} catch (Exception e) {
    Log.e("yuki", "error");
}
これを実行すると、Insert文とUpdate文がまとめて実行されます。

クエリはArrayList(operations)の先頭から順番に実行されます。
もし途中でクエリの実行が失敗すると、それ以降のクエリは実行されません。
# applyBatchにアトミック性を求めるなら「bulkInsertとapplyBatchのアトミック性を保証する」を参照

以上です。