Earlier in this book, we covered using services by sending commands to them to be processed. That “command pattern” is one of two primary means of interacting with a service — the binding pattern is the other. With the binding pattern, your service exposes a more traditional API, in the form of a “binder” object with methods of your choosing. On the plus side, you get a richer interface. However, it more tightly ties your activity to your service, which may cause you problems with configuration changes.
Either the command pattern or the binding pattern can be used, if
desired, across process boundaries, with the client being some
third-party application. In either case, you will need to export
your service via an <intent-filter>
. And, in the case of the binding
pattern, your “binder” implementation will have some restrictions.
This chapter covers the binding pattern for local services, plus inter-process commands and binding (a.k.a., remote services).
Understanding this chapter requires that you have read the chapters on:
Implementing the binding pattern requires work on both the service
side and the client side. The service will need to have a full
implementation of the onBind()
method, which typically just returns
null
or throws some sort of runtime exception
for a service solely implementing the command pattern. And,
the client (e.g., an activity) will need to ask to bind to the service,
instead of (or perhaps in addition to) starting the service.
The service implements a subclass of Binder
that represents the service’s
exposed API. For a local service, your Binder
can have pretty much whatever
methods you want: method names, parameters, return types, and exceptions
thrown are up to you. When you get into remote services, your Binder
implementation will be substantially more constrained, to support
inter-process communication.
Then, your onBind()
method returns an instance of the Binder
.
Clients call bindService()
, supplying the Intent
that identifies the service, a
ServiceConnection
object representing the client side of the binding, and an
optional BIND_AUTO_CREATE
flag. As with startService()
, bindService()
is
asynchronous. The client will not know anything about the status of the
binding until the ServiceConnection
object is called with
onServiceConnected()
. This not only indicates the binding has been
established, but for local services it provides the Binder object that the
service returned via onBind()
. At this point, the client can use the Binder
to
ask the service to do work on its behalf.
Note that if the service is not
already running, and if you provide BIND_AUTO_CREATE
, then the service will
be created first before being bound to the client. If you skip
BIND_AUTO_CREATE
, and the service is not already running,
bindService()
is supposed to return false
, indicating there was no
existing service to bind to. However, in actuality, Android returns true
,
due to an apparent bug.
Eventually, the client will need to call unbindService()
, to indicate it no
longer needs to communicate with the service. For example, an activity
might call bindService()
in its onCreate()
method, then call unbindService()
in its onDestroy()
method. Once you call unbindService()
, your Binder
object
is no longer safe to be used by the client. If there are no other bound clients
to the service, Android will shut down the service as well, releasing its
memory. Hence, we do not need to call stopService()
ourselves — Android
handles that, if needed, as a side effect of unbinding.
Your ServiceConnection
object will also need an onServiceDisconnected()
method. This will be called only if there is an unexpected disconnection,
such as the service crashing with an unhandled exception.
In the chapter introducing services, we saw a sample app that would
download a file off of a Web server. That sample
used the command pattern, telling the service what to download via an
Intent
extra. In this chapter, we will review a few variations of that
sample, all of which use the binding pattern instead of the command
pattern.
Right now, we are focused on local services, and so the
Binding/Local
sample project does the download via a local bound service.
We start by defining an interface that will serve as the “contract” between
the client (fragment) and service. This interface, IDownload
, contains a single
download()
method:
package com.commonsware.android.advservice.binding;
// Declare the interface.
interface IDownload {
void download(String url);
}
Our service, DownloadService
, implements just one method, onBind()
, which
returns an instance of a DownloadBinder
:
package com.commonsware.android.advservice.binding;
import android.app.Service;
import android.content.Intent;
import android.net.Uri;
import android.os.Binder;
import android.os.Environment;
import android.os.IBinder;
import android.util.Log;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
public class DownloadService extends Service {
@Override
public IBinder onBind(Intent intent) {
return(new DownloadBinder());
}
private static class DownloadBinder extends Binder implements IDownload {
@Override
public void download(String url) {
new DownloadThread(url).start();
}
}
private static class DownloadThread extends Thread {
String url=null;
DownloadThread(String url) {
this.url=url;
}
@Override
public void run() {
try {
File root=
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
root.mkdirs();
File output=new File(root, Uri.parse(url).getLastPathSegment());
if (output.exists()) {
output.delete();
}
HttpURLConnection c=(HttpURLConnection)new URL(url).openConnection();
FileOutputStream fos=new FileOutputStream(output.getPath());
BufferedOutputStream out=new BufferedOutputStream(fos);
try {
InputStream in=c.getInputStream();
byte[] buffer=new byte[8192];
int len=0;
while ((len=in.read(buffer)) >= 0) {
out.write(buffer, 0, len);
}
out.flush();
}
finally {
fos.getFD().sync();
out.close();
c.disconnect();
}
}
catch (IOException e2) {
Log.e("DownloadJob", "Exception in download", e2);
}
}
}
}
DownloadBinder
implements the IDownload
interface. Its download()
method, in turn, forks a DownloadThread
to perform the download
in the background — remember, for local services, the methods you
invoke on the Binder
are executed on whatever thread you call
them on.
Our fragment, DownloadFragment
, loads our layout, res/layout/main.xml
, containing
a Button
to trigger the download:
<?xml version="1.0" encoding="utf-8"?>
<Button xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/go"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/go"/>
The implementation of onCreateView()
simply loads that layout, gets the
Button
, sets up the fragment as being the click listener for the Button
,
and disables the Button
:
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
View result=inflater.inflate(R.layout.main, container, false);
btn=(Button)result.findViewById(R.id.go);
btn.setOnClickListener(this);
btn.setEnabled(binding!=null);
return(result);
}
The reason why we disable the Button
is because we are not connected to our
service at this point, and until we are, we cannot allow the user to try to
download a file.
In onCreate()
of our fragment, we mark the fragment as retained and bind to the service:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
appContext=(Application)getActivity().getApplicationContext();
appContext.bindService(new Intent(getActivity(),
DownloadService.class),
this, Context.BIND_AUTO_CREATE);
}
You will notice something curious here: getApplicationContext()
. Technically,
we could bind to the service directly from the Activity
, by calling bindService()
on it, as bindService()
is a method on Context
. However, our service binding
represents some state, and it is possible that this state will hold a reference
to the Context
that created the binding. In that case, we run the risk of
leaking our original activity during a configuration change. The
getApplicationContext()
method returns the global Application
singleton,
which is a Context
suitable for binding, but one that cannot be leaked, since
it is already in a global scope. In effect, it is “pre-leaked”.
The call to setRetainInstance()
allows the fragment –
serving as our ServiceConnection
— to survive a
configuration change, so we can cleanly unbind from the service later on, when onDestroy()
is called.
Some time after onCreate()
is called and we call bindService()
, our
onServiceConnected()
method will be called, as we designated our fragment to
be the ServiceConnection
. Here, we can cast the IBinder
object we receive
to be our IDownload
interface to the service, and we can enable the Button
:
@Override
public void onServiceConnected(ComponentName className, IBinder binder) {
binding=(IDownload)binder;
btn.setEnabled(true);
}
Since we are implementing the ServiceConnection
interface, our fragment
also needs to implement the onServiceDisconnected()
method, invoked if our
service crashes. Here, we delegate responsibility to a disconnect()
private
method, which removes our link to the IDownload
object and disables our Button
:
@Override
public void onServiceDisconnected(ComponentName className) {
disconnect();
}
private void disconnect() {
binding=null;
btn.setEnabled(false);
}
}
And, when our fragment is destroyed, we unbind from the service (using
the same Context
as before, from getApplicationContext()
) and disconnect()
:
@Override
public void onDestroy() {
appContext.unbindService(this);
disconnect();
super.onDestroy();
}
However, in between onServiceConnected()
and either onServiceDisconnected()
or onDestroy()
, the user can click the Button
, which will trigger the
download via a call to download()
on our IDownload
instance:
@Override
public void onClick(View view) {
binding.download(TO_DOWNLOAD);
}
The DownloadBindingDemo
activity adds our DownloadFragment
via a FragmentTransaction
:
package com.commonsware.android.advservice.binding;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
public class DownloadBindingDemo extends FragmentActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getSupportFragmentManager().findFragmentById(android.R.id.content) == null) {
getSupportFragmentManager().beginTransaction()
.add(android.R.id.content,
new DownloadFragment()).commit();
}
}
}
Some developers will use both startService()
and bindService()
at the same
time. The typical argument is that they need frequent updates from the service
(e.g., percentage of progress, for updating a ProgressBar
) in the client and
are concerned about the overhead of sending broadcasts.
With the advent of
LocalBroadcastManager
and other event bus implementations,
binding to a service
you are using with startService()
should no longer be necessary.
If you wish to extend the binding pattern to serve in the role of IPC,
whereby other processes can get at your Binder
and call its methods,
you will need to use AIDL: the Android Interface Description Language.
If you have used
IPC mechanisms like SOAP, XML-RPC, DCOM, CORBA, or the like, you will recognize the
notion of IDL. AIDL describes the public IPC interface, and Android
supplies tools to build the client and server side of that interface.
With that in mind, let’s take a look at AIDL and IPC.
IDLs are frequently written in a “language-neutral” syntax. AIDL, on the other hand, looks a lot like a Java interface file. For example, here is some AIDL:
package com.commonsware.android.advservice.remotebinding;
// Declare the interface.
interface IDownload {
void download(String url);
}
As you will notice, this looks suspiciously like the regular Java interface we used in the simple binding example earlier in this chapter.
As with a Java interface, you declare a package at the top. As with a
Java interface, the methods are wrapped in an interface declaration
(interface IDownload { ... }
). And, as with a Java interface, you
list the methods you are making available.
The differences, though, are critical.
First, not every Java type can be used as a parameter. Your choices are:
int
, float
, double
, boolean
, etc.)String
and CharSequence
List
and Map
(from java.util
)Parcelable
or Serializable
interfaceIn the case of the latter two categories, you need to include
import
statements referencing the names of the classes or
interfaces that you are using (e.g., import com.commonsware.android.ISomething
).
This is true even if these
classes are in your own package — you have to import them
anyway.
Next, parameters can be classified as in
, out
, or inout
. Values
that are out
or inout
can be changed by the service and those
changes will be propagated back to the client. Primitives (e.g.,
int
) can only be in
.
Also, you cannot throw any exceptions. You will need to catch all exceptions in your code, deal with them, and return failure indications some other way (e.g., error code return values).
Name your AIDL files with the .aidl
extension and place them in the
proper directory based on the package name:
aidl/
directory
in your src/
source set, as a peer of your java/
directory,
with the same sort of subdirectories-based-on-the-Java-package
approach as you use for regular Java source code.aidl
files will go alongside your
.java
files in the src/
directory treeWhen you build your project, either via an IDE or via command-line build tools, the aidl
utility from the Android SDK will translate your AIDL into a server
stub and a client proxy.
Given the AIDL-created server stub, now you need to implement the service, either directly in the stub, or by routing the stub implementation to other methods you have already written.
The mechanics of this are fairly straightforward:
.Stub
class
(e.g., IDownload.Stub
)onBind()
method in the
Service
subclassNote that AIDL IPC calls are synchronous, and so the caller is blocked until the IPC method returns. Hence, your services need to be quick about their work.
We will see examples of service stubs later in this chapter.
So, given our AIDL description, let us examine a sample implementation, using AIDL for a remote service.
Our sample applications — shown in the
Binding/Remote/Service
and
Binding/Remote/Client
sample projects — simply move the service logic into a separate project
from the client logic.
To bind to a service’s AIDL-defined API, you need to craft an Intent
that can identify the service in question. In the case of a local
service, that Intent can use the local approach of directly
referencing the service class.
Obviously, that is not possible in a remote service case, where the service class is not in the same process, and may not even be known by name to the client.
When you define a service to be used by remote, you need to add an
<intent-filter>
element to your service declaration in the manifest,
indicating how you want that service to be referred to by clients.
The manifest for RemoteService
is shown below:
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.commonsware.android.advservice.remotebinding.svc"
xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk
android:minSdkVersion="14"
android:targetSdkVersion="14" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@android:style/Theme.Holo.Light.DarkActionBar">
<service android:name=".DownloadService">
<intent-filter>
<action android:name="com.commonsware.android.advservice.remotebinding.IDownload" />
</intent-filter>
</service>
</application>
</manifest>
Here, we say that the service can be identified by the name
com.commonsware.android.advservice.remotebinding.IDownload
. So long as the client
uses this name to identify the service, it can bind to that service’s
API.
In this case, the name is not an implementation, but the AIDL API, as you will see below. In effect, this means that so long as some service exists on the device that implements this API, the client will be able to bind to something.
We are used to a device having multiple activities that can respond to the same
<intent-filter>
. In that case, by default, the user will see a chooser if we
try to start one of those activities.
We are used to a device having multiple BroadcastReceiver
components that can
respond to the same <intent-filter>
(or IntentFilter
). In that case, in a
regular broadcast, all eligible receivers will receive it.
We are used to it being impossible to have multiple ContentProvider
components
with the same authority, as the second one fails on install with an
INSTALL_FAILED_CONFLICTING_PROVIDER
error.
What happens if there are two (or more) services installed on
the device that claim to support the same <intent-filter>
, but have different
package names? You might think that this would fail on install, as happens
with providers with duplicate authorities. Alas, it does not… prior to
Android 5.0. Instead, the
higher-priority <intent-filter>
gets it (set via the android:priority
attribute). If 2+ implementations have the same priority, the first one
installed wins.
So, if we have BadService
and GoodService
, both responding to the same
<intent-filter>
, and a client app tries to communicate to GoodService
via the implicit Intent
matching that <intent-filter>
, it
might actually be communicating with BadService
, simply because BadService
was installed first. The user is oblivious to this.
Android 5.0 solves this by preventing binding using an implicit Intent
.
This, however, presents a conundrum:
Intent
Intent
identifying the
desired service, as that might be from a third-party appAs you will see, when we examine the client side of this sample, we have
to use PackageManager
to convert an implicit Intent
into a valid
explicit Intent
for our service. This not only allows us to comply with
the Android 5.0 binding restriction, but it gives us an opportunity to
detect and handle the cases where there is no matching service (e.g., the
service app has not yet been installed) or when there is more than one
matching service (e.g., BadService
and GoodService
). And the techniques
that all of this uses works on pretty much any version of Android, so
while we need them for Android 5.0 and higher, we can use them
anywhere.
Beyond the manifest, the service implementation is not too unusual.
There is the AIDL interface, IDownload
:
package com.commonsware.android.advservice.remotebinding;
// Declare the interface.
interface IDownload {
void download(String url);
}
And there is the actual service class itself, DownloadService
:
package com.commonsware.android.advservice.remotebinding.svc;
import android.app.Service;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;
import android.os.IBinder;
import android.util.Log;
import com.commonsware.android.advservice.remotebinding.IDownload;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
public class DownloadService extends Service {
@Override
public IBinder onBind(Intent intent) {
return(new DownloadBinder());
}
private static class DownloadBinder extends IDownload.Stub {
@Override
public void download(String url) {
new DownloadThread(url).start();
}
}
private static class DownloadThread extends Thread {
String url=null;
DownloadThread(String url) {
this.url=url;
}
@Override
public void run() {
try {
File root=
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
root.mkdirs();
File output=new File(root, Uri.parse(url).getLastPathSegment());
if (output.exists()) {
output.delete();
}
HttpURLConnection c=(HttpURLConnection)new URL(url).openConnection();
FileOutputStream fos=new FileOutputStream(output.getPath());
BufferedOutputStream out=new BufferedOutputStream(fos);
try {
InputStream in=c.getInputStream();
byte[] buffer=new byte[8192];
int len=0;
while ((len=in.read(buffer)) >= 0) {
out.write(buffer, 0, len);
}
out.flush();
}
finally {
fos.getFD().sync();
out.close();
c.disconnect();
}
}
catch (IOException e2) {
Log.e("DownloadJob", "Exception in download", e2);
}
}
}
}
This is identical to the local binding example, with one key difference:
DownloadBinder
now extends IDownload.Stub
rather than the generic
Binder
class.
The client — a revised version of DownloadFragment
— connects to the
remote service to ask it to download the file on the user’s behalf.
This has three changes of note over our original local implementation.
First, when we call download()
on the IDownload
object, we need
to catch a RemoteException
. This will be thrown if the service crashes
during our request or otherwise is unable to return properly:
@Override
public void onClick(View view) {
try {
binding.download(TO_DOWNLOAD);
}
catch (RemoteException e) {
Log.e(getClass().getSimpleName(), "Exception requesting download", e);
Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_LONG).show();
}
}
Second, our onServiceConnected()
uses IDownload.Stub.asInterface()
to convert
the raw IBinder
into an IDownload
object for use:
@Override
public void onServiceConnected(ComponentName className, IBinder binder) {
binding=IDownload.Stub.asInterface(binder);
btn.setEnabled(true);
}
Third, our binding logic in onCreate()
is significantly more complicated:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
appContext=(Application)getActivity().getApplicationContext();
Intent implicit=new Intent(IDownload.class.getName());
List<ResolveInfo> matches=getActivity().getPackageManager()
.queryIntentServices(implicit, 0);
if (matches.size() == 0) {
Toast.makeText(getActivity(), "Cannot find a matching service!",
Toast.LENGTH_LONG).show();
}
else if (matches.size() > 1) {
Toast.makeText(getActivity(), "Found multiple matching services!",
Toast.LENGTH_LONG).show();
}
else {
Intent explicit=new Intent(implicit);
ServiceInfo svcInfo=matches.get(0).serviceInfo;
ComponentName cn=new ComponentName(svcInfo.applicationInfo.packageName,
svcInfo.name);
explicit.setComponent(cn);
appContext.bindService(explicit, this, Context.BIND_AUTO_CREATE);
}
}
Here, we:
Application
singleton Context
as beforeIntent
for the service, using the appropriate action
string (which, in this case, happens to be the fully-qualified
name of the IDownload
interface)PackageManager
and queryIntentServices()
to find out all
services that implement a matching <intent-filter>
for that
implicit Intent
Toast
if there is not exactly one such serviceServiceInfo
object from our queryIntentServices()
call
to craft an explicit Intent
, with the same structure as the
implicit Intent
had, but also with the actual matched component
(via setComponent()
)Intent
to bind to the serviceNote that the client needs its own copy of IDownload.aidl
.
After all, it is a totally separate application, and therefore does
not share source code with the service.
If you compile both applications and upload them to the device, then start up the client, you can have the service download the file.
The previous sample confirms that there is exactly one service that
matches the desired Intent
. This catches the zero-service scenario
(requiring the user to install the other app) and catches the
multiple-service scenario (where one service is an attacker, presumably).
However, what happens if there is only one service installed, and it is not the desired service, but rather is an attacker? The preceding binding code will still go ahead and bind with that service.
You might consider just examining the package name/application ID of the other service, to see if it matches an expected value. However, that will not help you if the attacker is a modified version of the real service, one that kept its original package name but changed the service to do evil things.
Checking the digital signature of the other service is a more robust check, as that cannot readily be forged. Even if somebody modifies and repackages the app with the service, that app would wind up being signed by a different signing key, which you can detect.
Moreover, this approach can be used in both directions: the client can validate the service, and the service can validate the client. For example, perhaps as part of a licensing scheme, your service can only be used by apps developed by certain firms, based upon their signing keys.
The
Binding/SigCheck/Client
sample project illustrates a client that will perform this signature check
on the client side.
The corresponding service project –
Binding/SigCheck/Service
–
will perform a signature check on the service side.
Both projects use the CWAC-Security library, described elsewhere in this book, to do the signature checking. Hence, their Gradle build files have a dependency on that library:
repositories {
maven {
url "https://s3.amazonaws.com/repo.commonsware.com"
}
}
dependencies {
implementation 'com.android.support:support-fragment:27.1.1'
implementation 'com.commonsware.cwac:security:0.8.0'
}
The client’s DownloadFragment
is nearly the same as before, with an
adjustment to onCreate()
to check the signature if there is exactly
one service that matches the Intent
:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
appContext=(Application)getActivity().getApplicationContext();
Intent implicit=new Intent(IDownload.class.getName());
List<ResolveInfo> matches=getActivity().getPackageManager()
.queryIntentServices(implicit, 0);
if (matches.size() == 0) {
Toast.makeText(getActivity(), "Cannot find a matching service!",
Toast.LENGTH_LONG).show();
}
else if (matches.size() > 1) {
Toast.makeText(getActivity(), "Found multiple matching services!",
Toast.LENGTH_LONG).show();
}
else {
ServiceInfo svcInfo=matches.get(0).serviceInfo;
try {
String otherHash=SignatureUtils.getSignatureHash(getActivity(),
svcInfo.applicationInfo.packageName);
String expected=getActivity().getString(R.string.expected_sig_hash);
if (expected.equals(otherHash)) {
Intent explicit=new Intent(implicit);
ComponentName cn=new ComponentName(svcInfo.applicationInfo.packageName,
svcInfo.name);
explicit.setComponent(cn);
appContext.bindService(explicit, this, Context.BIND_AUTO_CREATE);
}
else {
Toast.makeText(getActivity(), "Unexpected signature found!",
Toast.LENGTH_LONG).show();
}
}
catch (Exception e) {
Log.e(getClass().getSimpleName(), "Exception trying to get signature hash", e);
}
}
}
In the one-match scenario, we get the signature of the other app,
by using getSignatureHash()
on SignatureUtils
, passing in the package name of the other app. We
then compare that with a hard-coded expected hash, pulled from a string resource,
one that is unfortunately too long to represent in this book.
Only if those two match do we go ahead with the binding.
This gets a bit more complicated, as we first need to figure out who
the client is, before we can validate the signature. In the case
of the client connecting to the service, we know the application ID
of the service courtesy of the queryIntentServices()
call. On the
service side, we need to use a different approach to identify who
the client is.
To do this work, DownloadBinder
now needs a Context
with which
to work, so onBind()
passes one to a revised DownloadBinder
constructor:
@Override
public IBinder onBind(Intent intent) {
return(new DownloadBinder(this));
}
The constructor holds on to three things:
Context
, in this case the Application
obtained from the Service
PackageManager
, as we will need this for the signature lookup private static class DownloadBinder extends IDownload.Stub {
private final PackageManager pm;
private final String expectedHash;
private final Context ctxt;
public DownloadBinder(Context ctxt) {
this.ctxt=ctxt.getApplicationContext();
this.pm=this.ctxt.getPackageManager();
this.expectedHash=this.ctxt.getString(
R.string.expected_sig_hash);
}
A Binder
can find out who is invoking one of its exposed methods via
Binder.getCallingUid()
. This returns the Linux user ID (uid) that the
client uses.
Normally, this will be tied to one application ID. However, it is possible
for a suite of apps to share a Linux user ID, via the android:sharedUserId
option in the manifest. Hence, the call to map the user ID to
an application ID is getPackagesForUid()
on PackageManager
, which returns
a list of application IDs.
So, the revised download()
method iterates over those application IDs
to see if any of them have the expected signature:
@Override
public void download(String url) {
boolean ok=false;
for (String pkg :
pm.getPackagesForUid(Binder.getCallingUid())) {
try {
String otherHash=
SignatureUtils.getSignatureHash(ctxt, pkg);
if (expectedHash.equals(otherHash)) {
ok=true;
break;
}
}
catch (Exception e) {
Log.e(getClass().getSimpleName(),
"Exception finding signature hash", e);
}
}
if (ok) {
new DownloadThread(url).start();
}
else {
Log.e(getClass().getSimpleName(),
"Could not validate client signature");
}
}
In practice, Android itself will ensure that if there are several application IDs sharing a Linux user ID, they will all be signed by the same signing key.
If and only if we find a signature match do we actually do the download; otherwise, we log an error.
This happens to be a very simple service with a single-method Binder
.
In a more complicated service, where there are several methods exposed
by the Binder
, the signature-check logic could be refactored into
a common private
method that the AIDL-defined Binder
methods could
all use to validate the client.
Today, there are two main ways you can get the expected hash:
keytool
getSignatureHash()
from your app and log the results,
running it against a known good copy of the other appHowever, we do not get any result back from the service to know if the download succeeded or failed. That is likely to be rather important information for the user.
In principle, download()
could return some success-or-failure
indication… but then we would have a blocking call. Neither
the client nor the service could proceed until the download is
completed. That would require the client to manage its own
background thread, which is a minor hassle. It also means that
the service ties up one of a limited number of “Binder
threads”,
which is not a good idea.
Another approach would be to pass some sort of callback object with
download()
, such that the server could run the script
asynchronously and invoke the callback on success or failure. This,
though, implies that there is some way to have the client export an
API to the service.
Fortunately, this is eminently doable, as you will see in this
section, and the accompanying samples (
Binding/Callback/Service
and
Binding/Callback/Client
).
AIDL does not have any concept of direction. It just knows interfaces, proxies, and stub implementations. In the preceding example, we used AIDL to have the service flesh out the stub implementation and have the client access the service via the AIDL-defined interface. However, there is nothing magic about services implementing interfaces and clients accessing them — it is equally possible to reverse matters and have the client implement something the service uses via an interface.
So, for example, we could create an IDownloadCallback.aidl
file:
package com.commonsware.android.advservice.callbackbinding;
// Declare the interface.
interface IDownloadCallback {
void onSuccess();
void onFailure(String msg);
}
Then, we can augment IDownload
itself, to pass an IDownloadCallback
with download()
:
package com.commonsware.android.advservice.callbackbinding;
import com.commonsware.android.advservice.callbackbinding.IDownloadCallback;
// Declare the interface.
interface IDownload {
void download(String url, IDownloadCallback cb);
}
Notice that we need to specifically import IDownloadCallback
, just like
we might import some “regular” Java interface. And, as before, we
need to make sure the client and the server are working off of the
same AIDL definitions, so these two AIDL files need to be replicated
across each project.
But other than that one little twist, this is all that is required, at the AIDL level, to have the client pass a callback object to the service: define the AIDL for the callback and add it as a parameter to some service API call.
Of course, there is a little more work to do on the client and server side to make use of this callback object.
On the client, we need to implement an IDownloadCallback
. In
onSuccess()
and onFailure()
we can do something like raise a Toast
.
The catch is that we cannot be certain we are being called on the UI
thread in our callback object. In fact, it is almost assured that we
are not. So, we need to get our work moved over to the main application
thread. To do that, this sample uses runOnUiThread()
:
IDownloadCallback.Stub cb=new IDownloadCallback.Stub() {
@Override
public void onSuccess() throws RemoteException {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(getActivity(), "Download successful!", Toast.LENGTH_LONG).show();
}
});
}
@Override
public void onFailure(final String msg) throws RemoteException {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(getActivity(), msg, Toast.LENGTH_LONG).show();
}
});
}
};
And, of course, we need to pass the IDownloadCallback
object in
our download()
call:
@Override
public void onClick(View view) {
try {
binding.download(TO_DOWNLOAD, cb);
}
catch (RemoteException e) {
Log.e(getClass().getSimpleName(), "Exception requesting download", e);
Toast.makeText(getActivity(), e.getMessage(), Toast.LENGTH_LONG).show();
}
}
The service also needs changing, to use the supplied callback object for the end results of the download.
DownloadBinder
now receives an IDownloadCallback
proxy in its
download()
method, which it passes along to the DownloadThread
:
private static class DownloadBinder extends IDownload.Stub {
@Override
public void download(String url, IDownloadCallback cb) {
new DownloadThread(url, cb).start();
}
}
Notice that the service’s own API just needs the IDownloadCallback
parameter, which can be passed around and used like any other Java
object. The fact that it happens to cause calls to be made
synchronously back to the remote client is invisible to the service.
DownloadThread
, in turn, invokes onSuccess()
or
onFailure()
as appropriate:
private static class DownloadThread extends Thread {
String url=null;
IDownloadCallback cb=null;
DownloadThread(String url, IDownloadCallback cb) {
this.url=url;
this.cb=cb;
}
@Override
public void run() {
boolean succeeded=false;
try {
File root=
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
root.mkdirs();
File output=new File(root, Uri.parse(url).getLastPathSegment());
if (output.exists()) {
output.delete();
}
HttpURLConnection c=(HttpURLConnection)new URL(url).openConnection();
FileOutputStream fos=new FileOutputStream(output.getPath());
BufferedOutputStream out=new BufferedOutputStream(fos);
try {
InputStream in=c.getInputStream();
byte[] buffer=new byte[8192];
int len=0;
while ((len=in.read(buffer)) >= 0) {
out.write(buffer, 0, len);
}
out.flush();
succeeded=true;
}
finally {
fos.getFD().sync();
out.close();
c.disconnect();
}
}
catch (IOException e2) {
Log.e("DownloadJob", "Exception in download", e2);
try {
cb.onFailure(e2.getMessage());
}
catch (RemoteException e) {
Log.e("DownloadJob", "Exception when calling onFailure()", e2);
}
}
if (succeeded) {
try {
cb.onSuccess();
}
catch (RemoteException e) {
Log.e("DownloadJob", "Exception when calling onSuccess()", e);
}
}
}
}
Remote services, by definition, are available for anyone to connect to. This may or may not be a good idea.
If the only client of your remote service is some other app of yours, you could protect the service using a custom signature-level permission.
If you anticipate third-party apps communicating with your service, you should strongly consider protecting the service with an ordinary custom permission, so the user can vote on whether the communication is allowed.
For local services, the simplest way to secure the service is to not
export it, typically by not having an <intent-filter>
element for
the <service>
in the manifest. Then, your app is the only app that
can work with the service.
One anti-pattern that is all too prevalent in Android is the
“everlasting service”. Such a service is started via startService()
and never stops — the component starting it does not stop it
and it does not stop itself via stopSelf()
.
Why is this an anti-pattern?
Occasionally, an everlasting service is the right solution. Take a VOIP client, for example. A VOIP client usually needs to hold an open socket with the VOIP server to know about incoming calls. The only way to continuously watch for incoming calls is to continuously hold open the socket. The only component capable of doing that would be a service, so the service would have to continuously run.
However, in the case of a VOIP client, or a music player, the user is
the one specifically requesting the service to run forever. By using
startForeground()
, a service can ensure it will not be stopped due
to old age for cases like this.
As a counter-example, imagine an email client. The client wishes to
check for new email messages periodically. The right solution for
this is the AlarmManager
pattern described elsewhere in this book.
The anti-pattern would have a service running
constantly, spending most of its time waiting for the polling period
to elapse (e.g., via Thread.sleep()
). There is no value to the user
in taking up RAM to watch the clock tick. Such services should be
rewritten to use AlarmManager
.
Most of the time, though, it appears that services are simply leaked.
That is one advantage of using AlarmManager
and an IntentService
– it is difficult to leak the service, causing it to run
indefinitely. In fact, IntentService
in general is a great
implementation to use whenever you use the command pattern, as it
ensures that the service will shut down eventually. If you use a
regular service, be sure to shut it down when it is no longer
actively delivering value to the user.