Notificationに設定するaction buttonで使うPending Intentについて、よくわかってなかった部分があったのでメモ。
現象
Notificationにactionを設定する際、Pending Intentを渡す必要があるが、複数のPending Intentで同じrequest codeを使いまわしていたらintentが上書きされるという現象がおきた。上書きされたintentは起動先activityは同じクラスだが、extraに異なるものを設定していた。
例えば、下記のようなpending intentをそれぞれactionに登録したとして、
val requestCode = 0
val intentFoo = getIntentFoo()
val pendingIntentFoo = PendingIntent.getActivity(this, requestCode, intentFoo, PendingIntent.FLAG_UPDATE_CURRENT)
val intentBar = getIntentBar()
val pendingIntentBar = PendingIntent.getActivity(this, requestCode, intentBar, PendingIntent.FLAG_UPDATE_CURRENT)
val notification = Notification.Builder(this, channelId)
.addAction(Notification.Action.Builder(icon, getString(R.string.foo), pendingIntentFoo).build())
.addAction(Notification.Action.Builder(icon, getString(R.string.bar), pendingIntentBar).build())
.build()
これでfooの方のbuttonを押下した場合でも、起動先のactivityで取得されるのはintentBar
だった。
公式のdocによると、
If you truly need multiple distinct PendingIntent objects active at the same time (such as to use as two notifications that are both shown at the same time), then you will need to ensure there is something that is different about them to associate them with different PendingIntents. This may be any of the Intent attributes considered by Intent.filterEquals, or different request code integers supplied to getActivity(Context, int, Intent, int), getActivities(Context, int, Intent[], int), getBroadcast(Context, int, Intent, int), or getService(Context, int, Intent, int).
とのことだったので、複数の異なるpending intentを同時に使いたい時はrequest codeを分ける必要があった。
Frameworkのコードを読む
上記のコードはPendingIntentを生成するときにPendingIntent#getActivity()
を利用している。この getActivity()
は内部でActivityManager#getIntentSender()
をコールしている。
public static PendingIntent getActivity(Context context, int requestCode,
@NonNull Intent intent, @Flags int flags, @Nullable Bundle options) {
String packageName = context.getPackageName();
String resolvedType = intent != null ? intent.resolveTypeIfNeeded(
context.getContentResolver()) : null;
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess(context);
IIntentSender target =
ActivityManager.getService().getIntentSender(
ActivityManager.INTENT_SENDER_ACTIVITY, packageName,
null, null, requestCode, new Intent[] { intent },
resolvedType != null ? new String[] { resolvedType } : null,
flags, options, context.getUserId());
return target != null ? new PendingIntent(target) : null;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
ここからActivityManagerService
経由でPendingIntentController#getIntentSender()
が呼ばれる。
public PendingIntentRecord getIntentSender(int type, String packageName,
@Nullable String featureId, int callingUid, int userId, IBinder token, String resultWho,
int requestCode, Intent[] intents, String[] resolvedTypes, int flags, Bundle bOptions) {
synchronized (mLock) {
PendingIntentRecord.Key key = new PendingIntentRecord.Key(type, packageName, featureId,
token, resultWho, requestCode, intents, resolvedTypes, flags,
SafeActivityOptions.fromBundle(bOptions), userId);
WeakReference<PendingIntentRecord> ref;
ref = mIntentSenderRecords.get(key);
ポイントはPendingIntentRecord.Key
を使ってIntentSenderRecord
を取得していること。Key#equals()
を見るとここでrequest codeが使われている。
@Override
public boolean equals(Object otherObj) {
if (otherObj == null) {
return false;
}
try {
Key other = (Key)otherObj;
if (type != other.type) {
return false;
}
if (userId != other.userId){
return false;
}
if (!Objects.equals(packageName, other.packageName)) {
return false;
}
if (!Objects.equals(featureId, other.featureId)) {
return false;
}
if (activity != other.activity) {
return false;
}
if (!Objects.equals(who, other.who)) {
return false;
}
if (requestCode != other.requestCode) {
return false;
}
if (requestIntent != other.requestIntent) {
if (requestIntent != null) {
if (!requestIntent.filterEquals(other.requestIntent)) {
return false;
}
} else if (other.requestIntent != null) {
return false;
}
}
if (!Objects.equals(requestResolvedType, other.requestResolvedType)) {
return false;
}
if (flags != other.flags) {
return false;
}
return true;
} catch (ClassCastException e) {
}
return false;
}
Package nameが同じ場合、起動先のintentのfilterEquals()
が一致してrequest codeも同じだった場合は同様のものとみなされる。(公式docでfilterEquals()
でもできるよって書いてあるのは、ここの条件のことっぽい。)
public boolean filterEquals(Intent other) {
if (other == null) {
return false;
}
if (!Objects.equals(this.mAction, other.mAction)) return false;
if (!Objects.equals(this.mData, other.mData)) return false;
if (!Objects.equals(this.mType, other.mType)) return false;
if (!Objects.equals(this.mIdentifier, other.mIdentifier)) return false;
if (!(this.hasPackageEquivalentComponent() && other.hasPackageEquivalentComponent())
&& !Objects.equals(this.mPackage, other.mPackage)) {
return false;
}
if (!Objects.equals(this.mComponent, other.mComponent)) return false;
if (!Objects.equals(this.mCategories, other.mCategories)) return false;
return true;
}
そしてfilterEquals()
はextraを見ていない。
さらに同じkeyだとみなされた場合はextraを上書きしている。
public PendingIntentRecord getIntentSender(int type, String packageName,
@Nullable String featureId, int callingUid, int userId, IBinder token, String resultWho,
int requestCode, Intent[] intents, String[] resolvedTypes, int flags, Bundle bOptions) {
synchronized (mLock) {
PendingIntentRecord.Key key = new PendingIntentRecord.Key(type, packageName, featureId,
token, resultWho, requestCode, intents, resolvedTypes, flags,
SafeActivityOptions.fromBundle(bOptions), userId);
WeakReference<PendingIntentRecord> ref;
ref = mIntentSenderRecords.get(key);
PendingIntentRecord rec = ref != null ? ref.get() : null;
if (rec != null) {
if (!cancelCurrent) {
if (updateCurrent) {
if (rec.key.requestIntent != null) {
rec.key.requestIntent.replaceExtras(intents != null ?
intents[intents.length - 1] : null);
}
Intent#replaceExtras()
の中身はこんな感じ。コメントにある通り、本当にまるっと入れ替えている。
Completely replace the extras in the Intent with the extras in the
given Intent.
@param src
public @NonNull Intent replaceExtras(@NonNull Intent src) {
mExtras = src.mExtras != null ? new Bundle(src.mExtras) : null;
return this;
}
というわけで、extraだけ異なるintentでrequest codeを使いまわした場合は上書きされる。
おまけ:Notificationのactionがタップされた時のコード
addaAction()
されたボタンの実態はRemoteView
である。こいつがNotificationに表示されるときにclick handlerが設定される。
RemoveView ※懐かしのAsyncTask
@Override
protected void onPostExecute(ViewTree viewTree) {
try {
if (mActions != null) {
OnClickHandler handler = mHandler == null
? DEFAULT_ON_CLICK_HANDLER : mHandler;
for (Action a : mActions) {
a.apply(viewTree.mRoot, mParent, handler);
}
}
} catch (Exception e) {
mError = e;
}
}
実際にユーザがactionをclickした際にはRemoteView#handleViewClick()
が呼ばれる。ここからいろいろ経由してNotificationRemoteInputManager
が持っているmOnClickHandlerでpending intentが使われる。
private final RemoteViews.OnClickHandler mOnClickHandler = new RemoteViews.OnClickHandler() {
@Override
public boolean onClickHandler(
View view, PendingIntent pendingIntent, RemoteViews.RemoteResponse response) {
mShadeController.get().wakeUpIfDozing(SystemClock.uptimeMillis(), view,
"NOTIFICATION_CLICK");
if (handleRemoteInput(view, pendingIntent)) {
return true;
}
if (DEBUG) {
Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent);
}
logActionClick(view, pendingIntent);
try {
ActivityManager.getService().resumeAppSwitches();
} catch (RemoteException e) {
}
return mCallback.handleRemoteViewClick(view, pendingIntent, () -> {
Pair<Intent, ActivityOptions> options = response.getLaunchOptions(view);
options.second.setLaunchWindowingMode(
WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY);
return RemoteViews.startPendingIntent(view, pendingIntent, options);
});
}
またRemoteViewに帰ってくる
public static boolean startPendingIntent(View view, PendingIntent pendingIntent,
Pair<Intent, ActivityOptions> options) {
try {
TODO
Context context = view.getContext();
context.startIntentSender(
pendingIntent.getIntentSender(), options.first,
0, 0, 0, options.second.toBundle());
} catch (IntentSender.SendIntentException e) {
Log.e(LOG_TAG, "Cannot send pending intent: ", e);
return false;
} catch (Exception e) {
Log.e(LOG_TAG, "Cannot send pending intent due to unknown exception: ", e);
return false;
}
return true;
}
ここで、上で見てきたIntentSenderを取得してstartするコードになっていた。
言いたいことは以上です。