2011年7月12日火曜日

Android AppWidget の PendingIntent で putExtra するときの注意

AppWidget 上のボタンをタップしたときに編集画面を開いたりする場合、RemoteViews を使った次のようなコードがよく紹介されています。


RemoteViews remoteView = new RemoteViews(context.getPackageName(), R.layout.appwidget);
Intent intent = new Intent(context, WidgetConfigure.class);

PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
// or
// PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0);
// or
// PendingIntent pendingIntent = PendingIntent.Broadcast(context, 0, intent, 0);

remoteView.setOnClickPendingIntent(R.id.button, pendingIntent);


画面に同じアプリのウィジェットが複数個ある場合、AppWidgetId を Intent の extras に詰めて渡したくなります。なるでしょう?

そこで


Intent intent = new Intent(context, WidgetConfigure.class);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent,0);


とやるとうまくいきません。画面にウィジェットが複数ある場合、どれをタップしてもどれか1つのボタンが押された挙動になってしまいます。

正しく動かすには、こう


PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, 0);


---
注意:

PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
// or
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, PendingIntent.FLAG_CANCEL_CURRENT);

でも動きますが、今回の目的では第4引数フラグを指定する必要はありません。
詳しくはこの後で説明します。
---

PeindingIntent の getActivity() (getService(), getBroadcast() も同じ) の第2引数に AppWidget の ID (同じアプリの各 AppWidget で一意になるならなんでもよい)を指定します!

ちょっと、ちゃんと PendingIntent の説明を読んでみましょう。

---
A description of an Intent and target action to perform with it. Instances of this class are created with getActivity(Context, int, Intent, int), getBroadcast(Context, int, Intent, int), getService(Context, int, Intent, int); the returned object can be handed to other applications so that they can perform the action you described on your behalf at a later time.

By giving a PendingIntent to another application, you are granting it the right to perform the operation you have specified as if the other application was yourself (with the same permissions and identity). As such, you should be careful about how you build the PendingIntent: often, for example, the base Intent you supply will have the component name explicitly set to one of your own components, to ensure it is ultimately sent there and nowhere else.

A PendingIntent itself is simply a reference to a token maintained by the system describing the original data used to retrieve it. This means that, even if its owning application's process is killed, the PendingIntent itself will remain usable from other processes that have been given it. If the creating application later re-retrieves the same kind of PendingIntent (same operation, same Intent action, data, categories, and components, and same flags), it will receive a PendingIntent representing the same token if that is still valid, and can thus call cancel() to remove it.
---

Intent と、その Intent を実行したい Action の記述。このクラスのインスタンスは getActivity(Context, int, Intent, int), getBroadcast(Context, int, Intent, int), getService(Context, int, Intent, int); で生成されます。後で他のアプリケーションが記述されたアクションを実行できるように、これらのメソッドで返されるオブジェクトは他のアプケーションに受け渡すことができます。

PendingIntent を他のアプリケーションに渡すことで、他のアプリケーションが自分自身であるかのように(同じパーミッションとIDで)指定した操作を実行する権限を付与します。
このようなことから、PendingIntent をどのように作成するのかについて注意するべきです。よく起こることで、例えば、あなたが渡す base Intent が他のところではなく最終的にそこに届けられることを確認するために、あなた自身のコンポーネントの1つとして明示的に設定された component name を持つようにしてください。

PendingIntent 自身は、単にシステムによって管理されるトークンへのリファレンスです。これは、PendingIntent を受信するために使用されるオリジナルデータを記述しています。
これはつまり、自身のアプリケーションプロセスが殺されても、PendingIntent 自身は他のプロセス(すでに受け取っている)から使用可能な状態のまま残ることを意味しています。もし作成したアプリケーションがあとで同じ種類の PendingIntent を再取得した場合(同じ操作、同じアクション、データ、カテゴリ、コンポーネント、フラグ)、同じトークンがまだ有効な場合、その同じトークンを代表する PendingIntent を受信します。したがって、cancel() を呼んでそれを削除することができます。

