14

I was wondering if it's possible to get some more info out of a PendingIntent that I haven't created myself. To be more precise: is it possible to somehow retrieve the original Intent of a PendingIntent? I don't need to execute it, but would like to print it's contents.

Looking through the code of PendingIntent it shows a hidden method:

/** @hide */
public IIntentSender getTarget() {
    return mTarget;
}

However this IIntentSender is also hidden and has to do with Binder and more IPC (I guess) related stuff. Not so easy. Any ideas?

Peterdk
  • 15,625
  • 20
  • 101
  • 140

3 Answers3

18

This method will work on Android 4.2.2 and above:

/**
 * Return the Intent for PendingIntent.
 * Return null in case of some (impossible) errors: see Android source.
 * @throws IllegalStateException in case of something goes wrong.
 * See {@link Throwable#getCause()} for more details.
 */
public Intent getIntent(PendingIntent pendingIntent) throws IllegalStateException {
    try {
        Method getIntent = PendingIntent.class.getDeclaredMethod("getIntent");
        return (Intent) getIntent.invoke(pendingIntent);
    } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
        throw new IllegalStateException(e);
    }
}

Below is incomplete implementation for Android 2.3 and above. It requires to write an additional piece of native (JNI) code. Then maybe it will work. See TODO comment for more details.

/**
 * Return the Intent for PendingIntent.
 * Return null in case of some (impossible) errors: see Android source.
 * @throws IllegalStateException in case of something goes wrong.
 * See {@link Throwable#getCause()} and {@link Throwable#getMessage()} for more details.
 */
public Intent getIntent(PendingIntent pendingIntent) throws IllegalStateException {
    try {
        Method getIntent = PendingIntent.class.getDeclaredMethod("getIntent");
        return (Intent) getIntent.invoke(pendingIntent);
    } catch (NoSuchMethodException e) {
        return getIntentDeep(pendingIntent);
    } catch (InvocationTargetException | IllegalAccessException e) {
        throw new IllegalStateException(e);
    }
}

private Intent getIntentDeep(PendingIntent pendingIntent) throws IllegalStateException {
    try {
        Class<?> activityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");
        Method getDefault = activityManagerNativeClass.getDeclaredMethod("getDefault");
        Object defaultManager = getDefault.invoke(null);
        if (defaultManager == null) {
            throw new IllegalStateException("ActivityManagerNative.getDefault() returned null");
        }
        Field mTargetField = PendingIntent.class.getDeclaredField("mTarget");
        mTargetField.setAccessible(true);
        Object mTarget = mTargetField.get(pendingIntent);
        if (mTarget == null) {
            throw new IllegalStateException("PendingIntent.mTarget field is null");
        }
        String defaultManagerClassName = defaultManager.getClass().getName();
        switch (defaultManagerClassName) {
        case "android.app.ActivityManagerProxy":
            try {
                return getIntentFromProxy(defaultManager, mTarget);
            } catch (RemoteException e) {
                // Note from PendingIntent.getIntent(): Should never happen.
                return null;
            }
        case "com.android.server.am.ActivityManagerService":
            return getIntentFromService(mTarget);
        default:
            throw new IllegalStateException("Unsupported IActivityManager inheritor: " + defaultManagerClassName);
        }
    } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException | NoSuchFieldException e) {
        throw new IllegalStateException(e);
    }
}

private Intent getIntentFromProxy(Object defaultManager, Object sender) throws RemoteException {
    Class<?> activityManagerProxyClass;
    IBinder mRemote;
    int GET_INTENT_FOR_INTENT_SENDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 160;
    String iActivityManagerDescriptor = "android.app.IActivityManager";
    try {
        activityManagerProxyClass = Class.forName("android.app.ActivityManagerProxy");
        Field mRemoteField = activityManagerProxyClass.getDeclaredField("mRemote");
        mRemoteField.setAccessible(true);
        mRemote = (IBinder) mRemoteField.get(defaultManager);
    } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
        throw new IllegalStateException(e);
    }

    // From ActivityManagerProxy.getIntentForIntentSender()
    Parcel data = Parcel.obtain();
    Parcel reply = Parcel.obtain();
    data.writeInterfaceToken(iActivityManagerDescriptor);
    data.writeStrongBinder(((IInterface) sender).asBinder());
    transact(mRemote, data, reply, 0);
    reply.readException();
    Intent res = reply.readInt() != 0
            ? Intent.CREATOR.createFromParcel(reply) : null;
    data.recycle();
    reply.recycle();
    return res;
}

