2012/07/31

Android:bulkInsertとapplyBatchのアトミック性を保証する

●blukInsert

ContentResolverやContentProviderClientにあるbulkInsertはアトミックな操作ではありません。
トランザクションの開始なしに、連続してinsertするAPIです。
ContentResolver.bulkInsert

・android.content.ContentProvider.bulkInsert(Uri, ContentValues[])
/**
 * Override this to handle requests to insert a set of new rows, or the
 * default implementation will iterate over the values and call
 * {@link #insert} on each of them.
 * As a courtesy, call {@link ContentResolver#notifyChange(android.net.Uri ,android.database.ContentObserver) notifyChange()}
 * after inserting.
 * This method can be called from multiple threads, as described in
 * <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html#Threads">Processes
 * and Threads</a>.
 *
 * @param uri The content:// URI of the insertion request.
 * @param values An array of sets of column_name/value pairs to add to the database.
 * @return The number of values that were inserted.
 */
public int bulkInsert(Uri uri, ContentValues[] values) {
    int numValues = values.length;
    for (int i = 0; i < numValues; i++) {
        insert(uri, values[i]);
    }
    return numValues;
}

bulkInsertのアトミック性を保証したい場合、独自のプロバイダでbulkInsertをオーバーライドします。
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
    SQLiteDatabase db = MyDataBase.getInstance(getContext())
            .getWritableDatabase();
    db.beginTransaction();
    try {
        SQLiteStatement insertStmt = db
                .compileStatement("INSERT INTO test "
                        + "(name) VALUES (?);");
        for (ContentValues value : values) {
            insertStmt.bindString(1, value.getAsString("name"));
            insertStmt.executeInsert();
        }
        db.setTransactionSuccessful();
    } finally {
        db.endTransaction();
    }
    ...
これでbulkInsertのアトミック性が保証されます。


●applyBatch

blukInsertと同じく。
ContentResolverやContentProviderClientにあるapplyBatchもアトミックな操作ではありません。
まとめてContentProviderOperationを実行するためのAPIで、トランザクションは開始されません。
ContentResolver.applyBatch

・android.content.ContentProvider.applyBatch(ArrayList<ContentProviderOperation>)
/**
 * Override this to handle requests to perform a batch of operations, or the
 * default implementation will iterate over the operations and call
 * {@link ContentProviderOperation#apply} on each of them.
 * If all calls to {@link ContentProviderOperation#apply} succeed
 * then a {@link ContentProviderResult} array with as many
 * elements as there were operations will be returned.  If any of the calls
 * fail, it is up to the implementation how many of the others take effect.
 * This method can be called from multiple threads, as described in
 * <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html#Threads">Processes
 * and Threads</a>.
 *
 * @param operations the operations to apply
 * @return the results of the applications
 * @throws OperationApplicationException thrown if any operation fails.
 * @see ContentProviderOperation#apply
 */
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
        throws OperationApplicationException {
    final int numOperations = operations.size();
    final ContentProviderResult[] results = new ContentProviderResult[numOperations];
    for (int i = 0; i < numOperations; i++) {
        results[i] = operations.get(i).apply(this, results, i);
    }
    return results;
}

applyBatchのアトミック性を保証したい場合、独自のプロバイダでapplyBatchをオーバーライドします。
@Override
public ContentProviderResult[] applyBatch(
        ArrayList<ContentProviderOperation> operations)
        throws OperationApplicationException {
    SQLiteDatabase db = MyDataBase.getInstance(getContext())
            .getWritableDatabase();
    db.beginTransaction();
    try {
        ContentProviderResult[] result = super.applyBatch(operations);
        db.setTransactionSuccessful();
        return result;
    } finally {
        db.endTransaction();
    }
}
これでapplyBatchのアトミック性が保証されます。

以上です。

2012/07/30

Android:SQLiteのロックとTransaction Immediate/Exclusive


!この記事は古くなっています. 更新版は下記をご覧ください.!
  Android: SQLite3 LockとTransaction Immediate/Exclusive
  http://yuki312.blogspot.jp/2014/10/android-sqlite3-locktransaction.html


SQLiteのロック機構と、Transactionで使用できるロック2種の選択基準についての考察。

●SQLiteのロック

SQLiteのロック単位は"データベース単位"。
このため、ロックを1つ取得すると同データベース上にある全てのテーブルに影響がある。

Oracle等の巨大なDBMSでは"行ロック"なんてものがあったりして細かくロックを制御できるが、軽量なSQLiteは"データベース単位"でのロックのみサポートしている。
そのため、長い時間ロックし続けると他テーブルになかなかアクセスできない状態になる。
全く問題にならないケースもあるが、"データベースを分割するかしないか"の判断基準の1つにはなりそう。


●Transaction Immediate

Transactionで指定できるロック種別の1つがImmediate。
Immediateロックモードのポイントは下記
・ロック中、他ユーザはデータの読み取りはできても書き込みはできない
長所は、ロック中でも他ユーザがデータを読めるため"ロック解除されるまで待たなくて良い"こと。
逆に"読めてしまうことによる弊害"がある場合はExclusiveロックの使用を検討する。

Immediateロックではマズいケースは下記のような場合かな?あまり良い例が思いつかない。
  1.  アプリの設定値を保持したテーブルがある
  2.  アプリの設定値は他モジュールから参照される
  3.  アプリの設定値は可能な限り最新値を返す
  4.  アプリの設定値の更新には多少時間が掛かる
アプリの設定値を更新する必要があるシーン(つまりDBテーブルの値が古い状態)で、設定値の更新を試みる。
ただし、設定値の更新は多少時間がかかる(4)。
Immediateロックでは、更新中(ロック中)にデータベースを参照されると古い値が返されるのでNG(3)。
そのため、参照もできなくするExclusiveロックを掛ける。


●Transaction Exclusive

Transactionで指定できるもう1つのロック種別がExclusive。
Exclusiveロックモードのポイントは下記
・ロック中、他ユーザはデータの読み書きができない
長所は、"ロック中に読み書きできるのは自ユーザのみ"なこと。
逆に"他ユーザの読み取りを待たせてしまうことによる弊害"がある場合はImmediateロックの使用を検討する。

Exclusiveロックだとマズいケースは下記のような場合かな?
  1.  とあるデータAは多数のユーザから頻繁に読み取りアクセスされる
  2.  とあるデータAを読み取るアプリは高い応答性が求められる
  3.  データ更新中に並行してデータ参照されても平気
Exclusiveロックを取得している間、他ユーザは同データベース内にあるテーブルや行/列のデータを参照できない。
そのため、Exclusiveロックが長くなればなるほど、データを参照したいアプリの応答性を損なう原因となり兼ねない(2)。
(3)があてはまるならImmediateロックで問題ないかな?

# 上記2つのケースがImmediate/Exclusiveロックの考え方として正しいのか自信なし。
# もっと良いケースがあれば後日修正します。


●ソースコード

AndroidでのTransactionはSQLiteDataBaseクラスを使用します。

基本的な使い方は割愛。下記サイトで詳しく取り上げられています。
AABlog:Androidアプリ開発 SQLiteデータベースを使用する(トランザクション)

AndroidでTransactionのロック種別を指定するにはSQLiteDataBaseクラスを使用します。
使用するのは下記のAPI
※beginTransactionNonExclusive/beginTransactionWithListenerNonExclusiveはAPI Level11(Honeycomb)で追加されたAPIです。

beginTransaction/beginTransactionWithListenerで開始されたトランザクションは「Exclusiveモード」で、
beginTransactionNonExclusive/beginTransactionWithListenerNonExclusiveで開始されたトランザクションは「Immediateモード」でデータベースをロックします。
// 「Exclusiveモード」でロック
db.beginTransaction();
try {
    // do something.
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}

// 「Immediateモード」でロック
db.beginTransactionNonExclusive();
try {
    // do something.
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}
簡単ですね。

●SQLiteDatabaseLockedException

ロックされているデータベースに対してSQLiteDatabase経由でクエリを実行した場合、、、

・SQLiteDatabase.query
ロックが取得できるまで待ちます。
API Lv16(JellyBean)以降はCancellationSignalを使ってクエリをキャンセルすることが出来ます。
ContentResolver.query()
CancellationSignal

・SQLiteDatabase.insert
SQLiteDatabaseがSQLiteDatabaseLockedExceptionをキャッチし、呼出し元に-1を返します。
public long insert(String table, String nullColumnHack, ContentValues values) {
    try {
        return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE);
    } catch (SQLException e) {
        Log.e(TAG, "Error inserting " + values, e);
        return -1;
    }
}
・SQLiteDatabase.insertOrThrow
SQLiteDatabaseLockedExceptionが投げられます。
public long insertOrThrow(String table, String nullColumnHack, ContentValues values)
        throws SQLException {
    return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE);
}