---

ふむふむ。

---

public static PendingIntent getActivity (Context context, int requestCode, Intent intent, int flags)

Since: API Level 1
Context.startActivity(Intent) を呼ぶように、新しい Activity を開始する PendingIntent を取得する。既存の activity の context の外で新しい activity が開始されるので、Intent に Intent.FLAG_ACTIVITY_NEW_TASK launch flag を使わなければならない。

Parameters

context : この PendingIntent が activity を開始する context
requestCode : sender 用のプライベートな request code
intent : 起動される Activity の Intent
flags : FLAG_ONE_SHOT, FLAG_NO_CREATE, FLAG_CANCEL_CURRENT, FLAG_UPDATE_CURRENT のいずれか。もしくは Intent.fillIn() でサポートされる全てのフラグ。intent の指定されてない部分のコントロール用。実際の送信が起こったときに intent に適用される。

Returns

与えられたパラメータにマッチする既存の、もしくは新しい PendingIntent を返す。FLAG_NO_CREATE が与えられたときのみ null が返る場合がある。

---

ふむふむふむ。
flags に指定する。FLAG_ONE_SHOT, FLAG_NO_CREATE, FLAG_CANCEL_CURRENT, FLAG_UPDATE_CURRENT の説明も見てみるか。

---

Constants

public static final int FLAG_CANCEL_CURRENT

Since: API Level 1
getActivity(Context, int, Intent, int), getBroadcast(Context, int, Intent, int), getService(Context, int, Intent, int) 用のフラグ : 記述された PendingIntent がすでに存在していた場合、新しいのが生成される前に現在のはキャンセルされる。Intent の extra data だけを変更する場合に新しい PendingIntent を取得するのに使える。以前の pending intent をキャンセルすることによって、新しいデータを与えられた実体だけが起動できることを保証する。この保証が問題はない場合、FLAG_UPDATE_CURRENT を検証する。

public static final int FLAG_NO_CREATE

Since: API Level 1
getActivity(Context, int, Intent, int), getBroadcast(Context, int, Intent, int), getService(Context, int, Intent, int) 用のフラグ : 記述された PendingIntent がまだ存在していない場合、生成せずに単に null を返す。

public static final int FLAG_ONE_SHOT

Since: API Level 1
getActivity(Context, int, Intent, int), getBroadcast(Context, int, Intent, int), getService(Context, int, Intent, int) 用のフラグ : この PendingIntent は1度だけ使える。このフラグがセットされた場合、send() が呼ばれた後に試みた send は自動的にキャンセルされる。

public static final int FLAG_UPDATE_CURRENT

Since: API Level 3
getActivity(Context, int, Intent, int), getBroadcast(Context, int, Intent, int), getService(Context, int, Intent, int) 用のフラグ : 記述された PendingIntent がすでに存在している場合、それをキープして extra data を新しい Intent のものに置き換える。extras だけを変えた intent を作成し、以前の PendingIntent が受け取った実体が、明示的に与えなくても新しい extras で起動できるのを気にしない場合に使える。

---

FLAG_UPDATE_CURRENT で問題ない場合は、こっちを考えてねって FLAG_CANCEL_CURRENT にあるので、FLAG_UPDATE_CURRENT の方がいいのかな?


第2引数についても検証してみた。
リファレンスには

---

requestCode Private request code for the sender (currently not used).

---

(currently not used) = 現在は使っていない

って書いてあるけど、どうもそうではないみたい。

例えば、同じ AppWidget を画面に2個配置して、それぞれに


Intent intent = new Intent(context, WidgetConfigure.class);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
views.setOnClickPendingIntent(R.id.widgetbase, pendingIntent);


を指定すると、先に追加した方の PendingIntent はキャンセルされるので、先に張り付けた AppWidget のボタンをタップしても反応しなくなります(PedingIntent が実行されなくなる)。

