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.
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.
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.
Remember to create a new Android Project in Eclipse, with the new package name com.marakana.logservice.
Just as in Java, our AIDL code specifies what package it’s part of.
However, unlike Java, we have to explicitly import other AIDL definitions, even if they are in the same package.
We specify the name of our interface. Interface names conventionally start with I for interface.
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.
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.
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.
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.
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 { // @Override public IBinder onBind(Intent intent) { // final String version = intent.getExtras().getString("version"); return new ILogService.Stub() { // public void log_d(String tag, String message) throws RemoteException { // Log.d(tag, message + " version: " + version); } public void log(Message msg) throws RemoteException { // Log.d(msg.getTag(), msg.getText()); } }; } }
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.
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.
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.
log_d()
is the simple
method that takes two strings and logs them. Our implementation
simply invokes the system’s Log.d()
.
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.
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.
package com.marakana.logservice; import android.os.Parcel; import android.os.Parcelable; public class Message implements Parcelable { // private String tag; private String text; public Message(Parcel in) { // tag = in.readString(); text = in.readString(); } public void writeToParcel(Parcel out, int flags) { // out.writeString(tag); out.writeString(text); } public int describeContents() { // return 0; } public static final Parcelable.Creator<Message> CREATOR = new Parcelable.Creator<Message>() { // public Message createFromParcel(Parcel source) { return new Message(source); } public Message[] newArray(int size) { return new Message[size]; } }; // Setters and Getters 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; } }
As we said before, Message
implements the Parcelable
interface.
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.
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.
We’re not using this method, because we have no special objects within our parcel.
A parcelable object must provide a Creator
. This Creator
is responsible for creating the
object from a parcel. It simply calls our other methods.
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.
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"> <!-- --> <service android:name=".LogService"> <!-- --> <intent-filter> <action android:name="com.marakana.logservice.ILogService" /> </intent-filter> </service> </application> <uses-sdk android:minSdkVersion="4" /> </manifest>
This is where we define our service. It is a <service>
element within the
application block.
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.
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:
After you have created your LogClient
project, right-click on your
project in Package Explorer and choose Properties.
In the “Properties for LogClient” dialog box, choose Java Build Path, and then click on the Projects tab.
In this tab, click on “Add…”, and point to your LogService
project.
This procedure will add
LogService
as a dependent project for
LogClient
.
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.
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.
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(); // Intent intent = new Intent("com.marakana.logservice.ILogService"); // intent.putExtra("version", "1.0"); // bindService(intent, conn, Context.BIND_AUTO_CREATE); // // Attach listener to button ((Button) findViewById(R.id.buttonClick)).setOnClickListener(this); } class LogConnection implements ServiceConnection { // public void onServiceConnected(ComponentName name, IBinder service) { // logService = ILogService.Stub.asInterface(service); // Log.i(TAG, "connected"); } public void onServiceDisconnected(ComponentName name) { // logService = null; Log.i(TAG, "disconnected"); } } public void onClick(View button) { try { logService.log_d("LogClient", "Hello from onClick()"); // Message msg = new Message(Parcel.obtain()); // msg.setTag("LogClient"); msg.setText("Hello from inClick() version 1.1"); logService.log(msg); // } catch (RemoteException e) { // Log.e(TAG, "onClick failed", e); } } @Override protected void onDestroy() { super.onDestroy(); Log.d(TAG, "onDestroyed"); unbindService(conn); // logService = null; } }
LogConnection
is our
class that both connects to and handles disconnections from the
remote service. The class is explained later.
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.
Here is where we add the data to the intent, to be extracted by the remote method.
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.
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 onServiceDisconnect
ed()
.
onServiceConnected()
is
called once the bind succeeds. At this point, the IBinder
instance represents our remote
service.
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.
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 log
Service
to
null to help with the garbage collection.
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
.
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.
Once we have the parcel, we simply call logService.log()
and pass it to LogService
, where it gets logged.
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
.
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.
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 Log
Service.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.
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.
3.145.96.86