Chapter 14. The Android Interface Definition Language

Each application in Android runs in its own process. For security reasons, an application cannot directly access the data of another application. However, a couple of mechanisms allow communication between applications. One such mechanism that you’ve seen throughout this book is Intents. Intents are asynchronous, meaning that you can post a message for someone to receive at some future point in time and just continue with your application.

Every once in a while we need a more direct, synchronous access to another process. There are many ways to implement this across process boundaries, and collectively they are called Interprocess Communication, or IPC for short.

To allow cross-application communication, Android provides its own version of an IPC protocol. One of the biggest challenges in IPC is passing data around, such as when passing parameters to method calls on the remote systems. IPC protocols tend to get complicated because they have to convert data from its in-memory format to a format that’s convenient for sending to another process. This is called marshaling, and the unpacking at the receiver is called unmarshaling.

To help with this, Android provides the Android Interface Definition Language, or AIDL. This lightweight implementation of IPC uses a syntax that is very familiar to Java developers, and there is a tool that automatically creates the hidden code required to connect a client and a remote service.

To illustrate how to use AIDL to create an interprocess communication, we’ll create two applications: a remote service called LogService and a client called LogClient that will bind to that remote service.

Implementing the Remote Service

Our remote service, LogService, will simply allow remote clients to log a message to it.

We are going to start by creating the interface for the remote service. This interface represents the API, or set of capabilities that the service provides. We write this interface in the AIDL language and save it in the same directory as our Java code with an .aidl extension.

The AIDL syntax is very similar to a regular Java interface. You simply define the method signature. The datatypes supported by AIDL are somewhat different from regular Java interfaces. However, all Java primitive datatypes are supported, and so are the String, List, Map, and CharSequence classes.

If you have a custom complex data type, such as a class, you need to make it Parcelable so that the Android runtime can marshal and unmarshal it. In this example, we’ll create a Message as a custom type.

Writing the AIDL

We start by defining the interface for our service. As you can see in Example 14-1, the interface very much resembles a typical Java interface. For readers who might have worked with CORBA in the past, AIDL has its roots in CORBA’s IDL.

Note

Remember to create a new Android Project in Eclipse, with the new package name com.marakana.logservice.

Example 14-1. ILogService.aidl
package com.marakana.logservice; // 1

import com.marakana.logservice.Message; // 2

interface ILogService { //3
  void log_d(String tag, String message); // 4
  void log(in Message msg); // 5
}
1

Just as in Java, our AIDL code specifies what package it’s part of.

2

However, unlike Java, we have to explicitly import other AIDL definitions, even if they are in the same package.

3

We specify the name of our interface. Interface names conventionally start with I for interface.

4

This method is simple because it doesn’t return anything and takes only primitives as inputs. Note that the String class is not a Java primitive, but AIDL considers it to be one.

5

This method takes our custom Message parcel as its input. We’ll define Message next.

Next, we’ll look at the implementation of the Message AIDL, shown in Example 14-2.

Example 14-2. Message.aidl
package com.marakana.logservice; // 1

/* 2 */
parcelable Message;
1

Specifies the package it’s in.

2

Declares that Message is a parcelable object. We will define this object later in Java.

At this point, we are done with the AIDL. As you save your files, Eclipse automatically builds the code to which the client will connect, called the stub because it looks like a complete method to the client but actually just passes on the client request to your remote service. The new Java file is located in the gen folder under /gen/com/marakana/logservice/ILogService.java. Because this file is derived from your AIDL, you should never modify it. The aidl tool that comes with the Android SDK will regenerate it whenever you make changes to your AIDL files.

Now that we have the AIDL and the generated Java stub, we are ready to implement the service.

Implementing the Service

Just like any Android service, we implement LogService in a Java class that subclasses the system Service class. But unlike our earlier Service implementations, where we ignored onBind() but implemented onCreate(), onStartCommand(), and onDestroy(), here we’re going to do the opposite. A method in a remote service starts when the client makes its request, which is called binding to the service, and therefore the client request triggers the service’s onBind() method.