・SQLiteDatabase.update
SQLiteDatabaseLockedExceptionが投げられます。

・SQLiteDatabase.delete
SQLiteDatabaseLockedExceptionが投げられます。


また、トランザクション開始のbeginTransactionでも同様。

・SQLiteDatabase.beginTransaction
SQLiteDatabaseLockedExceptionが投げられます。


以上です。
2012/07/25

Android:1つのDBに複数のContentProvider


データベースの構造が複雑化すると、これを扱うContentProviderも複雑になりがちです。
そんな時はContentProviderを分割するのも1つの方法です。

今回は複数のContentProviderで1つのデータベースを管理する方法について。

複雑な構造をもつデータベースを、たった1つのプロバイダが管理するのは大変です。
この場合、ContentProviderを分類毎や各機能毎に分割すれば複雑化を回避することができます。
また、ContentProviderの分割はセキュリティの向上にも役立ちます。

簡単な例を示します。

あなたのアプリが持つデータベースの状態が下記の場合...
  1. 1つのデータベースにデータタイプの異なるテーブルが複数存在する
  2. それぞれのテーブルは大きい、小さい、複雑、単純と様々
  3. いくつかのテーブルは外部アプリに提供する情報。いくつかのテーブルは非公開情報

複数のテーブルを管理するだけではなく、それぞれのデータの"公開"or"非公開"を意識する必要があります。
さらには、データの分類も多岐にわたるので、たった1つのContentProviderで管理するには無理がありそうです。


そこで、ContentProviderの分割を検討します。

まず、1つのContentProviderは1つのデータ分類を管理するようにします。
これでContentProviderの凝集度も上がります。

次に、公開or非公開のデータをどのように扱うかを考えます。
今回の場合は"データ分類B"が非公開情報です。
これは、データ分類BのContentProviderに対するアクセス権限を設定することで解決します。

対応後のイメージは下記になります。


それぞれのContentProviderを定義するマニフェストは次のようになります。
# あくまでサンプルです。実際には適切な権限と名前を割り当てます
...
<provider
    android:name="yuki.divcontentprovider.GlobalContentProvider"
    android:authorities="yuki.divcontentprovider.global" />
<provider
    android:name="yuki.divcontentprovider.LocalContentProvider"
    android:authorities="yuki.divcontentprovider.local"
    android:readPermission="yuki.divcontentprovider.dangerous" >
    <path-permission
        android:path="/ok"
        android:readPermission="yuki.divcontentprovider.normal" />
</provider>
...
GlobalContentProviderは公開情報(つまりデータ分類A)用のContentProviderです。
LocalContentProviderは非公開情報(つまりデータ分類B)用のContentProviderです。

LocalContentProviderはパスによって要求するパーミッションを変えています。
ContentProviderにはパスに対するパーミッション付与の機能が備わっているので制御も簡単です。

ソースコードに目を向けてみます。
ContentProviderを分割すると問題なのがデータベースの扱いです。
データベースの生成やアップグレードを担うSQLiteOpenHelperの設計には少し注意が必要です。

複数のContentProviderがいても、データベースは1つなのでSQLiteOpenHelperを継承するクラスはSingletonにします。
public class AppDataBase extends SQLiteOpenHelper {
    private static final String DATABASE_NAME = "hoge";
    private static final int DATABASE_VERSION = 1;

    private static AppDataBase sSingleton = null;

    public interface Table {
        public static final String TABLE1 = "TABLE_NAME";
    }

    public static synchronized AppDataBase getInstance(Context context) {
        if (sSingleton == null) {
            sSingleton = new AppDataBase(context);
        }
        return sSingleton;
    }

    public AppDataBase(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        ...
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        ...
    }
}
分割されたContentProvider達はAppDataBase.getInstance()メソッドを使用してインスタンスを取得します。

以上です。
2012/07/20

Android:抽象ドット密度tvdpi

抽象的なドット密度を表すリソース修飾子 tvdpiAndroid3.2で追加されました。
Android 3.2 Platform

tvdpiはテレビやそれに類似したデバイス向けに用意されています。
最近ではNexus7がtvdpiを持つ端末として知られています。
Getting Your App Ready for Jelly Bean and Nexus 7


●tvdpi概要

tvdpiは213dpi、mdpiより1.3312501倍のドット密度を持ちます。
これにより、各抽象ドット密度比は
 120:160:213:240:320 = 3 : 4 : 5.325 : 6 : 8
になります。

mdpiで縦横100pxの画像を用意する場合、tvdpiでは縦横133pxの画像が必要です。


●tvdpiを試す

下記の設定値をもつエミュレータでのAVDを作成することでtvdpiの動作を確認できます。
  • Skin Resolution:1280x800  or 600x960(Nexus7)
  • Abstract LCD:213
手元の環境だとSkinをWXGAとしても213dpiになりませんでした。
なので、手打ち(Resolution)で解像度を指定します。


●tvdpiのリソース修飾子選択基準

xxx-hdpi と xxx-tvdpi それぞれのリソースを持つアプリを下記の環境で動作させた場合どうなるのかを検証します。

・240dpiの端末で実行
結果:hdpiが参照される。もしhdpiリソースを持っていない場合はtvdpiが参照される。


・213dpiの端末で実行
結果:tvdpiが参照される。もしtvdpiリソースを持っていない場合はhdpiが参照される。

後者については、よりhigh densityなhdpiがダウンスケールされて参照されます。
don’t panic! We actively discourage you from rushing out and creating new assets at this density; Android will scale your existing assets for you. In fact the entire Jelly Bean OS contains only a single tvdpi asset, the remainder are scaled down from hdpi assets.    
[from Getting Your App Ready for Jelly Bean and Nexus 7]


以上です。

2012/07/19

Android:ACTION_MANAGE_NETWORK_USAGEのサポート


●Android4.0で追加されたNetwork Usage機能


Android4.0で、アプリのネットワーク使用量をユーザが確認できるNetwork Usage機能が追加されました。
アプリ毎やネットワーク種別毎に通信量の上限を設定することも可能です。

これはつまり、ユーザはネットワーク通信量や頻度が確認可能であり、またネットワーク通信量に関して興味を持つことに繋がります。
そういった意味で、ネットワーク通信を必要とするアプリにとって、その通信量・頻度が確認可能になったことによる影響を考える必要が出てきます。
Android developer -Android 4.0 for Users / Control over network data-



●インテントアクション:ACTION_MANAGE_NETWORK_USAGE


Network Usageでは、各アプリ毎の通信量や個別の設定を行うことが可能です。
アプリ毎の詳細画面では下記が確認・設定できます。
  • アプリのフォアグラウンド時の通信量
  • アプリのバックグラウンド時の通信量
  • アプリ個別のネットワーク設定の起動
  • バックグラウンドデータ通信の制限
ユーザはモバイル回線の通信量に上限を設けることができます。
また、アプリ毎にモバイル回線使用時のバックグラウンド通信を制限できます。
しかし、アプリによってはこれだけの設定項目では不十分な場合があります。
例えば、メールアプリでは新着メールのチェック機能を制限したくない場合です。
こういったアプリの特性を考慮した、より細かい通信制御は各アプリに委ねられます。

アプリ毎の詳細画面には[View app settings]ボタンが用意されています。
これは、そのアプリが用意したネットワーク設定を起動するトリガになるボタンです。
ネットワーク設定をサポートするには、アプリのネットワーク設定ActivityのIntentFilter
に下記を追加します。
<intent-filter>
    <action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
    <category android:name="android.intent.category.DEFAULT" />
</intent-filter>
[View app settings]ボタンが押されると、MANAGE_NETWORK_USAGEアクションでアプリの
ネットワーク設定画面を起動します。
IntentFilterが登録されていないと[View app settings]ボタンは無効化されます。

用意したネットワーク設定画面で、ユーザが通信を細かく制御できる仕組みを提供することが推奨されます。

以上です。
2012/07/18

Android:ブロードキャストを受信するコンポーネントを調べる方法

