How PendingIntent gets Activity with request key
Tags: android
Published: 2021-06-07 13:14:47

Recently I wrote code like this for my Android app:

```kotlin
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](https://developer.android.com/reference/android/app/PendingIntent), 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.

```java
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`.

```java
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`.

```java
@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.
```java
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.

```java
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);
                    }
```

```java
    /**
     * 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👍