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
になる端末があったり、そうならない端末があったりします。
この辺の詳細は未調査。

以上です。