特定のBroadcastIntentを受信するBroadcastReceiverの一覧を取得するには次のコマンド
を使用します。
$ adb shell dumpsys activity broadcasts
実行すると登録されているIntentFilterの一覧が取得できます。
ACTIVITY MANAGER BROADCAST STATE (dumpsys activity broadcasts)
  Registered Receivers:
  * ReceiverList{421bc0e0 8893 com.example.networkcheck/10146 remote:4227fdf0}
    app=ProcessRecord{41c1f560 8893:com.example.networkcheck/10146} pid=8893 uid=10146
    Filter #0: BroadcastFilter{421bc140}
      Action: "android.net.conn.CONNECTIVITY_CHANGE"
  ...略
この例だと、com.example.networkcheckコンポーネントが
Action名:android.net.conn.CONNECTIVITY_CHANGE のインテントフィルタを登録してい
ることがわかります。

以上です。
2012/07/17

Android:エイリアスを使ったマルチスクリーン対応


画面サイズによって、レイアウトのパネル数を変化させるテクニックは有名です。
レイアウトパターンの1つMaster/Detailパターンは、タブレット等の大画面端末でよく
使われるパターンですが、ハンドセット端末では画面領域が限られている為、ほとんど使
われません。

今回は、もしあなたのアプリに下記の要求があった場合、
  • 画面サイズによってパネル数を変化させる必要がある
  • Android3.0との互換性も考える必要がある
どのようにレイアウトリソースを定義すればよいかを考えます。
(Android3.0より前のバージョンでも使えるテクニックです)

●リソース別名を使わない場合

まずは、画面領域が小さい端末(ハンドセット等)を対象にしたレイアウト。
これは1パネルレイアウトになります。
・res/layout/main.xml
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    android:name="yuki.sample.ItemListFragment"
    android:id="@+id/item_list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginLeft="16dp"
    android:layout_marginRight="16dp" />
次に、画面領域が大きい端末(タブレット等)を対象にしたレイアウト。
これは2パネルレイアウトになります。
・res/layout-sw600dp/main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginLeft="16dp"
    android:layout_marginRight="16dp"
    android:divider="?android:attr/dividerHorizontal"
    android:showDividers="middle">

    <fragment android:name="yuki.sample.ItemListFragment"
        android:id="@+id/item_list"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />

    <FrameLayout android:id="@+id/item_detail_container"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="3" />

</LinearLayout>
さらに、リソース修飾子sw<N>dpはAndroid3.2以降に追加されたため、
Android3.1以前をサポートするために抽象画面サイズのlarge版を用意します。
・res/layout-large/main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    .... />
    // 内容はres/layout-sw600dp/main.xmlと同じ
</LinearLayout>
今回用意したレイアウトファイルは次の3つです。
  • res/layout/main.xml (1パネル)
  • res/layout-sw600dp/main.xml (2パネル)
  • res/layout-large/main.xml (2パネル)

●リソース別名を使った場合

もし、sw600dpとlargeで用意したレイアウトファイルのような、レイアウト定義の重複を
避けたい場合、レイアウト別名ファイルを用意します。

例えば、次のような2ファイルを用意します。
  • res/layout/one_panels.xml (1パネル)
  • res/layout/two_panels.xml (2パネル)

そして、レイアウト別名を定義するファイルを用意します。
・res/values/layout.xml:
<resources>
    <item name="main" type="layout">@layout/one_panels</item>
</resources>
・res/values-sw600dp/layout.xml:
<resources>
    <item name="main" type="layout">@layout/two_panels</item>
</resources>
・res/values-large/layout.xml:
<resources>
    <item name="main" type="layout">@layout/two_panels</item>
</resources>
それぞれのファイルではレイアウト自体を定義していません。
レイアウトの別名を定義することでレイアウト定義の重複を避けています。
sw600dpの端末でレイアウト"main"を参照すると、結果的にres/layout/two_panels.xml
を参照することになります。
レイアウトを読み込むjavaコード側では、mainレイアウトリソースを読み込むだけで、
画面サイズにあった適切なレイアウトが取得できます。

画面サイズのみならず画面の向き(land or port)でもレイアウトを変化させるといった、
レイアウトリソースが爆発的に増えやすい仕様のアプリで有効な方法です。

●現在のレイアウトは1パネルか?2パネルか?

javaコード側で、現在のレイアウトが1パネルか2パネルかを判断する方法について考えます。
まず思い浮かぶのは下記でしょう。
if (findViewById(R.id.item_detail_container) != null) {
    // 2パネルモード
} else {
    // 1パネルモード
}
2パネルレイアウトでのみ定義しているitem_detail_containerが見つかれば2パネルモー
ドであると判断します。

しかし、この方法だとjavaコード側がレイアウトの詳細を知っていることになります。
さらに、レイアウト定義側もitem_detail_containerの扱いに注意が必要です。
この厄介な問題を解決するには、2パネルかどうかの判断フラグをリソースとして定義す
る方法があります。
・res/values/layout.xml:
<resources>
    <item name="main" type="layout">@layout/one_panels</item>
    <bool name="has_multi_panels">false</bool>
</resources>
・res/values-sw600dp/layout.xml:
<resources>
    <item name="main" type="layout">@layout/two_panels</item>
    <bool name="has_multi_panels">true</bool>
</resources>
javaコード側ではhas_multi_panelsのboolリソースを参照してパネル数を判断します。
こうすることで、javaコードとレイアウトリソースそれぞれの結合度を低く保つことが
できます。

より詳しい情報は下記を参照。
http://developer.android.com/training/multiscreen/screensizes.html

以上です。
2012/07/14

Android:ic_launcher-web.png


ADT Rev.20でプロジェクトを新規作成するとic_launcher-web.pngという名の画像ファイル
が、プロジェクトルート直下に自動生成されています。

これはGoogle Playにアプリを公開する時に必要となる
 "高解像度アプリケーション アイコン"
です。

デベロッパー向けGoogle Play - アプリケーション用の画像アセット

この画像は512x512pxサイズのPNG(αチャネル付)で作成する必要があります。
主にPC版Google Playのアプリアイコンとして使用されます。

以上です。
2012/07/12

Android:ADT Rev.20 雛形MasterDetailFlow


図1


ADT Rev.20でAndroidプロジェクトを新規作成する際の"Create Activity"(図1)で
"MasterDetailFlow"を選択した場合に作成される雛形の設計・実装を調査しました。

MasterDetailFlowはよくある2パネルのアプリを作成します。
ただし、画面領域を十分に確保できない場合は1ペインで表示します。
イメージはこんな感じです。
http://developer.android.com/images/fundamentals/fragments.png

MasterDetailFlowなプロジェクトを作成すると下記の構成で雛形が作成されます。


パッと見でこの辺がパネル数を制御してそうですね。
  • layout/activity_item_twopane.xml
  • values-large/refs.xml
  • values-sw600dp/refs.xml

activity_item_twopane.xmlは2パネル用のレイアウトファイル。
<LinearLayout
    ...一部省略...
    android:orientation="horizontal"
    android:showDividers="middle">

    <fragment android:name="com.example.fragment.ItemListFragment"
        ...一部省略...
        android:layout_weight="1" />

    <FrameLayout android:id="@+id/item_detail_container"
        ...一部省略...
        android:layout_weight="3" />

</LinearLayout>
リストと詳細の画面割当は1:3。それぞれの間はdividerで区切ってます。
リストパネルはItemListFragmentで管理。
詳細パネルはitem_detail_containerのIDを持つ空コンテナで、ItemDetailFragment
ではないようです。

次に2パネルレイアウトのactivity_item_twopane.xmlを読み込んでいる人を探します。
順当にレイアウト参照元を辿っていくと...

はじめに目をつけた通り、↓のようです。
  • values-large/refs.xml
  • values-sw600dp/refs.xml

どちらも内容は全く同じ。
layoutリソースactivity_item_twopaneをactivity_item_listとして定義してる。
<resources>
    <item type="layout" name="activity_item_list">@layout/activity_item_twopane</item>
</resources>
と、ここでもう一度layoutフォルダの中を見るとactivity_item_list.xmlが既に存在し
ている。
そのため、先ほどのrefs.xmlはlayoutリソースの再定義(上書き)となる。

まとめると、refs.xmlによって下記のようにリソースが定義されている。
  • デフォルト=activity_item_listはそのままactivity_item_list
  • 大画面端末=activity_item_listはactivity_item_twopane.xmlで上書き
  • 画面最短幅が600dp以上=activity_item_listはactivity_item_twopane.xmlで上書き
ということで、レイアウトactivity_item_listを読み込めば適切なリソースが選択されます。
refs.xmlでのリソース再定義は結構スマートですね。