To implement our remote service, we’ll return an IBinder object from the onBind() method in our service class. IBinder represents the implementation of the remote service. To implement IBinder, we subclass the ILogService.Stub class from the autogenerated Java code, and provide the implementation for our AIDL-defined methods, in this case various log() methods. Example 14-3 shows the code.

Example 14-3. LogService.java
package com.marakana.logservice;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;

public class LogService extends Service { // 1

  @Override
  public IBinder onBind(Intent intent) { // 2
    final String version = intent.getExtras().getString("version");

    return new ILogService.Stub() { // 3

      public void log_d(String tag, String message) throws RemoteException { // 4
        Log.d(tag, message + " version: " + version);
      }

      public void log(Message msg) throws RemoteException { // 5
        Log.d(msg.getTag(), msg.getText());
      }
    };
  }

}
1

LogService is an Android class derived from Service. We’ve seen many services, but this time around, it’s a bound service, as opposed to UpdaterService, which was unbound.

2

Since this is a bound service, we must implement onBind() and have it return a correct instance of IBinder class. The client passes us an Intent, from which we extract some string data. During the client implementation, we’ll see how it sets this, and thus how we can pass small amounts of data into the remote service as part of the binding process.

3

This instance of IBinder is represented by ILogService.Stub, a helper method that is generated for us in the Java stub file created by the aidl tool when we saved our AIDL interface. This code is part of /gen/com/marakana/logservice/ILogService.java.

4

log_d() is the simple method that takes two strings and logs them. Our implementation simply invokes the system’s Log.d().

5

We also provide a log() method that gets our Message parcel as its input parameter. Out of this object we extract the tag and the message. Again, for this trivial implementation, we just invoke Android’s logging mechanism.

Now that we have implemented the service in Java, we have to provide the Java implementation of the Message parcel as well.

Implementing a Parcel

Since Message is a Java object that we’re passing across processes, we need a way to encode and decode this object—marshal and unmarshal it—so that it can be passed. In Android, the object that can do that is called a Parcel and implements the Parcelable interface.

To be a parcel, this object must know how to write itself to a stream and how to recreate itself. Example 14-4 shows the code.

Example 14-4. Message.java
package com.marakana.logservice;

import android.os.Parcel;
import android.os.Parcelable;

public class Message implements Parcelable { // 1
  private String tag;
  private String text;

  public Message(Parcel in) { // 2
    tag = in.readString();
    text = in.readString();
  }

  public void writeToParcel(Parcel out, int flags) { // 3
    out.writeString(tag);
    out.writeString(text);
  }

  public int describeContents() { // 4
    return 0;
  }

  public static final Parcelable.Creator<Message> CREATOR
      = new Parcelable.Creator<Message>() { // 5

    public Message createFromParcel(Parcel source) {
      return new Message(source);
    }

    public Message[] newArray(int size) {
      return new Message[size];
    }

  };

  // Setters and Getters 6
  public String getTag() {
    return tag;
  }

  public void setTag(String tag) {
    this.tag = tag;
  }

  public String getText() {
    return text;
  }

  public void setText(String text) {
    this.text = text;
  }

}
1

As we said before, Message implements the Parcelable interface.

2

To be parcelable, this object must provide a constructor that takes in a Parcel and recreates the object. Here we read the data from the parcel into our local variables. The order in which we read in data is important: it must correspond to the order in which the data was written out.

3

writeToParcel() is the counterpart to the constructor. This method is responsible for taking the current state of this object and writing it out into a parcel. Again, the order in which variables are written out must match the order in which they are read in by the constructor that gets this parcel as its input.

4

We’re not using this method, because we have no special objects within our parcel.

5

A parcelable object must provide a Creator. This Creator is responsible for creating the object from a parcel. It simply calls our other methods.

6

These are just various setter and getter methods for our private data.

