When I decided that a particular idea of mine could be best implemented as an Android Service, running in the background, I found that there is not much information available on the web, beyond some very basic examples, android.com's
API Demo sample, and the
AIDL tutorial page.
However, those are rather "sparse" guidelines, and several gaps are left for oneself to fill out by trial and error: so I decided to post this brief how-to, along with
the code itself.
As
noted in the Android Developer's guide:
A service doesn't have a visual user interface, but rather runs in the background for an indefinite period of time. For example, a service might play background music as the user attends to other matters, or it might fetch data over the network or calculate something and provide the result to activities that need it. Each service extends the Service base class.
There are actually
two ways of implementing a service: one is to just extend the
Service class, implement the onBind() method and then implement the
Binder interface in your service implementation class (this is the approach described here); the other is to use AIDL (Android Interface Description Language), followed by the API Demo sample (and further described in Part II of this blog - coming soon).
So, here we go: let's assume that we want to implement a simple service, which enables developers to add a simple UI element to their app to allow delighted users to make donations, but freeing them from having to implement all the machinery: our service will take a Developer Key and an Amount value ($ cents), and will credit the amount to the developer's account (we assume that the user's credentials can be retrieved from the system - how to do that is outside the scope of this blog); the app developer herself will only have to implement a simple UI (or a fancy complicated one: that's up to her) and connect to our service.
The service class definition is pretty trivial:
public class DonateService extends Service {
@Override
public IBinder onBind(Intent intent) {
return new DonateServiceImpl(getResources());
}
}
The service implementation itself is rather simple too:
public class DonateServiceImpl extends Binder { ... }
with all the 'action' being in its onTransact() method:
@Override
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) {
if (code != res.getInteger(R.id.SERVICE_CODE)) {
Log.e(getClass().getSimpleName(), "Transaction code should be " +
res.getInteger(R.id.SERVICE_CODE) + ";" + " received instead " + code);
return false;
}
Bundle values = data.readBundle();
String devKey = values.getString(res.getString(R.string.DEV_KEY));
int amountInCents = values.getInt(res.getString(R.string.AMT));
Log.i(getClass().getSimpleName(), getUser() + " wants to donate " +
amountInCents + " to " + devKey);
if (amountInCents <= 0) {
Log.e(getClass().getSimpleName(), "Amount should be a positive integer (" +
amountInCents + " is not).");
return false;
}
Log.d(getClass().getSimpleName(), "Sending request to server");
// This is where we would implement our HTTPS connection service, most likely to
// some RESTful service
return true;
}
And this is pretty much all there is to it, as far as it concerns to handling a service call (please do read the description on the Service class, as well as the notes about a
service's lifecycle: similar to an Activity, there is an onCreate(), onDestroy() etc.) and retrieving the data marshalled by the system, from the calling Activity.
If you now try to 'test' it by using the Android Instrumentation framework from a 'sibling' test project (you create one typically at the same time as you create a new Android project in Eclipse), your code should look something like this:
public void testServiceRuns() {
Resources myRes = getContext().getResources();
assertNotNull("The test case resources are null", myRes);
Intent i = new Intent(ACTION);
IBinder binder = bindService(i);
assertNotNull(binder);
Bundle values = new Bundle();
values.putString(myRes.getString(R.string.DEV_KEY), "12345ABCDE");
values.putInt(myRes.getString(R.string.AMT), 99);
Parcel data = Parcel.obtain();
data.writeBundle(values);
try {
int serviceCode = myRes.getInteger(R.id.SERVICE_CODE);
assertTrue(binder.transact(serviceCode, data, null, 0));
Log.i("test", "Service executed successfully");
} catch (Exception ex) {
Log.e("test", "Could not transact service: " + ex.getLocalizedMessage(), ex);
fail(ex.getLocalizedMessage());
};
}
Funnily enough, this will work even if you mistype the name of the service's implementation class in the Android Manifest (AndroidManifest.xml):
<application android:label="@string/service_label"
android:icon="@drawable/app_icon">
<service android:exported="true"
android:name=".DonateService"
android:process=":remote">
<intent-filter>
<action android:name="@string/ACTION_DONATE" />
</intent-filter>
</service>
</application>
(try changing .DonateService to .blah and this will still work) -- all this to say: the mechanics of Android unit testing are different from a remote invocation, and may succeed even though your service may be unavailable to other applications.
Notice in particular android:process=":remote" - this is what tells Android application manager to lookup your service's intent-filter too, when looking for possible targets, when another process invokes startService(Intent).
Finally, your test case should extends ServiceTestCase
- but make sure you change the constructor, from Eclipse's auto-generated one to something like this:
public DonateServiceBinderTest(String name) {
super(DonateService.class);
setName(name);
}
as explained
elsewhere.
The <intent-filter> is the trick that does all the magic here: when matched against the Intent's 'action' will cause your service to be invoked.
Which brings us to the next topic: how do we invoke the service from a separately developed, completely independent application?
I have created a very simple Activity (download
service_example_usage-0.2_beta from our site) that does exactly that.
In the following, I will largely ignore the niceties of building an Android UI, as these are widely explained elsewhere and are largely outside the scope of this post, and will focus instead on the specifics of calling a Service.
As a general rule, when invoking a Service, you must do it outside of your primary UI thread: for starters, you have no idea how long will it take to complete and, secondly, you want to give the users the ability to terminate it, should it take too long.
Hence:
Rule #1 - execute a service invocation from within a Runnable that executes outside your main UI thread, and set up the service call, so that it has a callback handler;
Which neatly leads to
Rule #2 - to handle the service outcome where this needs to influence changes in the UI (and it most likely will, even for the trivial task of notifying the user that the call succeeded/failed, whatever...) use a Handler, or your app will crash unceremoniously.
Let us dissect the code in
SimpleApplication.java (
activateService(final int amt)) one step at a time:
1. Visually notify the user that we're engaging in something that will take time to complete, and we aren't quite sure how long:
LinearLayout panel = (LinearLayout) findViewById(R.id.ProgressPanel);
panel.setVisibility(View.VISIBLE);
the progress bar has been defined in the layout/main.xml file as 'indeterminate' and as a spinning wheel:
<ProgressBar android:id="@+id/ProgressBar" android:layout_height="wrap_content"
android:layout_marginLeft="15px" android:layout_width="wrap_content"
android:indeterminate="true" android:indeterminateBehavior="repeat"
android:visibility="visible"/>
2. Create an intent, whose action will match the service's action's intent-filter (see note below
[*] regarding sharing common strings between the service and its intended users):
Intent i = new Intent(getResources().getString(R.string.ACTION_DONATE));
3. Using this Activity's Context, bind it to a service that can service this Intent, by providing an implementation of a ConnectionService interface:
boolean isConnected = bindService(i, new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Bundle values = new Bundle();
values.putString(getResources().getString(R.string.DEV_KEY),
getResources().getString(R.string.DEVELOPER_KEY_VALUE));
values.putInt(getResources().getString(R.string.AMT), amt);
Parcel data = Parcel.obtain();
data.writeBundle(values);
boolean res = false;
try {
res = service.transact(serviceCode, data, null, 0);
} catch (RemoteException ex) {
Log.e("onServiceConnected", "Remote exception when calling service", ex);
res = false;
}
Message msg = Message.obtain(h, serviceCode, amt, (res ? 1 : -1));
msg.sendToTarget();
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
}, Context.BIND_AUTO_CREATE);
4. Wrap the above into a Runnable, and then kick the Thread alive:
Thread serviceThread = new Thread(new Runnable() {
@Override
public void run() { //... }
});
serviceThread.start();
5. To enable this newly created thread to 'callback' your activity and carry out tasks inside the UI thread, you need to implement a Handler class, create a
Message to wrap your returned results and then configure your handler as the message's target:
// outside the Runnable:
final Handler h = new ServiceCompleteHandler(result, panel, ctx);
// this will be invoked inside the UI thread when called
// at the end of onServiceConnected, in the service activation thread
Message msg = Message.obtain(h, serviceCode, amt, (res ? 1 : -1));
msg.sendToTarget();
Your Handler needs to override the default
handleMessage() method (that does nothing otherwise) and will be run by the System inside your UI thread (and will thus have access to your Activity's widgets, without causing a
RuntimeException):
public static class ServiceCompleteHandler extends Handler {
@Override
public void handleMessage(Message msg) {
int amt = msg.arg1;
boolean outcome = msg.arg2 > 0;
serviceComplete(amt, outcome);
}
void serviceComplete(int amt, boolean outcome) {
panel.setVisibility(View.INVISIBLE);
Rect r = new Rect(0, 0, 48, 48);
Drawable icon;
if (outcome) {
icon = getResources().getDrawable(R.drawable.accepted_48);
} else {
icon = getResources().getDrawable(R.drawable.cancel_48);
}
icon.setBounds(r);
result.setCompoundDrawables(icon, null, null, null);
int dollars = amt / 100;
int cents = amt % 100;
result.setText(getResources().getString(outcome ?
R.string.thanks : R.string.sorry) + dollars +
"." + cents + "\n" + getResources().getString(R.string.promo));
result.setVisibility(View.VISIBLE);
}
}
And this is pretty much all there is to it: sure, it's a bit more convoluted than just invoking a method in class, but we're talking here inter-process communication (IPC) and remote invocation (RMI) and they ain't pretty in any of the other frameworks either (think J5EE or, God forbid, CORBA).
In a subsequent post, I will show how to use AIDL to make invoking the service a more painless experience for the clients, but at the cost of having a slightly more convoluted development process on the service implementation's side.
---
[*]A note about re-using common strings (see the file
res/values/donate_service_strings.xml): it is good practice
not to use hard-coded strings in your service calls and data maps, and even better to have them in a commonly shared file (instead of duplicated everywhere in each applications
strings.xml file).
Ideally, if you publish a service, you should make such a file freely available to your service users (eg, downloadable from your API docs site's pages); equally, if you use a service, either use the available strings file (hopefully made available from your provider -- if they don't, question your desire to use their service...) or partition those service-specific strings in its ownd dedicated file.
Eclipse users: while developing your service, you will certainly want to test it out with a simple app (very much like I describe here). The obvious way to heed the above advice would then be to add the 'strings' file as a "link" in Eclipse Package Explorer, so that any changes made during the service development, would be automatically picked up the 'testing' app.
This won't work (Android's plug-in does not pick linked .
xml files in the
res/... tree, to generate the
R.java file). A simple workaround would be to create a soft link to the one file in the
res/values/ directory:
$ ln -s ../DonateService/res/values/donate_service_strings.xml res/values/service_defs.xml
(you can give whatever name you wish to the link).
In the downloadable code, I've added a physical copy to the file in each package, to avoid people having troubles when re-using it: if you do use both to follow along and/or make changes, I recommend removing one copy and making a link as suggested here.