activity_item_listを読み込んでいるのはItemListActivity。
単にactivity_item_listを読み込むだけで、1パネルor2パネルのレイアウトが適切に
選択されます。
Activity自身はonCreate内で詳細表示領域の空コンテナ(item_detail_container)がレイ
アウト内に存在しているかをチェックして、2パネルモードかどうかを判定しています。

また、ActivityがFragmentの状態を管理(ItemListFragment.setActivateOnItemClick)
していることもわかります。
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_item_list);

    if (findViewById(R.id.item_detail_container) != null) {
        mTwoPane = true;
        ((ItemListFragment) getSupportFragmentManager()
                .findFragmentById(R.id.item_list))
                .setActivateOnItemClick(true);
    }
}

ItemListActivityはonItemSelectedメソッドをオーバーライドしています。
これはItemListFragment.Callbacksインタフェースで定義されたコールバックメソッド。
ItemListFragmentでリストが選択されるとここにコールバックされます。
Fragment⇒Activityの連携はコールバックで実現しているようです。
この辺りはDeveloperサイトでも紹介されている方法ですね。
http://developer.android.com/training/basics/fragments/communicating.html
public class ItemListActivity extends FragmentActivity
        implements ItemListFragment.Callbacks {

    //...省略...

    @Override
    public void onItemSelected(String id) {
        if (mTwoPane) {
            Bundle arguments = new Bundle();
            arguments.putString(ItemDetailFragment.ARG_ITEM_ID, id);
            ItemDetailFragment fragment = new ItemDetailFragment();
            fragment.setArguments(arguments);
            getSupportFragmentManager().beginTransaction()
                    .replace(R.id.item_detail_container, fragment)
                    .commit();
        } else {
            Intent detailIntent = new Intent(this, ItemDetailActivity.class);
            detailIntent.putExtra(ItemDetailFragment.ARG_ITEM_ID, id);
            startActivity(detailIntent);
        }
    }
}

詳細画面の表示に関して。
2パネルの場合、空コンテナitem_detail_containerをFragmentのreplaceトランザクショ
ンで置き換えるようにしています。
1パネルの場合は、Activity自身でstartActivityを実行しているのがわかります。

詳細画面への情報連携(どのリストアイテムを選んだか)はIntentのextra値を使用。
リストアイテム選択時の処理はActivityまかせですね。


コールバック部分をもう少し追ってみます。

ItemListFragmentはコールバックリスナーであるmCallbacksを管理します。
初期値はsDummyCallbacks。こいつはNullオブジェクトの役目をします。
private Callbacks mCallbacks = sDummyCallbacks;

public interface Callbacks {
    public void onItemSelected(String id);
}

private static Callbacks sDummyCallbacks = new Callbacks() {
    @Override
    public void onItemSelected(String id) {
    }
};

mCallbacksはonAttachで登録。onDetachでNullオブジェクト化されます。
登録時はactivityインスタンスをCallbacksにキャストするため、事前にキャスト可能か
チェックをしてます。
このことから、ItemListFragmentを使うActivityはCallbacksインタフェースを実装する
必要があります。

Developerサイトではtry-catchでcallback登録していましたが、instanceOfで例外判定す
るこちらのほうが良い方法といえそうですね。
http://developer.android.com/training/basics/fragments/communicating.html#DefineInterface
@Override
public void onAttach(Activity activity) {
    super.onAttach(activity);
    if (!(activity instanceof Callbacks)) {
        throw new IllegalStateException("Activity must implement fragment's callbacks.");
    }

    mCallbacks = (Callbacks) activity;
}

@Override
public void onDetach() {
    super.onDetach();
    mCallbacks = sDummyCallbacks;
}

@Override
public void onListItemClick(ListView listView, View view, int position, long id) {
    super.onListItemClick(listView, view, position, id);
    mCallbacks.onItemSelected(DummyContent.ITEMS.get(position).id);
}

●おわりに...

MasterDetailFlowの雛形からは、Fragment⇒Activityへのメッセージ通信方法と、
画面サイズに依存したパネル数の変更方法についてのヒントがありました。

もし、手元にスマートフォンしかない場合、この雛形では縦横切り替えしてパネル数が
変化する動作を確認できません(sw600dp or largeを満たさない為)
values-sw600dp ⇒ values-land に変更すればスマートフォンでも手軽にこれを確認
できます。

以上です。
2012/07/11

Android:ActionBar/ActionModeのListModeを使う


ActionBarのViewControlは、アプリ内のViewを切り替えるUIとして使われます。
ViewControlは3つのActionModeを持っています。
  • Normal
  • Tab
  • List

今回はListModeについてです。

●ActionMode:ListMode

ActionModeをListModeに設定すると、ViewControl部分にドロップダウンメニューが表示
されます。



・サンプルコード
ActionBar actBar = getActionBar();
// リストに表示するアダプタを登録
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
        android.R.layout.simple_list_item_1, new String[] {
                "List Item 1", "List Item 2" });
// Navigationコールバックを登録する
actBar.setListNavigationCallbacks(adapter,
        new ActionBar.OnNavigationListener() {
            @Override
            public boolean onNavigationItemSelected(int itemPosition,
                    long itemId) {
                return false;
            }
        });
// 最後にNavigationModeをListModeに設定
actBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);

しかし、これだとアプリのタイトル文字列とケンカして見づらいです。
ListModeを使う場合、アプリアイコンで十分アイデンティティが確保されていれば、
アプリタイトルを非表示としたほうが良い場合がありそうです。


・アプリタイトルを表示しないサンプルコード

ActionBar actBar = getActionBar();
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
        android.R.layout.simple_list_item_1, new String[] {
                "List Item 1", "List Item 2" });
actBar.setListNavigationCallbacks(adapter,
        new ActionBar.OnNavigationListener() {
            @Override
            public boolean onNavigationItemSelected(int itemPosition,
                    long itemId) {
                return false;
            }
        });
// アプリタイトルを非表示に設定
actBar.setDisplayOptions(0, ActionBar.DISPLAY_SHOW_TITLE);
actBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);

●ActionBarを上下に分割して表示

ListModeを採用すると、どうしてもActionBarの領域が狭くなりがちです。
ここにActionボタンも併用すると窮屈なUIになってしまいます。

その場合、ActionBarを画面上下に分割するsplitActionBarWhenNarrowの利用を考えます。



実装方法はいたってシンプル。
AndroidManifest.xmlのActivity要素にandroid:uiOptions="splitActionBarWhenNarrow"
を追加します。

・サンプルコード

<activity
    android:name=".MainActivity"
    android:label="@string/title_activity_main"
    android:uiOptions="splitActionBarWhenNarrow" >

ただし、画面横幅が一定サイズを超える場合はsplitActionBarWhenNarrowが有効とならな
い場合があります。
http://yuki312.blogspot.jp/2012/07/androidsplitactionbarwhennarrow.html

以上です。
2012/07/10

Android:HeaderViewListAdapterでOutOfBoundsException


過去に出会った不具合について覚書。

Header付きListViewを持つDialogを表示しながら画面回転したり、
言語切替した後に復帰すると稀にエラーが発生しました。

●手順

  1. Header付きListViewを持つDialogを表示中に画面回転
または
  1. Header付きListViewを持つDialogを表示
  2. Homeボタンでアプリをバックグラウンドへ
  3. 言語切り替えを行う
  4. 手順1の画面に復帰

●試験結果

アプリが強制終了する場合がある。

エラーログ:
FATAL EXCEPTION: main
java.lang.IndexOutOfBoundsException: Invalid index 0, size is 0
  at java.util.ArrayList.throwIndexOutOfBoundsException(ArrayList.java:251)
  at java.util.ArrayList.get(ArrayList.java:304)
  at android.widget.HeaderViewListAdapter.isEnabled(HeaderViewListAdapter.java:164)
  at android.widget.ListView.dispatchDraw(ListView.java:3342)
  at android.view.View.draw(View.java:10999)
  at android.widget.AbsListView.draw(AbsListView.java:3591)
  at android.view.View.getDisplayList(View.java:10435)
  at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:2597)
  at android.view.View.getDisplayList(View.java:10398)
  at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:2597)
  at android.view.View.getDisplayList(View.java:10398)
  at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:2597)
  at android.view.View.getDisplayList(View.java:10398)
  at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:2597)
  at android.view.View.getDisplayList(View.java:10398)
  at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:2597)
  at android.view.View.getDisplayList(View.java:10398)
  at android.view.HardwareRenderer$GlRenderer.draw(HardwareRenderer.java:875)
  at android.view.ViewRootImpl.draw(ViewRootImpl.java:2027)
  at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1751)
  at android.view.ViewRootImpl.handleMessage(ViewRootImpl.java:2559)
  at android.os.Handler.dispatchMessage(Handler.java:99)
  at android.os.Looper.loop(Looper.java:137)
  at android.app.ActivityThread.main(ActivityThread.java:4475)
  at java.lang.reflect.Method.invokeNative(Native Method)
  at java.lang.reflect.Method.invoke(Method.java:511)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:792)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:559)
  at dalvik.system.NativeStart.main(Native Method)

