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?
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.
So there is no actual difference between the two.
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.
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;
}
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();
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With