これは、extras 以外が同じ PendingIntent なので、以前のがキャンセルされるからです。
これを


Intent intent = new Intent(context, WidgetConfigure.class);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, PendingIntent.FLAG_CANCEL_CURRENT);
views.setOnClickPendingIntent(R.id.widgetbase, pendingIntent);


にすると、先に張り付けた AppWidget のボタンもちゃんと反応します。
extras 以外に appWidgetId も違うので、同じ PendingIntent だと判定されないのでキャンセルされないからです。

FLAG_UPDATE_CURRENT も同じことです。


Intent intent = new Intent(context, WidgetConfigure.class);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
views.setOnClickPendingIntent(R.id.widgetbase, pendingIntent);


を指定すると、先に追加した方の PendingIntent はキャンセルされないので起動できますが、新しい Extras が適用されてしまう(FLAG_UPDATE_CURRENT のところに説明されている通り) ので、どの AppWidget でも同じ Extras の Activity が起動されることになります。

これを


Intent intent = new Intent(context, WidgetConfigure.class);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
views.setOnClickPendingIntent(R.id.widgetbase, pendingIntent);


にすれば、同じ PendingIntent と判定されないので、以前の Extras が新しいのに置き換わることはなくなります。


。。。あれれ、結局第2引数で同じ PendingIntent と判定されないようにするなら、第4引数いらなくね?

てことで、


Intent intent = new Intent(context, WidgetConfigure.class);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
PendingIntent pendingIntent = PendingIntent.getActivity(context, appWidgetId, intent, 0);
views.setOnClickPendingIntent(R.id.widgetbase, pendingIntent);


でいいじゃん!ちゃんと意図した通り動きました。


ちなみに、コードをちょっと追ってみました。

PendingIntent.java#189

189 public static PendingIntent getActivity(Context context, int requestCode,
190 Intent intent, int flags) {
191 String packageName = context.getPackageName();
192 String resolvedType = intent != null ? intent.resolveTypeIfNeeded(
193 context.getContentResolver()) : null;
194 try {
195 IIntentSender target =
196 ActivityManagerNative.getDefault().getIntentSender(
197 IActivityManager.INTENT_SENDER_ACTIVITY, packageName,
198 null, null, requestCode, intent, resolvedType, flags);
199 return target != null ? new PendingIntent(target) : null;
200 } catch (RemoteException e) {
201 }
202 return null;
203 }


ActivityManagerNative.java#2233

2233 public IIntentSender getIntentSender(int type,
2234 String packageName, IBinder token, String resultWho,
2235 int requestCode, Intent intent, String resolvedType, int flags)
2236 throws RemoteException {
2237 Parcel data = Parcel.obtain();
2238 Parcel reply = Parcel.obtain();
2239 data.writeInterfaceToken(IActivityManager.descriptor);
2240 data.writeInt(type);
2241 data.writeString(packageName);
2242 data.writeStrongBinder(token);
2243 data.writeString(resultWho);
2244 data.writeInt(requestCode);
2245 if (intent != null) {
2246 data.writeInt(1);
2247 intent.writeToParcel(data, 0);
2248 } else {
2249 data.writeInt(0);
2250 }
2251 data.writeString(resolvedType);
2252 data.writeInt(flags);
2253 mRemote.transact(GET_INTENT_SENDER_TRANSACTION, data, reply, 0);
2254 reply.readException();
2255 IIntentSender res = IIntentSender.Stub.asInterface(
2256 reply.readStrongBinder());
2257 data.recycle();
2258 reply.recycle();
2259 return res;
2260 }


requestCode もちゃんと Parcel に入れられてます = 使ってるじゃん!


2244 data.writeInt(requestCode);



   

1 件のコメント:

  1. 初めまして!
    今回ステータスバーを利用したアプリで、PendingIntentでのputExtraで詰まっていて、この記事にたどり着きました。
    おかげさまで、狙い通りの動作をさせることができました!

    返信削除