●再現率

3/5

●原因解析

同様の現象が発生する最小構成のコードが下記。
public class HeaderListViewActivity extends Activity {
    ArrayList<String> elements = new ArrayList<String>();

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        elements.clear();
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_MENU) {
            showDialog(1);
        }
        return super.onKeyDown(keyCode, event);
    }

    @Override
    protected Dialog onCreateDialog(int id) {
        if (id == 1) {
            elements.add("A");
            elements.add("B");
            elements.add("C");
            ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
                    android.R.layout.simple_list_item_1, elements);
            AlertDialog dialog = new AlertDialog.Builder(this)
                    .setSingleChoiceItems(adapter, 1, null).create();

            TextView header = new TextView(this);
            header.setText("header");
            dialog.getListView().addHeaderView(header, null, false);

            return dialog;
        }

        return super.onCreateDialog(id);
    }
}

問題が発生するトリガはonDestroyで実行しているelements.clear()でした。
elementsはDialogが持つListViewに紐付けられたデータセットです。
タイミングによっては、onDestroyの後にDialogのListViewがelementsへアクセスする
ようで、HeaderViewListAdapter.isEnabledで範囲外を参照してしまうようです。

また、この例外は37行目のaddHeaderViewで、第三引数にfalseを渡した場合に発生します。
# つまりtrueを設定すると再現しない

●解決策

今回のケースだと、elementsをclearする理由がなかったので、これをやめるだけで対応
できたけれど、clearする必要がある場合はどうするのだろう。。。

stackoverflowをみると「Samsung phneだと起きる」という人がいたりするけれど、
今回の結果を見ると、組む側の問題のような気がします...

# 個人的にListHeaderViewやListFooterView周辺はよく不具合が出るので、
# なるべくなら使用を避けたいところです。

以上です。

Android:splitActionBarWhenNarrowでActionBarが分割されなくなる条件


ActionBarで、タブモードやリストモードとActionボタンを併用すると、表示スペースが足
りなくなることがよくあります。
これを解決する方法の1つにsplitActionBarWhenNarrowがあります。

splitActionBarWhenNarrowは、ActionBarに十分な表示領域が無い場合、
画面の上下にActionBarを分割してくれるUIオプションです。
実際に分割された画面は図1のようになります。
しかし、分割されない場合もあります(図2)

図1

図2
ほとんどの端末で横画面にするとActionBarが分割されません。
なぜでしょう?

●ActionBarが分割されない条件は?

splitActionBarWhenNarrowでActionBarを分割するには画面横幅480dp以上必要です。

