Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android ResultReceiver across packages

I have an activity in package A (SignerClient), and a service in package B (MyService)

The activity's resultreceiver:

private ResultReceiver resultreceiver = new ResultReceiver(null) {
            @Override
            protected void onReceiveResult(int resultCode, Bundle resultData) {
            ...
            }
        };

Starting the service:

Intent intent = new Intent("com.example.STARTSERVICE");
intent.putExtra("resultreceiver", resultreceiver);            
startService(intent);

Receiving end:

 ResultReceiver rr = (ResultReceiver) intent.getParcelableExtra("resultreceiver");

Doing this when client and server are in the same package works fine. But in this case i get:

FATAL EXCEPTION: IntentService[MyService]
android.os.BadParcelableException: ClassNotFoundException when unmarshalling: com.example.cryptoclient.SignerClient$1
at android.os.Parcel.readParcelable(Parcel.java:1883)
at android.os.Parcel.readValue(Parcel.java:1771)
at android.os.Parcel.readMapInternal(Parcel.java:2008)
at android.os.Bundle.unparcel(Bundle.java:208)
at android.os.Bundle.getParcelable(Bundle.java:1100)
at android.content.Intent.getParcelableExtra(Intent.java:3396)
at org.axades.service.MyService.onHandleIntent(MyService.java:28)
at android.app.IntentService$ServiceHandler.handleMessage(IntentService.java:59)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:123)
at android.os.HandlerThread.run(HandlerThread.java:60)

What am I missing? Is my idea even possible?

like image 579
endre Avatar asked Apr 21 '11 11:04

endre


2 Answers

I wanted to use a ResultReceiver across packages and having to load the other package's context just didn't seem right to me... after all, the receiving package doesn't need to know the particular subclass of ResultReceiver used, it just needs to be able to call send() and let the IPC binder magic happen. Needing to have the particular subclass available seems like a design flaw.

There is a workaround:

public static ResultReceiver receiverForSending(ResultReceiver actualReceiver) {
    Parcel parcel = Parcel.obtain();
    actualReceiver.writeToParcel(parcel,0);
    parcel.setDataPosition(0);
    ResultReceiver receiverForSending = ResultReceiver.CREATOR.createFromParcel(parcel);
    parcel.recycle();
    return receiverForSending;
}

This code converts your instance of some subclass of ResultReceiver into a instance of ResultReceiver itself, which nonetheless still sends results to your original actualReceiver. The receiverForSending can be sent in an Intent extra to another package and can be unmarshalled just fine.

like image 192
Robert Tupelo-Schneck Avatar answered Nov 11 '22 02:11

Robert Tupelo-Schneck


Yes, your idea is possible. The ClassNotFoundException exception is thrown because you are trying to unparcel a class that was created in a different process by a different ClassLoader.

ResultReceiver class implements Parcelable interface which is suitable for inter-process calls (IPC), however to read your object in the service you need to use the same ClassLoader, that was used for object creation in the client application (i.e. in the activity). To get that ClassLoader on the service side, call createPackageContext method passing client package name and CONTEXT_INCLUDE_CODE|CONTEXT_IGNORE_SECURITY combination of flags. This will return client Context object from which the correct ClassLoader object can be obtained.

Example:

public int onStartCommand(Intent intent, int flags, int startId) {
  try {

// assuming SignerClient activity is located in the package "com.example.client.A"
    Context context = createPackageContext("com.example.client.A", Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
    ClassLoader cl = context.getClassLoader();

    Bundle bundle = intent.getExtras();
    bundle.setClassLoader(cl);
    ResultReceiver rr = bundle.getParcelable("resultreceiver");

//... your interaction with ResultReceiver ...
    rr.send(1, null);   // will result in a onReceiveResult call in the client activity

  } catch (NameNotFoundException e) {
    Log.e("MyService", "SignerClient package context was not found", e);
    throw new RuntimeException(e);
  }
  return START_STICKY;
}

I've just used it in my code - works like a charm.

UPDATE
However I suggest to consider using Messenger instead of ResultReceiver. It implements Parcelable interface and does not need to be extended thus the ClassLoader issue isn't possible. And it's also recommended in the official documentation.

UPDATE 2
In case you still prefer to use ResultReceiver take a look at Robert's answer in this thread. It looks cleaner and simpler than hacky context manipulations.

like image 15
Idolon Avatar answered Nov 11 '22 00:11

Idolon