Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get details about Intent of a PendingIntent

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?

like image 988
Peterdk Avatar asked Jan 09 '13 16:01

Peterdk


People also ask

What is the purpose of PendingIntent?

A PendingIntent itself is simply a reference to a token maintained by the system describing the original data used to retrieve it. This means that, even if its owning application's process is killed, the PendingIntent itself will remain usable from other processes that have been given it.

What is difference between Intent and PendingIntent?

So there is no actual difference between the two.

What is pending Intent in notification?

In other words, PendingIntent lets us pass a future Intent to another application and allow that application to execute that Intent as if it had the same permissions as our application, whether or not our application is still around when the Intent is eventually invoked.


2 Answers

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;
}
like image 79
praetorian droid Avatar answered Oct 19 '22 18:10

praetorian droid


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();
}
like image 21
iamreptar Avatar answered Oct 19 '22 19:10

iamreptar