ActionBarを分割するか否かを判断するロジックは、
com.android.internal.policy.impl.PhoneWindow.installDecorメソッドにあります。
if (splitWhenNarrow) {
    splitActionBar = getContext().getResources().getBoolean(
            com.android.internal.R.bool.split_action_bar_is_narrow);
boolリソースのsplit_action_bar_is_narrow値を読み込んでいるのがわかりますね。
このbool値は、下記のように定義されています。

・framework/base/core/res/res/values/bools.xml
<resources>
...
    <bool name="split_action_bar_is_narrow">true</bool>
</resources>
・framework/base/core/res/res/values-w480dp/bools.xml
<resources>
...
    <bool name="split_action_bar_is_narrow">false</bool>
</resources>
つまり、画面横幅が480dp以上ある場合はfalse(分割されない)というわけです。


●おわりに...

本当にw480dp未満であれば横画面でも分割されるのか試してみました。

QVGA(240x320 / Low dpi)のスキンで確認した結果↓



ちゃんと分割されますね。

以上です。

2012/07/09

Android:Menuボタンの廃止とActionBar


Android3.0 (Honeycomb)以前のアンドロイド端末はMenuボタンを備える必要がありました。
しかし、Android3.0で物理的なMenuボタンへの依存が排除されたため、Menuボタンを持た
ない端末が登場しました。


●Honeycomb以降のMenuボタンに変わるもの

Honeycomb以降は物理的なMenuボタンを持つ、あるいは持たない端末の両方をサポートする必要があります。(Menuキーを持たない端末としてGalaxy Nexusがあります)

Honeycomb以降でもオプションメニューはサポートされています。
オプションメニューにアクセスするためのUIとしてMenuボタンの代わりにActionButtonとOverflowButtonが用意されました。



●Gingerbread以前との互換性

Menuボタンを持たない端末でGingerbread以前を対象としたアプリを動作させた場合、
システムバー(Honeycomb)、ナビゲーションバー(ICS or later)にMenuボタンが表示されます。
※本稿ではActionBarのOverflowButtonと区別するためLegacyOverflowButtonと表記

(IceCreamSandwitch - ナビゲーションバー)
(Honeycomb - システムバー)

●表示されるのはOverflowButtonか?LegacyOverflowButtonか?

AndroidはOverflowMenuを表示すべきか、LegacyOverflowButtonを表示すべきかをアプリ
のuses-sdkタグで判断します。
  • targetSdkVersionが11以上(Honeycomb以降)の場合、LegacyOverflowButtonを非表示
  • これ以外の場合、Android3.0以降の端末ではLegacyOverflowButtonを表示
  • ただし、下記を全て満たす場合は例外的にLegacyMenuButtonを表示
     - minSdkVersionが10以前(つまりGingerbread以前)
     - targetSdkVersionが11~13(つまりHoneycomb~HoneycombMR2)
     - ActionBarを使用しない
     - Android4.0以降のスーマートフォン(ハンドセット)端末で実行
最後の例外については"Gingerbread以前のハンドセットとHoneycombのタブレットをサポ
ートするアプリ"と判断され、Menuキーが必要であると判断されます。

まとめると、
  • Honeycomb以降をサポートするアプリはOverflowMenuをサポートすること
  • Honeycombより前をサポートするアプリはLegacyOverflowButtonを表示
  • Gingerbread以前~HoneycombまでをサポートするアプリはLegacyOverflowButtonを表示
となります。

実際にどのように表示されるのかを下記にまとめます。

【Honeycomb以降のMenuボタンを持たない端末で...】
・Honeycomb以降をターゲットにしたアプリ
オプションメニューはActionBar内にあるActionButtonあるいはActionOverflowに集約さ
れます。ユーザはこれらのUIを通してオプションメニューにアクセスします。


・Honeycombより前をターゲットにしたアプリ
ナビゲーションバーにlegacyOverflowButtonを表示します。
これにより、Honeycomb以前をターゲットにしたアプリとの互換性を保ちます。


・Honeycombをターゲットにしたアプリ
下記全てを満たすアプリはナビゲーションバーにLegacyOverflowButtonが表示されます。
 - minSdkVersionが10以前(つまりGingerbread以前)
 - targetSdkVersionが11~13(つまりHoneycomb)
 - ActionBarを使用しない
 - Android4.0以降のスーマートフォン(ハンドセット)端末で実行

下記全てを満たすアプリは、ユーザがオプションメニューにアクセスできなくなります。
 - minSdkVersionが10以前(つまりGingerbread以前)
 - targetSdkVersionが11~13(つまりHoneycomb)
 - ActionBarを使用しない
 - Android3.x系タブレット端末で実行


・ICSをターゲットにしたアプリ
下記全てを満たすアプリは、ユーザがオプションメニューにアクセスできなくなります。
 - targetSdkVersionが14以上(つまりICS以降)
 - ActionBarを使用しない
 - Menuキーを搭載しない端末で実行


【Honeycomb以降のMenuボタンを持つ端末で...】
・Honeycomb以降をターゲットにしたアプリ
ユーザはMenuボタンを通してオプションメニューにアクセスします。
従来(Honeycombより前)と同じスタイルですが、いくつかのメニューアイテムを
ActionButtonとして提供することも可能です。



●Gingerbread以前~Honeycomb以降をサポートする

Honeycomb以降はActionBarを備えている必要があります。
しかしながら、Gingerbread以前はActionBarが搭載されていません。
この差を埋めるテクニックにシステムバージョン修飾子を使用したものがあります。

サンプルコード
・res/values/styles.xml
<resources>
    <style name="AppTheme" parent="android:Theme.Light" />
</resources>
・res/values-v11/styles.xml
<resources>
    <style name="AppTheme" parent="android:Theme.Holo.Light" />
</resources>
・AndroidManifest.xml
<application
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme" >
以上です。
2012/07/05

Android:高速化。ContentResolver?ContentProviderClient?


ContentProviderClientというクラスがあります。
これはContentResolver同様、ContentProviderとやり取りするためのクライアントです。

アプリからこれを使用するにはContentResolverのacquireContentProviderClient(Uri)
またはacquireContentProviderClient(String)を呼び出してインスタンスを取得すします。

ContentProviderClientのAPIを見るとContentResolverと似ていることがわかります。
ContentProviderClient:Android Developers


●ContentResolverとContentProviderClientの違いは何か?

ContentResolverはDBへ問合せる時、毎回コンテンツURIのAUTHORITYをキーに
ContentProviderを検索します。
そして、問合せ結果のcursorがcloseされるとContentProviderへの参照を破棄します。

ContentProviderClientは検索して取得したContentProviderへの参照を保持し続けます。
これは、問合せ結果のcursorがcloseされても続きます。
ContentProviderClientは、ContentResolverがクエリ発行の度にContentProviderを検索
する処理を省略する分高速に動作します。

また、ContentResolverと異なり、ContentProviderClientはqueryとopenFileメソッドは
スレッドセーフではありません。


●ContentProviderClientを扱う場合の注意点

開発者はContentProviderClientが不要となった場合にこれを明示的に破棄(release() )
する必要があります。
これを怠ると、システムがContentProviderの必要・不要を判断できないためリークに繋
がります。


●ContentProviderプロセスの死亡とクライアントプロセスの死亡

ContentResolver経由でCursorオブジェクトを取得した場合、Cursor取得~Cursor.close()
までの間に対象のContentProviderプロセスが死亡すると、呼び出し側のプロセスも道連れ
となります。

例えば、あなたのアプリが電話帳データを取得している最中にacoreプロセスが死亡すると、
あなたのアプリプロセスも強制終了されます。
これは、ContentProviderプロセスが死亡した時、これに依存するプロセスも強制終了させ
るようシステムがクリーンアップする仕組みです。

ContentProviderClientの場合は、ContentProviderClientの取得~破棄(release())まで
となります。

ContentProviderClientはその性質上、キャッシュされる期間がonResume~onPauseに相当
する程長くなりがちです。
ContentResolverの強制終了され得る範囲がCursor取得~Cursor.close()であることを
考えると、ContentProviderClientが強制終了され得る範囲は非常に長くなります。
※これについてはJellyBean以降、関連するAPIが追加されました


●それぞれのメリット・デメリット

ContentProviderClientはContentResolverと比べて高速に動作するメリットがあります。

軽量な電話帳データ1件をqueryで取得した場合のパフォーマンスは下記です。
平均:125ms (ContentResolver)
平均:23ms  (ContentProviderClient)
ContentProviderClientの方が5倍以上高速です。
# 両者の差はContentProviderの検索時間がほとんどなので、1度に大量のデータを取得す
# るような場合、この恩恵はあまり感じられないかもしれません。

ただし、ContentProviderClientは明示的に破棄(release())する必要があるため、
別途管理しないといけないデメリットがあります。
また、ContentProviderの強制終了による"道連れ強制終了"を少なからず考慮する必要が
あるでしょう。
※これについてはJellyBean以降、関連するAPIが追加されました

もし、あなたのアプリがContentResolver経由で軽量なクエリを複数回呼び出しているよ
うであれば、ContentProviderClientに置き換えることで大きなパフォーマンス向上が見込
めます。
ただし、ContentProviderClient(release())を管理することを忘れてはいけません。


●サンプルコード

ContentProviderClientを取得するacquireContentProviderClientメソッドの引数には、
対象のContentProviderを取得できるURIを指定します。
@Override
protected void onResume() {
    super.onResume();
    initContentProviderClient();
}

@Override
protected void onPause() {
    super.onPause();
    releaseContentProviderClient();
}

private void initContentProviderClient() {
    releaseContentProviderClient();
    mContentProviderClient = getContentResolver()
            .acquireContentProviderClient(
                    ContactsContract.Contacts.CONTENT_URI);
}

private void releaseContentProviderClient() {
    if (mContentProviderClient != null) {
        mContentProviderClient.release();
    }
}

private void onUpdate() {
    Cursor c = null;
    try {
        c = mContentProviderClient.query(
                ContactsContract.Contacts.CONTENT_URI,
                null, null, null, null);
    } catch (RemoteException e) {
        // ContentProviderがDeadObject化している。
        // 必要に応じて、ContentProviderClientの再取得とリクエリを実施
        initContentProviderClient();
    } finally {
        if (c != null) {
            try {
                c.close();
            } catch (Exception e) {
                // N.O.P
            }
        }
    }
}

●JellyBeanで拡張されたAPI acquireUnstableContentProviderClient

JellyBeanで下記のAPIが追加されました。

public final ContentProviderClient acquireUnstableContentProviderClient(String)
public final ContentProviderClient acquireUnstableContentProviderClient(Uri)

既存のacquireContentProviderClientメソッドとほぼ同じですが、下記の点で異なります。

ContentProviderClientへの参照を保持している間に、対象のContentProviderが強制終了
してもクライアントプロセスは強制終了されません。
"ContentProviderプロセスの死亡とクライアントプロセスの死亡"
で述べた"道連れ強制終了"が上記のAPIを呼び出して取得したContentProviderClientでは
発生しないということです。

このメソッドは、対象のContentProviderが安全でない、あるいは信頼できない場合に
使用します。

ContentProviderが強制終了された後、このContentProviderにリンクするContentProviderClient
を経由してクエリを発行するとDeadObject例外が投げられます。
その場合、catch節の中で古いContentProviderClientを破棄(release())して、再生成する
必要があります。

サンプルコードは、先ほどのコードにある
acquireContentProviderClient

acquireUnstableContentProviderClient
に置き換えるだけで動作するでしょう。

以上です。

2012/07/04

Android:UPナビゲーションをカスタマイズする

前回の投稿ではJellyBeanでのUPナビゲーションについて触れました。

今回は前回に残課題とした
「UPナビゲーションでparentActivityを起動するIntentに介入する」
の調査結果となります。

後述しますが、UPナビゲーションについてのより深い考察は下記サイトを参照。
BackとUPナビゲーションデザイン
BackとUPナビゲーションの提供
UPナビゲーションについて
タスクとバックスタック


●動機

AndroidManifest.xmlに定義されたActivity要素のparentActivityName属性で提供される
UPナビゲーション。
しかし、デフォルトの挙動ではアプリへの要求を満たせない場合があります。
例えば、単に親Activityを起動するのではなく、起動Intentにextra値を持たせて親Activity
の状態に関与したい場合などです。


●拡張されたAPI

JellyBean以降、Activityクラスが拡張されてUPナビゲーションに介入するためのメソッド
が追加されました。
主要なメソッドを下記に取り上げます。


public boolean onNavigateUp ()
このメソッドはユーザがアクションバーのUPナビゲーションを選択する度に呼ばれます。

もし、AndroidManifest.xmlのparentActivityNameで親Activitを指定していれば標準の
UPナビゲーションが自動で実行されます。
もし"親Activityチェーン"に沿う任意のActivityが、Intentにexstra値を必要とするなら
ば、onPrepareNavigateUpTaskStack(TaskStackBuilder)メソッドをオーバーライドする必
要があります。

UPナビゲーションのカスタマイズにあたっては、下記を参照しておきましょう。
BackとUPナビゲーションデザイン
タスクとバックスタック
TaskStackBuilderクラス
・Activity.getParentActivityIntent()
・Activity.shouldUpRecreateTask(Intent)
・Activity.navigateUpTo(Intent)
・SDKのAppNavigationサンプルアプリ

Return:
trueの場合、ナビゲーションは正常に完了し、自Activityはfinishされます。

---

public void onPrepareNavigateUpTaskStack (TaskStackBuilder builder)
異なるタスクからのUPナビゲーションで生成される合成タスクスタックを準備します。

このメソッドはonCreateNavigateUpTaskStackによって構築されたIntentを持つ
TaskStackBuilderを受け取ります。
もし、新たなタスクを起動する前にIntentにextra値を追加したい場合は、このメソッド
をオーバーライドして、ここでデータを追加する必要があります。

Param:
builder
onCreateNavigateUpTaskStackによってIntent構築済みのTaskStackBuilder。

---

public void onCreateNavigateUpTaskStack (TaskStackBuilder builder)
UPナビゲーションにより、異なるタスクが生成される場合に呼ばれます。
# つまりはshouldUpRecreateTask==trueの状態。

このメソッドのデフォルト実装は、AndroidManifest.xmlでparentActivityNameに指定し
た親ActivityをTaskStackBuilderの親Activityチェーンに追加します。

このメソッドは、getParentActivityIntent()で取得したIntentが
shouldUpRecreateTask(intent)でfalseを返すものである場合、onNavigateUp()から呼ば
れるのが標準の動作です。

もし、通常とは異なる方法でタスクスタックを生成した場合はこのメソッドをオーバーラ
イドできます。

Param:
builder
空のTaskStackBuilderです。
これに目的のタスクスタックを表現するIntentを追加します。

---

public Intent getParentActivityIntent ()
このActivityがAndroidManifest.xmlでparentActivityNameに指定した親Activityを起動
するためのIntentを取得します。

親Activityを起動するIntentを変更するにはこのメソッドをオーバーライドします。
super.getParentActivityIntent()を呼べば、純粋なUPナビゲーション用のIntentが作成
されます。

Return:
このActivityの親として定義されたActivityを起動するためのIntent。
有効な親がいない場合はnullを返します。

---

public boolean shouldUpRecreateTask (Intent targetIntent)
引数のtargetIntentを使用したUPナビゲーションを実現する時に、タスクを再生成すべき
かどうかを判定します。
false(タスクを生成すべきでない場合)は、同引数をとるnavigateUpTo(Intent)を呼び出
すことで正しくナビゲーションされます。

Params:
targetIntent
UPナビゲーションでの送信先(親Activity)をターゲットにしたIntent。

Return:
trueである場合、UPナビゲーションは新しいタスクスタックを生成すべき。
falseである場合、Intentは同じタスクに対して送信されるべき。

---

public boolean navigateUpTo (Intent upIntent)
このActivityから、引数upIntentで特定されるActivity(親)にナビゲートする時に呼ばれ
ます。
この過程で自Activityはfinishされます。

もしupIntentで指定されたActivityがヒストリスタックに存在する場合、自Activityと、
それよりも上のActivityが破棄されます(Like FLAG_ACTIVITY_CLEAR_TOP)

もしupIntentで指定されたActivityがヒストリスタックに存在しない場合、自タスクの
ルートActivityに到達するまでにある各Activityを破棄しルートActivityを起動します。
(これはアプリのホームActivityに戻るような動作です)
これは"Activityが正規の親Activityを通過しないで到達可能"といった、複雑なナビゲー
ション階層をもつアプリケーションで有効な方法です。

このメソッドは同じタスクを宛先とするUPナビゲーションの実行で呼ばれます。
もしタスクを跨ぐUPナビゲーションが必要な場合はshouldUpRecreateTask(Intent)を参照。

Param:
upIntent
UPナビゲーションでの送信先(親Activity)をターゲットにしたIntent。

Return:
trueの場合、ナビゲーションは正常にupIntentで指定されたActivityまで到達可能であり、
それが配信されたことを示します。
falseの場合、upIntentで指定されたActivityを見つけることができなかったことを示します。
この時、自Activityは単純にfinishするのみです。

---

●JellyBeanより古い環境で

ICS以前で同等の処理を実装したい場合は互換性ライブラリを使用する必要があります。

TaskStackBuilder(v4版)
NavUtil

下記サンプルです。
@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case android.R.id.home:
            Intent upIntent = new Intent(this, ParentActivity.class);
            if (NavUtils.shouldUpRecreateTask(this, upIntent)) {
                // 下記のような状態のバックスタックを生成
                // | ParentActivity            | (peek)
                // |---------------------------|
                // | GrandParentActivity       |
                // |---------------------------|
                // | GreateGrandParentActivity |
                // |---------------------------|
                TaskStackBuilder.from(this)
                        .addNextIntent(new Intent(this, GreatGrandParentActivity.class))
                        .addNextIntent(new Intent(this, GrandParentActivity.class))
                        .addNextIntent(upIntent)
                        .startActivities();
                finish();
            } else {
                // ParentActivityは自タスク上に生成される。
                // シンプルなUPナビゲーション
                NavUtils.navigateUpTo(this, upIntent);
            }
            return true;
    }
    return super.onOptionsItemSelected(item);
}