At this point, we have implemented the required Java code. We now need to register our service with the manifest file.

Registering with the Manifest File

As always, whenever we provide one of the new main building blocks for an application, we must register it with the system. The most common way to do that is to define it in the manifest file.

Just as we registered UpdaterService earlier, we provide a <service> element specifying our service. The difference this time around is that this service is going to be invoked remotely, so we should specify what action this service responds to. To do that, we specify the action and the intent filter as part of this service registration:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.marakana.logservice" android:versionCode="1"
  android:versionName="1.0">
  <application android:icon="@drawable/icon" android:label="@string/app_name">

  <!-- 1 -->
    <service android:name=".LogService">
      <!-- 2 -->
      <intent-filter>
        <action android:name="com.marakana.logservice.ILogService" />
      </intent-filter>
    </service>

  </application>
  <uses-sdk android:minSdkVersion="4" />
</manifest>
1

This is where we define our service. It is a <service> element within the application block.

2

The difference between this service and our UpdaterService is that this service is going to be remote to the client. Therefore, calling it by an explicit class name wouldn’t work well, because the client might not have access to the same set of classes. So instead, we provide the intent filter and action to which this service is registered to respond.

At this point, our service is complete. We can now move on to the client implementation.

Implementing the Remote Client

Now that we have the remote service, we are going to create a client that connects to that service to test that it all works well. Note that in this example we purposely separated the client and the server into two separate projects with different Java packages altogether, in order to demonstrate how they are separate apps.

So we’re going to create a new Android project in Eclipse for this client, just as we’ve done before for various other applications. However, this time around we are also going to make this project depend on the LogService project. This is important because LogClient has to find the AIDL files we created as part of LogService in order to know what that remote interface looks like. To do this in Eclipse:

  1. After you have created your LogClient project, right-click on your project in Package Explorer and choose Properties.

  2. In the “Properties for LogClient” dialog box, choose Java Build Path, and then click on the Projects tab.

  3. In this tab, click on “Add…”, and point to your LogService project.

This procedure will add LogService as a dependent project for LogClient.

Note

You need to copy both AIDL files as well as Message.java from Server to Client, preserving the original package names. By doing so, both projects can use the same interfaces. Otherwise, one app will not be able to load another app’s code.

Binding to the Remote Service

Our client is going to be an activity so that we can see it working graphically. In this activity, we’re going to bind to the remote service, and from that point on, use it as if it were just like any other local class. Behind the scenes, the Android binder will marshal and unmarshal the calls to the service.

The binding process is asynchronous, meaning we request it and it happens at some later point in time. To handle that, we need a callback mechanism to handle remote service connections and disconnections.

Once we have the service connected, we can make calls to it as if it were any other local object. However, if we want to pass any complex data types, such as a custom Java object, we have to create a parcel for it first. In our case, we have Message as a custom type, and we have already made it parcelable. Example 14-5 shows the code.

Example 14-5. LogActivity.java
package com.marakana.logclient;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

import com.marakana.logservice.ILogService;
import com.marakana.logservice.Message;

public class LogActivity extends Activity implements OnClickListener {
  private static final String TAG = "LogActivity";
  ILogService logService;
  LogConnection conn;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    // Request bind to the service
    conn = new LogConnection(); // 1
    Intent intent = new Intent("com.marakana.logservice.ILogService"); // 2
    intent.putExtra("version", "1.0"); // 3
    bindService(intent, conn, Context.BIND_AUTO_CREATE); // 4

