Recently I wrote code like this for my Android app:
val requestCode = 0
val intentFoo = Intent(context, MyClass::class.java)
.putExtra(KEY, "foo")
val pendingIntentFoo = PendingIntent.getActivity(
this, requestCode, intentFoo, PendingIntent.FLAG_UPDATE_CURRENT)
val intentBar = Intent(context, MyClass::class.java)
.putExtra(KEY, "bar")
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()
I expected that MyActivity was launched with intentBar
when I tapped bar
action button in notification, but actually it was launched with intentFoo
.
According to Document, we have to differentiate request code when we use multiple pending intent.
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).
In order to understand why intentFoo
was launched, let's dive into the framework source code.
Firstly, PendingIntent#getActivity()
calls ActivityManager#getIntentSender()
internally.
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();
}
}
From it PendingIntentController#getIntentSender()
is called through ActivityManagerService
.
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);
Please note that in the last line we get IntentSenderRecord
with PendingIntentRecord.Key
. Let's check equals()
method of PendingIntentRecord.Key
.
@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;
}
You can see requestCode
is used here. If the package name, activity. the result of filterEquals()
, requestCode
and so on are all same, they're considered as equal.
Let's look at the detail of filterEquals()
. We don't check extra as long as we don't override this method.
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;
}
Let's go back to getIntentSender
method. After getting IntentSenderRecord
, we replace extras completely in case of PendingIntent.FLAG_UPDATE_CURRENT
flag.
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);
}
/**
* Completely replace the extras in the Intent with the extras in the
* given Intent.
*
* @param src The exact extras contained in this Intent are copied
* into the target intent, replacing any that were previously there.
*/
public @NonNull Intent replaceExtras(@NonNull Intent src) {
mExtras = src.mExtras != null ? new Bundle(src.mExtras) : null;
return this;
}
Now I understand why intentFoo
was launched👍
Top comments (0)