●UPナビゲーションのデフォルトの挙動

UPナビゲーションのひょう動作を解析。

・同じアプリケーションタスクへのUPナビゲーション
メソッドコール
onNavigateUp
  |-- getParentActivityIntent
  |    親ActivityのComponentNameを持つIntentを生成
  |
  |-- shouldUpRecreateTask
  |    同アプリタスクへのUPナビゲーションのため戻り値はfalse
  |
  |-- navigateUpTo
       ヒストリスタックに親Activityがいる場合true。そうでない場合false


・異なるアプリケーションタスクへのUPナビゲーション
メソッドコール
onNavigateUp
  |-- getParentActivityIntent
  |    親ActivityのComponentNameを持つIntentを生成
  |
  |-- shouldUpRecreateTask
  |    異なるアプリケーションタスクへのUPナビゲーションのため戻り値はtrue
  |
  |-- onCreateNavigateUpTaskStack
  |   | ここでTaskStackBuilderが構築されていく
  |   |
  |   |-  getParentActivityIntent
  |          親ActivityのComponentNameを持つIntentを生成
  |
  |-- onPrepareNavigateUpTaskStack
       構築済みTaskStackBuilderが渡され、この後タスク生成とActivity起動を開始


・存在しない不正な親Activity名を指定した場合
onNavigateUp
  |-- getParentActivityIntent
  |    親ActivityのComponentNameを持つIntentを生成
  |
  |-- shouldUpRecreateTask
  |    存在しないActivityの場合、タスクを生成すべきでないため戻り値はfalse
  |
  |-- navigateUpTo
       メソッドの結果はfalseとなり、ルートActivityまで遡って起動

AndroidManifest.xmlでparentActivityNameに存在しない不正な親Activityを指定しても
ActivityNotFoundExceptionは発生しないことがわかる。

以上です。

2012/07/03

Android:Upナビゲーションを実現するparentActivityName属性


JellyBeanでUPナビゲーションに関するAPIが追加されています。

AndroidManifest.xmlで定義するactivity要素のandroid:parentActivityName属性が新設
されました。
ここに"親"となるActivity(parentActivity)を指定することにより、UPナビゲーシ
ョンの遷移先を指定することができます。
http://developer.android.com/guide/topics/manifest/activity-element.html#parent

開発者はandroid:parentActivityNameを指定するだけで、UPナビゲーションを実現できます。
下記のようなonOptionsItemSelected()でandroid.R.id.homeを拾うようなコードはもう不
要です。
@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case android.R.id.home:
            // app icon in Action Bar clicked; go home
            Intent intent = new Intent(this, HomeActivity.class);
            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
            startActivity(intent);
            return true;
        default:
            return super.onOptionsItemSelected(item);
    }
}
この機能を使用することで、今まで暗に推奨されていたルール(UPナビゲーションで発行
するIntentにはFLAG_ACTIVITY_CLEAR_TOPを指定する等)を守る責任を開発者が背負う必要
がなくなります。
# これで、UPナビゲーションの動作が端末上で統一されますね。

下記はandroid:parentActivityNameを指定した簡単なサンプルです。

<activity
    android:name=".ChildActivity"
    android:label="@string/title_activity_child"
    android:parentActivityName=".ParentActivity" >
    <intent-filter>
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>
---
2012/07/05 加筆・修正
一部誤解を招きそうな文章となっていたのを修正。
parentActivityNameで指定したActivityのタスクアフィニティが、自Activityの現在属している
タスクアフィニティと異なる場合に動作が変わる旨を追記
---

android:parentActivityName版UPナビゲーションの動作は非常にシンプルです。
基本は親Activityを起動するIntentをFLAG_ACTIVITY_CLEAR_TOPのような効果を付けて起
動します。

ただし、自Activityが現在属しているタスクアフィニティと、親Activityのタスクアフィ
ニティが異なる場合、UPナビゲーションは新たにタスクを生成して、そこに親Activityを
配置します。

また、android:parentActivityNameで指定した親Activity名に不正(存在しない)Activity
名を指定してもActivityNotFound例外は発生しません。
これは、UPナビゲーションは"親Activityの起動"ではなく、ナビゲーション階層の上部へ
の移動に重きを置いているためでしょう。

