DEV Community

Atsuko Fukui
Atsuko Fukui

Posted on

How PendingIntent gets Activity with request key

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()
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
                    }
Enter fullscreen mode Exit fullscreen mode
    /**
     * 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;
    }
Enter fullscreen mode Exit fullscreen mode

Now I understand why intentFoo was launched👍

Top comments (0)