I ended up with something like this:
1) to give the auxiliary stuff some scope, I created an internal class. At least, the ugly internals are separated from the rest of the code. I needed a remote service doing something, therefore the word Something
in class name
private RemoteSomethingHelper mRemoteSomethingHelper = new RemoteSomethingHelper();
class RemoteSomethingHelper {
//...
}
2) there are two things necessary to invoke a remote service method: the IBinder and the code to execute. Since we don't know which one becomes known first, we store them:
private ISomethingService mISomethingService;
private Runnable mActionRunnable;
Each time we write to one of these fileds, we invoke _startActionIfPossible()
:
private void _startActionIfPossible() {
if (mActionRunnable != null && mISomethingService != null) {
mActionRunnable.run();
mActionRunnable = null;
}
}
private void performAction(Runnable r) {
mActionRunnable = r;
_startActionIfPossible();
}
This, of course, assumes that the Runnable has access to mISomethingService, but this is true for runnables created within the methods of the RemoteSomethingHelper
class.
It is really good that the ServiceConnection
callbacks are called on the UI thread: if we are going to invoke the service methods from the main thread, we do not need to care about synchronization.
ISomethingService
is, of course, defined via AIDL.
3) Instead of just passing arguments to methods, we create a Runnable that will invoke the method with these arguments later, when invocation is possible:
private boolean mServiceBound;
void startSomething(final String arg1) {
// ... starting the service ...
final String arg2 = ...;
performAction(new Runnable() {
@Override
public void run() {
try {
// arg1 and arg2 must be final!
mISomethingService.startSomething(arg1, arg2);
} catch (RemoteException e) {
e.printStackTrace();
}
}
});
}
4) finally, we get:
private RemoteSomethingHelper mRemoteSomethingHelper = new RemoteSomethingHelper();
class RemoteSomethingHelper {
private ISomethingService mISomethingService;
private Runnable mActionRunnable;
private boolean mServiceBound;
private void _startActionIfPossible() {
if (mActionRunnable != null && mISomethingService != null) {
mActionRunnable.run();
mActionRunnable = null;
}
}
private ServiceConnection mServiceConnection = new ServiceConnection() {
// the methods on this class are called from the main thread of your process.
@Override
public void onServiceDisconnected(ComponentName name) {
mISomethingService = null;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mISomethingService = ISomethingService.Stub.asInterface(service);
_startActionIfPossible();
}
}
private void performAction(Runnable r) {
mActionRunnable = r;
_startActionIfPossible();
}
public void startSomething(final String arg1) {
Intent intent = new Intent(context.getApplicationContext(),SomethingService.class);
if (!mServiceBound) {
mServiceBound = context.getApplicationContext().bindService(intent, mServiceConnection, 0);
}
ComponentName cn = context.getApplicationContext().startService(intent);
final String arg2 = ...;
performAction(new Runnable() {
@Override
public void run() {
try {
mISomethingService.startSomething(arg1, arg2);
} catch (RemoteException e) {
e.printStackTrace();
}
}
});
}
}
context
is a field in my class; in an Activity, you can define it as Context context=this;
I did not need queuing actions; if you do, you can implement it.
You likely will need a result callback in startSomething(); I did, but this is not shown in this code.