    // Attach listener to button
    ((Button) findViewById(R.id.buttonClick)).setOnClickListener(this);
  }

  class LogConnection implements ServiceConnection { // 5

    public void onServiceConnected(ComponentName name, IBinder service) { // 6
      logService = ILogService.Stub.asInterface(service); // 7
      Log.i(TAG, "connected");
    }

    public void onServiceDisconnected(ComponentName name) { // 8
      logService = null;
      Log.i(TAG, "disconnected");
    }

  }

  public void onClick(View button) {
    try {
      logService.log_d("LogClient", "Hello from onClick()"); // 9
      Message msg = new Message(Parcel.obtain()); // 10
      msg.setTag("LogClient");
      msg.setText("Hello from inClick() version 1.1");
      logService.log(msg); // 11
    } catch (RemoteException e) { // 12
      Log.e(TAG, "onClick failed", e);
    }

  }

  @Override
  protected void onDestroy() {
    super.onDestroy();
    Log.d(TAG, "onDestroyed");

    unbindService(conn); // 13

    logService = null;
  }
}
1

LogConnection is our class that both connects to and handles disconnections from the remote service. The class is explained later.

2

This is the action intent that we’re using to connect to the remote service. It must match the action that LogService specified in the manifest file as part of its intent filter.

3

Here is where we add the data to the intent, to be extracted by the remote method.

4

The bindService() method asks the Android runtime to bind this activity to the remote service specified by the intent action. In addition to the intent, we pass on the Service Connection class to handle the actual connection. The BIND_AUTO_CREATE flag indicates that if the service we’re trying to connect to doesn’t already exist, it should be created.

5

LogConnection is the class that will be called back upon successful connection to the remote service and whenever the service disconnects. This class needs to subclass ServiceConnection and implement onServiceConnected() and onServiceDisconnected().

6

onServiceConnected() is called once the bind succeeds. At this point, the IBinder instance represents our remote service.

7

We now need to cast the bound service into our LogService instance. To do that, we use a helper method named ILogService.Stub.asInterface(), provided by that Java stub that was created automatically by the aidl tool when we saved our AIDL files.

8

onServiceDisconnected() is called once the remote service is no longer available. It is an opportunity to handle any necessary cleanup. In this case, we just set logService to null to help with the garbage collection.

9

Assuming that we have successfully bound to the remote service, we can now make calls to it as if it were a local call. logService.log_d() simply passes two strings to the log_d() method that we saw defined in LogService.

10

As mentioned earlier, if we want to pass a Message to the remote method, we have to create a parcel for it first. This is possible because Message is a parcelable object. We then set its properties using appropriate setters.

11

Once we have the parcel, we simply call logService.log() and pass it to LogService, where it gets logged.

12

Whenever we make a remote call, it could fail for a variety of reasons outside of our control. Because of that, it is a good practice to handle a possible RemoteException.

13

When this activity is about to be destroyed, we ask to unbind the service and free those resources.

At this point our client is complete. There’s a simple UI with a single button that triggers an onClick() call. Once the user clicks the button, our client should invoke the remote call in the service.

Testing That It All Works

Try to run the client from within Eclipse. Since Eclipse knows that LogClient is dependent on LogService, it should install both packages onto your device. Once the client starts, it should bind to the service. Try clicking on the button and check that LogService is indeed logging. Your adb logcat call should give you something like this:

...
I/LogActivity(  613): connected
...
D/LogClient(  554): Hello from onClick() version: 1.0
D/LogClient(  554): Hello from inClick() version 1.1
...

The first line is from the LogConnection in the client, indicating that we’ve successfully bound to the service. The other two lines are from the remote service, one for LogService.log_d() and the other one for LogService.log(), where we passed in the Message parcel.

If you run adb shell ps to see the running processes on your device, you’ll notice two separate line items for the client and the server:

app_43    554   33    130684 12748 ffffffff afd0eb08 S com.marakana.logservice
app_42    613   33    132576 16552 ffffffff afd0eb08 S com.marakana.logclient

This indicates that indeed the client and server are two separate applications.

Summary

Android provides an interprocess communication mechanism based on its binder, a high-performance, shared-memory system. To create a remote service, we define it using the Android Interface Definition Language (AIDL), in a way similar to Java interfaces. We then implement the remote interface and connect to it via the IBinder object. This allows us to connect our client to a remote service in a different process altogether.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.96.86