private boolean transact(IBinder remote, Parcel data, Parcel reply, int i) {
    // TODO: Here must be some native call to convert ((BinderProxy) remote).mObject int
    // to IBinder* native pointer and do some more magic with it.
    // See android_util_Binder.cpp: android_os_BinderProxy_transact() in the Android sources.
}

private Intent getIntentFromService(Object sender) {
    String pendingIntentRecordClassName = "com.android.server.am.PendingIntentRecord";
    if (!(sender.getClass().getName().equals(pendingIntentRecordClassName))) {
        return null;
    }
    try {
        Class<?> pendingIntentRecordClass = Class.forName(pendingIntentRecordClassName);
        Field keyField = pendingIntentRecordClass.getDeclaredField("key");
        Object key = keyField.get(sender);
        Class<?> keyClass = Class.forName("com.android.server.am.PendingIntentRecord$Key");
        Field requestIntentField = keyClass.getDeclaredField("requestIntent");
        requestIntentField.setAccessible(true);
        Intent requestIntent = (Intent) requestIntentField.get(key);
        return requestIntent != null ? new Intent(requestIntent) : null;
    } catch (ClassCastException e) {
    } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) {
        throw new IllegalStateException(e);
    }
    return null;
}
praetorian droid
  • 2,989
  • 1
  • 17
  • 19
  • 1
    This method is marked with `@hide`, which means it cannot be called normally. Reflection may work, though, unless it requires a system-level permission or something. – matiash Jan 06 '15 at 17:54
  • 1
    Of course we need reflection here, because there is no normal way to do that. And I don't think that some additional permissions are necessary. Anyway, it's easy to check this. The only obvious problem is that this call is available since Android 4.2.2, so can't be used on prior versions. And possibly (but unlikely) it may disappear in the future versions. – praetorian droid Jan 06 '15 at 19:11
  • I've tested this method and it looks good. Works fine for `PendingIntent`s created by Google+ sign in. Would be nice to have similar solution for devices older than 4.2.2, because this code isn't safe to use in production. I think the public method should catch all the exceptions which means there's no way to obtain `Intent` and return `null` instead of crashing the app. – tomrozb Jan 07 '15 at 09:15
  • This code should work on Android 2.3 and above. The only exception that thrown in this code - IllegalStateException with details about what goes wrong. If you don't need that details, no problem, catch it and return null. – praetorian droid Jan 07 '15 at 13:19
  • Well, it does not working on earlier versions because `GET_INTENT_FOR_INTENT_SENDER_TRANSACTION` constant and its handling were added in Android 4.2.2. – praetorian droid Jan 08 '15 at 01:38
  • The solution for earlier Android versions requires native code (with all its disadvantages) that don't guarantee the success. So I gave up :) – praetorian droid Jan 08 '15 at 02:38
  • 1
    This solution will fail on 4.4 (and I assume above) if you try and get a Parceable bundle from the intent using reflection. – Kristy Welsh Mar 21 '16 at 15:26
2

If you want the Intent for testing purposes in Robolectric, then use ShadowPendingIntent:

public static Intent getIntent(PendingIntent pendingIntent) {
    return ((ShadowPendingIntent) ShadowExtractor.extract(pendingIntent))
        .getSavedIntent();
}
iamreptar
  • 1,461
  • 16
  • 29
-1

You can use IntentSender which can be obtained from PendingIntent.getIntentSender(). IntentSender's function getCreatorPackage(), will give the package which has created this PendingIntent.

tomrozb
  • 25,773
  • 31
  • 101
  • 122
Munish Katoch
  • 517
  • 3
  • 6
  • 2
    Yes I understand that, but I am in need of the original Intent. This will not give me that. – Peterdk Jan 10 '13 at 13:43
  • Or perhaps more to the point, how would you get the Extras from the Intent from an IntentSender object? – Michael Jan 26 '15 at 22:26