Remote Services and the Binding Pattern

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).

Prerequisites

Understanding this chapter requires that you have read the chapters on:

The Binding Pattern

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.

What the Service Does

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.

What the Client Does

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.

A Binding Sample

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);
}
(from Binding/Local/app/src/main/java/com/commonsware/android/advservice/binding/IDownload.java)

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);
      }
    }
  }
}
(from Binding/Local/app/src/main/java/com/commonsware/android/advservice/binding/DownloadService.java)

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"/>
(from Binding/Local/app/src/main/res/layout/main.xml)

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);
  }
(from Binding/Local/app/src/main/java/com/commonsware/android/advservice/binding/DownloadFragment.java)

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);
  }
(from Binding/Local/app/src/main/java/com/commonsware/android/advservice/binding/DownloadFragment.java)

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);
  }
(from Binding/Local/app/src/main/java/com/commonsware/android/advservice/binding/DownloadFragment.java)

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);
  }
}
(from Binding/Local/app/src/main/java/com/commonsware/android/advservice/binding/DownloadFragment.java)

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();
  }
(from Binding/Local/app/src/main/java/com/commonsware/android/advservice/binding/DownloadFragment.java)

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);
  }
(from Binding/Local/app/src/main/java/com/commonsware/android/advservice/binding/DownloadFragment.java)

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();
    }
  }
}
(from Binding/Local/app/src/main/java/com/commonsware/android/advservice/binding/DownloadBindingDemo.java)

Starting and Binding

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.

When IPC Attacks!

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.

Write the AIDL

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);
}
(from Binding/Remote/Service/app/src/main/aidl/com/commonsware/android/advservice/remotebinding/IDownload.aidl)

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:

  1. Primitive values (int, float, double, boolean, etc.)
  2. String and CharSequence
  3. List and Map (from java.util)
  4. Any other AIDL-defined interfaces
  5. Any Java classes that implement the Parcelable or Serializable interface

In 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:

When 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.

Implement the Interface

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:

  1. Create a subclass of the AIDL-generated .Stub class (e.g., IDownload.Stub)
  2. Implement methods matching up with each of the methods you placed in the AIDL
  3. Return an instance of this subclass from your onBind() method in the Service subclass

Note 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.

Service From Afar

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.

Service Names

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>
(from Binding/Remote/Service/app/src/main/AndroidManifest.xml)

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.

Remote Services and Implicit Intents

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:

As 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.

The Service

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);
}
(from Binding/Remote/Service/app/src/main/aidl/com/commonsware/android/advservice/remotebinding/IDownload.aidl)

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);
      }
    }
  }
}
(from Binding/Remote/Service/app/src/main/java/com/commonsware/android/advservice/remotebinding/svc/DownloadService.java)

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

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();
    }
  }
(from Binding/Remote/Client/app/src/main/java/com/commonsware/android/advservice/remotebinding/client/DownloadFragment.java)

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);
  }
(from Binding/Remote/Client/app/src/main/java/com/commonsware/android/advservice/remotebinding/client/DownloadFragment.java)

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);
    }
  }
(from Binding/Remote/Client/app/src/main/java/com/commonsware/android/advservice/remotebinding/client/DownloadFragment.java)

Here, we:

Note 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.

Tightening Up the Security

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.

Adding the Dependency

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'
}
(from Binding/SigCheck/Client/app/build.gradle)

Adding the Signature Check: Client

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);
      }
    }
  }
(from Binding/SigCheck/Client/app/src/main/java/com/commonsware/android/advservice/remotebinding/sigcheck/DownloadFragment.java)

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.

Adding the Signature Check: Service

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));
  }
(from Binding/SigCheck/Service/app/src/main/java/com/commonsware/android/advservice/remotebinding/sig/DownloadService.java)

The constructor holds on to three things:

  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);
    }
(from Binding/SigCheck/Service/app/src/main/java/com/commonsware/android/advservice/remotebinding/sig/DownloadService.java)

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");
      }
    }
(from Binding/SigCheck/Service/app/src/main/java/com/commonsware/android/advservice/remotebinding/sig/DownloadService.java)

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.

So, Where Do We Get the Expected Hash From?

Today, there are two main ways you can get the expected hash:

Servicing the Service

However, 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).

Callbacks via AIDL

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);
}
(from Binding/Callback/Service/app/src/main/aidl/com/commonsware/android/advservice/callbackbinding/IDownloadCallback.aidl)

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);
}
(from Binding/Callback/Service/app/src/main/aidl/com/commonsware/android/advservice/callbackbinding/IDownload.aidl)

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.

Revising the Client

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();
        }
      });
    }
  };
(from Binding/Callback/Client/app/src/main/java/com/commonsware/android/advservice/callbackbinding/client/DownloadFragment.java)

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();
    }
  }
(from Binding/Callback/Client/app/src/main/java/com/commonsware/android/advservice/callbackbinding/client/DownloadFragment.java)

Revising the Service

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();
    }
  }
(from Binding/Callback/Service/app/src/main/java/com/commonsware/android/advservice/callbackbinding/svc/DownloadService.java)

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);
        }
      }
    }
  }
(from Binding/Callback/Service/app/src/main/java/com/commonsware/android/advservice/callbackbinding/svc/DownloadService.java)

Thinking About Security

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.

The “Everlasting Service” Anti-Pattern

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?

  1. The service takes up memory all of the time. This is bad in its own right if the service is not continuously delivering sufficient value to be worth the memory.
  2. Users, fearing services that sap their device’s CPU or RAM, may attack the service with so-called “task killer” applications or may terminate the service via the Settings app, thereby defeating your original goal.
  3. Android itself, due to user frustration with sloppy developers, will terminate services it deems ill-used, particularly ones that have run for quite some time.

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.