下記に各シーン毎のUPナビゲーション動作を列挙します。
(parentActivityのlaunchModeはstandardを指定)

●前提条件:
自Activityが現在属しているタスクアフィニティと、親Activityのタスクアフィニティが
同じ場合

【+親Activityが自Activityと同じスタック上に存在する場合】
parentActivityより上にあるActivity(自Activity含む)は破棄される。
また、parentActivityも再表示ではなく再生成される。


【+親Activityが自Activityと同じスタック上に複数存在する場合】
Activityスタック上でよりpeekに近い(最も上にある)Activityの位置に遷移する。
これ以外は"親Activityが自Activityと同じスタック上に存在する場合"と同様


【+親Activityが自Activityと同じスタック上に存在しない場合】
ルートActivity、またはランチャーが再表示される(再生成ではない)。


●前提条件:
自Activityが現在属しているタスクアフィニティと、親Activityのタスクアフィニティが
異なる場合

UPナビゲーションを契機に新たにタスクを生成します。
親Activityはこのタスクに生成され、自Activityは破棄されます。

後者の前提条件は例えば下記のようなケースで起こりえます。
http://developer.android.com/design/media/navigation_between_apps_up.png

詳しくは下記を参照
http://developer.android.com/intl/ja/design/patterns/navigation.html



●おわりに...


この機能はUPナビゲーションの実装を容易にします。
しかし、その反面UPナビゲーションを細かく制御することができません。
parentActivityを起動するIntentに関与したくなるケースがあるかもしれません。

この問題を解決するためにActivityクラスが拡張され、また新規クラスが作成されました。
http://developer.android.com/sdk/api_diff/16/changes/android.app.Activity.html
http://developer.android.com/reference/android/app/TaskStackBuilder.html

Activityには、アップナビゲーションに関するonNavigateUp()や
onPrepareNavigateUpTaskStack()が追加されました。
また、クロスタスクナビゲーションを実現するための合成バックスタックを構築するため
のユーティリティクラスTaskStackBuilderが新設されました。
UPナビゲーションのカスタマイズ方法は次の投稿にまとめます。

以上です。

2012/07/01

Android:Serviceの基本とonStartCommandの戻り値による動作の違い


開始状態
startService()の呼び出しでサービスを開始すると、サービスは"開始状態"となります。
"開始状態"となったサービスは、システムからkillされるか明示的に終了しない限り停止しません。

バインド状態
bindService()を呼び出してサービスにバインドすると"バインド状態"となります。
バインドされたサービスは、バインドしたコンポーネントとの双方向通信やメッセージに
よるやりとりを提供します。
"バインド状態"のサービスは全てのコンポーネントがらアンバインドされることで停止し
ます。

開始+バインド状態
サービスがstartService()とbindService()両方で起動されると"開始状態"かつ
"バインド状態"となります。
この状態のサービスは、両方の状態の停止条件を満たした場合に停止します。
つまり、全てのコンポーネントからアンバインドされてもstopService/stopSelfが呼ばれ
ないと停止しません。
反対に、stopService/stopSelfが呼ばれても全てのコンポーネントからアンバインドされ
ないと停止しません。
この状態をとるサービスの実装は複雑になりがちです。可能であれば分割するほうが良い
でしょう。


基本メソッド

onStartCommand()
startServiceでサービスが開始要求を受けたときのコールバックです。
このメソッドはサービスの開始ポイントとなります。
このメソッドの戻り値は、サービスがシステムから不意にkillされた場合の動作を決定し
ます。
bindServiceを呼び出してサービスをバインドした場合、このメソッドは呼ばれません。

onBind()
bindServiceサービスがバインドされた時のコールバックです。
サービスがバインドを拒否したい場合はnullを返すようにします。

onCreate()
サービスが最初に作成された時のコールバックです。
このメソッドはonStartCommandやonBindよりも前に呼び出され、サービスが起動中の場合
は呼ばれません。

onDestroy()
サービスが破棄される時のコールバックです。



サービスの強制終了

サービスはシステムによる強制終了と再起動を考慮した設計にする必要があります。
サービスがバックグラウンドで長時間実行し続けるほど、システムにより強制終了を受け
る確率が高くなります。
システムによりサービスが強制終了されても、リソースが再利用可能になればサービスを
再起動します。
ただし、onStartCommandの戻り値によってサービスの強制終了時の動作を買えることは
可能です。



onStartCommandの戻り値

onStartCommandメソッドの戻り値でサービス強制終了の振る舞いを制御することができます。

START_NOT_STICKY
サービスを起動するペンディングインテントが存在しない限りサービスは再起動されません 。
強制終了によりサービスが終了した場合、勝手な再起動を防ぐ場合にはこれを使用します。

START_STICKY
システムはサービスを新たにインスタンス化し、サービスの再起動を行います。
サービスを起動するペンディングインテントが存在しない場合、システムはintentをnull
にしてonStartCommandを呼び出します。
つまり、onStartCommandの引数intentがnullである状態があり得ます。
また、startServiceによりサービスを複数回起動していたとしても再起動は1度しか行わ
れません。

START_REDELIVER_INTENT
システムはサービスを新たにインスタンス化し、サービスの再起動を行います。
再起動時のonStartCommandには、強制終了前と同じ内容のIntentが渡されます。
再起動順序は強制終了前の起動順序と同じです。(A⇒Bで起動した場合、A⇒Bで再起動)
また、startServiceによりサービスを複数回起動していた場合は、起動した回数分
onStartCommandが呼ばれます。

START_STICKY_COMPATIBILITY
システムにより再起動されることが保障されません。
これはSTART_STICKYとの互換性のために用意されています。
このモードが指定されている場合、onStartCommandの引数intentにnullが格納されること
はありません。


各戻り値を指定した場合、再起動のonStartCommandの引数がどうなるかを検証
(見方)
●<戻り値に指定する定数名>
onStartCommand #<サービスインスタンスID>
- 引数intentの内容
- 引数flagsの内容(0x01:START_FLAG_REDELIVERY / 0x02:START_FLAG_RETRY)
- 引数startIdの内容
--------------------

●START_NOT_STICKY
- 再起動前
onStartCommand #1098310008
- intent  = Intent { cmp=yuki.test/.ServiceTest$MyService }
- flags   = 0
- startId = 1

... 強制終了 ...

- 再起動後

再起動されない。
--------------------

●START_STICKY
- 再起動前
onStartCommand #1098310008
- intent  = Intent { cmp=yuki.test/.ServiceTest$MyService }
- flags   = 0
- startId = 1
onStartCommand #1098310008
- intent  = Intent { cmp=yuki.test/.ServiceTest$MyService }
- flags   = 0
- startId = 2

... 強制終了 ...

- 再起動後
onStartCommand #1098245632
- intent  = null
- flags   = 0
- startId = 2

再起動前はサービスを2つ起動しているが、再起動後のサービス起動は1つだけ。
--------------------

●START_STICKY_COMPATIBILITY
- 再起動前
onStartCommand #1098310008
- intent  = Intent { cmp=yuki.test/.ServiceTest$MyService }
- flags   = 0
- startId = 2

... 強制終了 ...

- 再起動後

onStartCommandが呼ばれない(ただしサービスのonCreateはされている)
これは端末or端末状態依存のような気がする。
--------------------

●START_REDELIVER_INTENT
- 再起動前
onStartCommand #1098310008
- intent  = Intent { cmp=yuki.test/.ServiceTest$MyService }:1098309208
- flags   = 2
- startId = 1
onStartCommand #1098310008
- intent  = Intent { cmp=yuki.test/.ServiceTest$MyService (has extras) }:1098314896
- flags   = 2
- startId = 2

... 強制終了 ...

- 再起動後
onStartCommand #1098245632
- intent  = Intent { cmp=yuki.test/.ServiceTest$MyService }:1098225896
- flags   = 3
- startId = 1
onStartCommand #1098245632
- intent  = Intent { cmp=yuki.test/.ServiceTest$MyService (has extras) }:1098226304
- flags   = 3
- startId = 2

再起動前と同じ順番で再起動してくれる。
Intentも渡ってきています。
--------------------



どうやらSTART_STICKYはSTART_REDELIVER_INTENTに比べて再起動が早いようで、強制終了
から約10秒程で再起動してきます。
START_REDELIVER_INTENTは再起動までに約1分程かかるようです。

flagsの値は端末によって異なるようです。
START_FLAG_REDELIVERYのON/OFFは期待通りですが、START_FLAG_RETRYが初回起動時でON
になる端末があったり、そうならない端末があったりします。
この辺の詳細は未調査。

以上です。