Search This Blog

18 February 2012

AIDL Step 7: Using Callbacks

What if we would like the service to callback into the client?

Rather than repeating all the previous tutorials, let's just alter the v2 API. Note: In the real world, you should never do that as you need to remain backward compatible. Since I know no one else is using the api, we'll cheat.

First, let's create API/src/main/java/org/eoti/android/test/apkextensions/api/v2/IServerCallback.aidl:
package org.eoti.android.test.apkextensions.api.v2;

oneway interface IServerCallback {
    void message(String text);
}


And update our IServer.aidl:
package org.eoti.android.test.apkextensions.api.v2;

import org.eoti.android.test.apkextensions.api.v2.Registration;
import org.eoti.android.test.apkextensions.api.v2.IServerCallback;

interface IServer
{
    void register(inout Registration registration, IServerCallback cb);
    void unregister(in String registrationName, IServerCallback cb);
    String getServerIdentifier();
}


Recompile our API
malachi@onyx:~/work/apkextensions/API$ mvn clean install


Next, we update our server.  Replace createV2Binder() with:
    final RemoteCallbackList<IServerCallback> callbacks = new RemoteCallbackList<IServerCallback>();

    protected void broadcast(String message)
    {
        final int N = callbacks.beginBroadcast();
        for(int i=0; i<N; i++)
        {
            try {
                callbacks.getBroadcastItem(i).message(message);
            } catch (RemoteException e) {
                // RemoteCallbackList will take care of removing dead objects
            }
        }
        callbacks.finishBroadcast();
    }
   
    private IBinder createV2Binder()
    {
        return new IServer.Stub(){
            @Override
            public void register(Registration registration, IServerCallback cb) throws RemoteException {
                if(registration == null) throw new RemoteException(); // API15 required for 'new RemoteException(string)'
                registrations.put(registration.getName(), registration);
                if(cb != null)
                {
                    callbacks.register(cb);
                    cb.message("Registration successful");
                }
                Log.d(TAG, "Registration received:  " + registration.getName() + " v" + registration.getVersion());
                broadcast(registration.getName() + " has entered the room");
            }

            @Override
            public void unregister(String registrationName, IServerCallback cb) throws RemoteException {
                if(registrationName == null)  throw new RemoteException(); // API15 required for 'new RemoteException(string)'
                registrations.remove(registrationName);
                if(cb != null)
                {
                    cb.message("Unregistration successful");
                    callbacks.unregister(cb);
                }
                Log.d(TAG, "Registration removed: " + registrationName);
                broadcast(registrationName + " has left the room");
            }

            @Override
            public String getServerIdentifier() throws RemoteException {
                return TheServer.class.getName() + "#" + TheServer.this.hashCode();
            }
        };
    }

And compile it
malachi@onyx:~/work/apkextensions/TheServer$ mvn clean install


Next, we'll update our v2 client...
In our Client2/src/main/java/org/eoti/android/test/apkextensions/client2/Client2Activity.java, add:
    private IServerCallback callback = new IServerCallback.Stub() {
        @Override
        public void message(String text) throws RemoteException {
            Log.d(TAG, "Message received: " + text);
        }
    };

change:
server.register(registration);
to:
server.register(registration, callback);

and change:
server.unregister(REG_NAME);
to:
server.unregister(REG_NAME, callback);

Rebuild it
malachi@onyx:~/work/apkextensions/Client2$ mvn clean install

Run Client1:
I/ActivityManager(   61): Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=org.eoti.android.test.apkextensions.client1/.Client1Activity } from pid 127
I/ActivityManager(   61): Start proc org.eoti.android.test.apkextensions.server for service org.eoti.android.test.apkextensions.server/.TheServer: pid=846 uid=10045 gids={1015}
E/Client1 (  546): Server bound
I/ActivityManager(   61): Displayed org.eoti.android.test.apkextensions.client1/.Client1Activity: +726ms
D/Client1 (  546): Server connected
D/TheServer(  846): Registration received from deprecated client:  org.eoti.android.test.apkextensions.client1.Client1Activity
D/Client1 (  546): Registered org.eoti.android.test.apkextensions.client1.Client1Activity
D/Client1 (  546): Running tests...
D/Client1 (  546): Tests done...
D/TheServer(  846): Registration removed from deprecated client: org.eoti.android.test.apkextensions.client1.Client1Activity
E/Client1 (  546): Server unbound

Run Client2:
W/KeyCharacterMap(  546): No keyboard for id 0
W/KeyCharacterMap(  546): Using default keymap: /system/usr/keychars/qwerty.kcm.bin
I/ActivityManager(   61): Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=org.eoti.android.test.apkextensions.client2/.Client2Activity } from pid 127
I/ActivityManager(   61): Start proc org.eoti.android.test.apkextensions.client2 for activity org.eoti.android.test.apkextensions.client2/.Client2Activity: pid=856 uid=10047 gids={1015}
E/Client2 (  856): Server bound
D/Client2 (  856): Server connected: org.eoti.android.test.apkextensions.server.TheServer#1079096160
D/Client2 (  856): Message received: Registration successful
D/TheServer(  846): Registration received:  org.eoti.android.test.apkextensions.client2.Client2Activity v1.0alpha
D/Client2 (  856): Message received: org.eoti.android.test.apkextensions.client2.Client2Activity has entered the room
D/Client2 (  856): Registered org.eoti.android.test.apkextensions.client2.Client2Activity
D/Client2 (  856): Running tests...
D/Client2 (  856): Tests done...
D/TheServer(  846): Registration removed: org.eoti.android.test.apkextensions.client2.Client2Activity
D/Client2 (  856): Message received: Unregistration successful
E/Client2 (  856): Server unbound
I/ActivityManager(   61): Displayed org.eoti.android.test.apkextensions.client2/.Client2Activity: +931ms


This all looks good -- but why didn't Client1 announce when Client2 entered the room? 
It's because Client1 is still using the old deprecated API.

AIDL Step 6: Creating an updated client

Create our new module:

malachi@onyx:~/work/apkextensions$ mvn archetype:generate -DarchetypeCatalog=http://repository-malachid.forge.cloudbees.com/public-snapshot/archetype-catalog.xml
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Building extension test 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] >>> maven-archetype-plugin:2.2:generate (default-cli) @ pom >>>
[INFO]
[INFO] <<< maven-archetype-plugin:2.2:generate (default-cli) @ pom <<<
[INFO]
[INFO] --- maven-archetype-plugin:2.2:generate (default-cli) @ pom ---
[INFO] Generating project in Interactive mode
[INFO] No archetype defined. Using maven-archetype-quickstart (org.apache.maven.archetypes:maven-archetype-quickstart:1.0)
Choose archetype:
1: http://repository-malachid.forge.cloudbees.com/public-snapshot/archetype-catalog.xml -> org.eoti.kryten:kryten-archetype (kryten-archetype)
2: http://repository-malachid.forge.cloudbees.com/public-snapshot/archetype-catalog.xml -> org.eoti.galatea:galatea-archetype (galatea-archetype)
3: http://repository-malachid.forge.cloudbees.com/public-snapshot/archetype-catalog.xml -> org.eoti.archtest:archtest-archetype (archtest-archetype)
Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): : 2
Define value for property 'groupId': : org.eoti.android.test.apkextensions.client2
Define value for property 'artifactId': : Client2
Define value for property 'version':  1.0-SNAPSHOT: :
Define value for property 'package':  org.eoti.android.test.apkextensions.client2: :
Confirm properties configuration:
groupId: org.eoti.android.test.apkextensions.client2
artifactId: Client2
version: 1.0-SNAPSHOT
package: org.eoti.android.test.apkextensions.client2
 Y: :
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating project from Archetype: galatea-archetype:1.1-SNAPSHOT
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: org.eoti.android.test.apkextensions.client2
[INFO] Parameter: artifactId, Value: Client2
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: package, Value: org.eoti.android.test.apkextensions.client2
[INFO] Parameter: packageInPathFormat, Value: org/eoti/android/test/apkextensions/client2
[INFO] Parameter: package, Value: org.eoti.android.test.apkextensions.client2
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: groupId, Value: org.eoti.android.test.apkextensions.client2
[INFO] Parameter: artifactId, Value: Client2
[WARNING] Don't override file /home/malachi/work/apkextensions/Client2/src/main/android/res/values/strings.xml
[WARNING] Don't override file /home/malachi/work/apkextensions/Client2/src/main/android/res/layout/main.xml
[INFO] project created from Archetype in dir: /home/malachi/work/apkextensions/Client2
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 19.317s
[INFO] Finished at: Sat Feb 18 08:09:07 PST 2012
[INFO] Final Memory: 11M/245M
[INFO] ------------------------------------------------------------------------


Take care of the keystore and dependency...

And replace our Client2Activity:
package org.eoti.android.test.apkextensions.client2;

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.RemoteException;
import android.util.Log;
import org.eoti.android.test.apkextensions.api.v2.IServer;
import org.eoti.android.test.apkextensions.api.v2.Registration;

public class Client2Activity extends Activity {
    private static String TAG = "Client2";

    private static final String REG_NAME = Client2Activity.class.getName();
    private static final String REG_VERSION = "1.0alpha";
    private enum State{Unbound,Bound,Connected,Disconnected}
    private State state = State.Unbound;
    private IServer server;

    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            server = IServer.Stub.asInterface(iBinder);
            state = State.Connected;
            try {
                Log.d(TAG, "Server connected: " + server.getServerIdentifier());
                Registration registration = new Registration();
                registration.setName(REG_NAME);
                registration.setVersion(REG_VERSION);
                server.register(registration);
                Log.d(TAG, "Registered " + registration.getName());
            } catch (RemoteException e) {
                Log.e(TAG, "Unable to register", e);
            }

            doTest();
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {
            state = State.Disconnected;
            server = null;
            Log.d(TAG, "Server disconnected");
        }
    };

    private void bindServer()
    {
        switch(state)
        {
            case Bound:
            case Connected:
            case Disconnected:
                return;
            default:
                bindService(new Intent(IServer.class.getName()), connection, Context.BIND_AUTO_CREATE);
                state = State.Bound;
                Log.e(TAG, "Server bound");
                break;
        }
    }

    private void unbindServer()
    {
        switch(state)
        {
            case Unbound:
                return;
            case Connected:
                try{
                    server.unregister(REG_NAME);
                }catch(RemoteException e){
                    Log.e(TAG, "Unable to unregister", e);
                }
                server = null;
                // fall through
            default:
                unbindService(connection);
                state = State.Unbound;
                Log.e(TAG, "Server unbound");
                break;
        }
    }

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

    @Override
    protected void onDestroy() {
        unbindServer();
        super.onDestroy();
    }

    protected void doTest()
    {
        Log.d(TAG, "Running tests...");
        // @TODO add tests here
        Log.d(TAG, "Tests done...");
        unbindServer();
    }
}

Build it... run it...
malachi@onyx:~/work/apkextensions$ cd Client2
malachi@onyx:~/work/apkextensions/Client2$ mvn clean install

I/ActivityManager(   61): Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=org.eoti.android.test.apkextensions.client2/.Client2Activity } from pid 127
I/ActivityManager(   61): Start proc org.eoti.android.test.apkextensions.client2 for activity org.eoti.android.test.apkextensions.client2/.Client2Activity: pid=697 uid=10047 gids={1015}
I/ActivityManager(   61): Start proc org.eoti.android.test.apkextensions.server for service org.eoti.android.test.apkextensions.server/.TheServer: pid=705 uid=10045 gids={1015}
E/Client2 (  697): Server bound
I/ActivityManager(   61): Displayed org.eoti.android.test.apkextensions.client2/.Client2Activity: +1s67ms
D/Client2 (  697): Server connected: org.eoti.android.test.apkextensions.server.TheServer#1079078320
D/TheServer(  705): Registration received:  org.eoti.android.test.apkextensions.client2.Client2Activity v1.0alpha
D/Client2 (  697): Registered org.eoti.android.test.apkextensions.client2.Client2Activity
D/Client2 (  697): Running tests...
D/Client2 (  697): Tests done...
D/TheServer(  705): Registration removed: org.eoti.android.test.apkextensions.client2.Client2Activity
E/Client2 (  697): Server unbound
I/ActivityManager(   61): No longer want com.android.settings (pid 155): hidden #16


So we get deprecated messages when Client1 connects and the additional functionality when Client2 connects.
Client2 also has access to functionality that Client1 is unaware of.

Next up, AIDL Step 7: Using Callbacks

AIDL Step 5: Upgrading the Service

Let's update our service to handle an additional intent...

First, in the AndroidManifest.xml it should now read:
        <service android:name=".TheServer">
            <intent-filter>
                <action android:name="org.eoti.android.test.apkextensions.api.v1.IServer"/>
                <action android:name="org.eoti.android.test.apkextensions.api.v2.IServer"/>
            </intent-filter>
        </service>

Then, let's update TheServer.java to handle both old and new clients:
package org.eoti.android.test.apkextensions.server;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import org.eoti.android.test.apkextensions.api.v2.IServer;
import org.eoti.android.test.apkextensions.api.v2.Registration;

import java.util.concurrent.ConcurrentHashMap;

public class TheServer extends Service
{
    private static final String TAG = TheServer.class.getSimpleName();
    private ConcurrentHashMap<String, IBinder> binders = new ConcurrentHashMap<String, IBinder>();
    private ConcurrentHashMap<String, Registration> registrations = new ConcurrentHashMap<String, Registration>();

    private IBinder createV2Binder()
    {
        return new IServer.Stub(){
            @Override
            public void register(Registration registration) throws RemoteException {
                if(registration == null) throw new RemoteException(); // API15 required for 'new RemoteException(string)'
                registrations.put(registration.getName(), registration);
                Log.d(TAG, "Registration received:  " + registration.getName() + " v" + registration.getVersion());
            }

            @Override
            public void unregister(String registrationName) throws RemoteException {
                if(registrationName == null)  throw new RemoteException(); // API15 required for 'new RemoteException(string)'
                registrations.remove(registrationName);
                Log.d(TAG, "Registration removed: " + registrationName);            }

            @Override
            public String getServerIdentifier() throws RemoteException {
                return TheServer.class.getName() + "#" + TheServer.this.hashCode();
            }
        };
    }

    private IBinder createV1Binder()
    {
        return new org.eoti.android.test.apkextensions.api.v1.IServer.Stub(){
            @Override
            public void register(org.eoti.android.test.apkextensions.api.v1.Registration registration) throws RemoteException {
                if(registration == null) throw new RemoteException(); // API15 required for 'new RemoteException(string)'
                registrations.put(registration.getName(), new Registration(registration));
                Log.d(TAG, "Registration received from deprecated client:  " + registration.getName());
            }

            @Override
            public void unregister(String registrationName) throws RemoteException {
                if(registrationName == null)  throw new RemoteException(); // API15 required for 'new RemoteException(string)'
                registrations.remove(registrationName);
                Log.d(TAG, "Registration removed from deprecated client: " + registrationName);
            }
        };
    }
  
    public IBinder onBind(Intent intent) {
        if(intent == null) return null;
        String action = intent.getAction();
        if(action == null) return null;
        IBinder binder = binders.get(action);
        if(binder != null)
            return binder;
      
        if(org.eoti.android.test.apkextensions.api.v1.IServer.class.getName().equals(action))
            binder = createV1Binder();

        if(IServer.class.getName().equals(action))
            binder = createV2Binder();

        if(binder != null)
            binders.put(action, binder);
      
        return binder;
    }

    @Override
    public void onDestroy() {
        registrations.clear();
        super.onDestroy();
    }
}


Make sure it all compiles...
malachi@onyx:~/work/apkextensions$ cd TheServer/
malachi@onyx:~/work/apkextensions/TheServer$ mvn clean install


And make sure the now deprecated client can still connect:
I/ActivityManager(   61): Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=org.eoti.android.test.apkextensions.client1/.Client1Activity } from pid 127
I/ActivityManager(   61): Start proc org.eoti.android.test.apkextensions.server for service org.eoti.android.test.apkextensions.server/.TheServer: pid=605 uid=10045 gids={1015}
E/Client1 (  546): Server bound
I/ActivityManager(   61): Displayed org.eoti.android.test.apkextensions.client1/.Client1Activity: +671ms
D/Client1 (  546): Server connected
D/TheServer(  605): Registration received from deprecated client:  org.eoti.android.test.apkextensions.client1.Client1Activity
D/Client1 (  546): Registered org.eoti.android.test.apkextensions.client1.Client1Activity
D/Client1 (  546): Running tests...
D/Client1 (  546): Tests done...
D/TheServer(  605): Registration removed from deprecated client: org.eoti.android.test.apkextensions.client1.Client1Activity
E/Client1 (  546): Server unbound


Next up, AIDL Step 6: Creating an updated client

AIDL Step 4: Upgrading the API

Let's create a new API/src/main/java/org/eoti/android/test/apkextensions/api/v2/Registration.java
package org.eoti.android.test.apkextensions.api.v2;

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

public class Registration
implements Parcelable
{
    private String name = "unset";
    private String version = "unset";

    public static final Creator<Registration> CREATOR = new Creator<Registration>()
    {
        @Override
        public Registration createFromParcel(Parcel parcel) {
            return new Registration(parcel);
        }

        @Override
        public Registration[] newArray(int size) {
            return new Registration[size];
        }
    };

    public Registration()
    {

    }

    public Registration(Parcel in)
    {
        readFromParcel(in);
    }

    public Registration(org.eoti.android.test.apkextensions.api.v1.Registration deprecated)
    {
        this();
        this.name = deprecated.getName();
    }

    public void setName(String name){this.name = name;}
    public String getName(){return name;}
    public void setVersion(String version){this.version = version;}
    public String getVersion(){return version;}

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel out, int flags) {
        out.writeString(getName());
        out.writeString(getVersion());
    }

    public void readFromParcel(Parcel in)
    {
        setName(in.readString());
        setVersion(in.readString());
    }
}


You might be wondering why we don't extend the previous class.  It's because of the final Creator method.  If you try to send a v2 registration across the previous AIDL, it will send it as a v1.  Using a base class that they both extend would be doable - but I decided to keep the example simple.

Create a new API/src/main/java/org/eoti/android/test/apkextensions/api/v2/Registration.aidl
package org.eoti.android.test.apkextensions.api.v2;

parcelable Registration;


And let's create our new server API that contains some extra functionality...
API/src/main/java/org/eoti/android/test/apkextensions/api/v2/IServer.aidl
package org.eoti.android.test.apkextensions.api.v2;

import org.eoti.android.test.apkextensions.api.v2.Registration;

interface IServer
{
    void register(inout Registration registration);
    void unregister(in String registrationName);
    String getServerIdentifier();
}


Make sure it all compiles...
malachi@onyx:~/work/apkextensions$ cd API
malachi@onyx:~/work/apkextensions/API$ mvn clean install

Next up, AIDL Step 5: Upgrading the Service

AIDL Step 3: Creating the Client

malachi@onyx:~/work/apkextensions$ mvn archetype:generate -DarchetypeCatalog=http://repository-malachid.forge.cloudbees.com/public-snapshot/archetype-catalog.xml
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Building extension test 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] >>> maven-archetype-plugin:2.2:generate (default-cli) @ pom >>>
[INFO]
[INFO] <<< maven-archetype-plugin:2.2:generate (default-cli) @ pom <<<
[INFO]
[INFO] --- maven-archetype-plugin:2.2:generate (default-cli) @ pom ---
[INFO] Generating project in Interactive mode
[INFO] No archetype defined. Using maven-archetype-quickstart (org.apache.maven.archetypes:maven-archetype-quickstart:1.0)
Choose archetype:
1: http://repository-malachid.forge.cloudbees.com/public-snapshot/archetype-catalog.xml -> org.eoti.kryten:kryten-archetype (kryten-archetype)
2: http://repository-malachid.forge.cloudbees.com/public-snapshot/archetype-catalog.xml -> org.eoti.galatea:galatea-archetype (galatea-archetype)
3: http://repository-malachid.forge.cloudbees.com/public-snapshot/archetype-catalog.xml -> org.eoti.archtest:archtest-archetype (archtest-archetype)
Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): : 2
Define value for property 'groupId': : org.eoti.android.test.apkextensions.client1
Define value for property 'artifactId': : Client1
Define value for property 'version':  1.0-SNAPSHOT: :
Define value for property 'package':  org.eoti.android.test.apkextensions.client1: :
Confirm properties configuration:
groupId: org.eoti.android.test.apkextensions.client1
artifactId: Client1
version: 1.0-SNAPSHOT
package: org.eoti.android.test.apkextensions.client1
 Y: :
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating project from Archetype: galatea-archetype:1.1-SNAPSHOT
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: org.eoti.android.test.apkextensions.client1
[INFO] Parameter: artifactId, Value: Client1
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: package, Value: org.eoti.android.test.apkextensions.client1
[INFO] Parameter: packageInPathFormat, Value: org/eoti/android/test/apkextensions/client1
[INFO] Parameter: package, Value: org.eoti.android.test.apkextensions.client1
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: groupId, Value: org.eoti.android.test.apkextensions.client1
[INFO] Parameter: artifactId, Value: Client1
[WARNING] Don't override file /home/malachi/work/apkextensions/Client1/src/main/android/res/values/strings.xml
[WARNING] Don't override file /home/malachi/work/apkextensions/Client1/src/main/android/res/layout/main.xml
[INFO] project created from Archetype in dir: /home/malachi/work/apkextensions/Client1
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 25.201s
[INFO] Finished at: Sat Feb 18 07:32:17 PST 2012
[INFO] Final Memory: 13M/309M
[INFO] ------------------------------------------------------------------------


Take care of the keystore stuff in the pom...
and add the dependency:
        <dependency>
            <groupId>org.eoti.android.test.apkextensions.api</groupId>
            <artifactId>API</artifactId>
            <version>1.0-SNAPSHOT</version>
            <type>apklib</type>
        </dependency>
       
edit your Client1/src/main/java/org/eoti/android/test/apkextensions/client1/Client1Activity.java
package org.eoti.android.test.apkextensions.client1;

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.RemoteException;
import android.util.Log;
import org.eoti.android.test.apkextensions.api.v1.IServer;
import org.eoti.android.test.apkextensions.api.v1.Registration;

public class Client1Activity extends Activity {
    private static String TAG = "Client1";

    private static final String REG_NAME = Client1Activity.class.getName();
    private enum State{Unbound,Bound,Connected,Disconnected}
    private State state = State.Unbound;
    private IServer server;

    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            server = IServer.Stub.asInterface(iBinder);
            state = State.Connected;
            Log.d(TAG, "Server connected");
            try {
                Registration registration = new Registration();
                registration.setName(REG_NAME);
                server.register(registration);
                Log.d(TAG, "Registered " + registration.getName());
            } catch (RemoteException e) {
                Log.e(TAG, "Unable to register", e);
            }

            doTest();
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {
            state = State.Disconnected;
            server = null;
            Log.d(TAG, "Server disconnected");
        }
    };

    private void bindServer()
    {
        switch(state)
        {
            case Bound:
            case Connected:
            case Disconnected:
                return;
            default:
                bindService(new Intent(IServer.class.getName()), connection, Context.BIND_AUTO_CREATE);
                state = State.Bound;
                Log.e(TAG, "Server bound");
                break;
        }
    }

    private void unbindServer()
    {
        switch(state)
        {
            case Unbound:
                return;
            case Connected:
                try{
                    server.unregister(REG_NAME);
                }catch(RemoteException e){
                    Log.e(TAG, "Unable to unregister", e);
                }
                server = null;
                // fall through
            default:
                unbindService(connection);
                state = State.Unbound;
                Log.e(TAG, "Server unbound");
                break;
        }
    }

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

    @Override
    protected void onDestroy() {
        unbindServer();
        super.onDestroy();
    }

    protected void doTest()
    {
        Log.d(TAG, "Running tests...");
        // @TODO add tests here
        Log.d(TAG, "Tests done...");
        unbindServer();
    }
}


Let's compile it
malachi@onyx:~/work/apkextensions$ cd Client1
malachi@onyx:~/work/apkextensions/Client1$ mvn clean install

To verify it is working, watch logcat as you launch Client1
I/ActivityManager(   61): Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=org.eoti.android.test.apkextensions.client1/.Client1Activity } from pid 127
I/ActivityManager(   61): Start proc org.eoti.android.test.apkextensions.client1 for activity org.eoti.android.test.apkextensions.client1/.Client1Activity: pid=546 uid=10046 gids={1015}
I/ARMAssembler(   61): generated scanline__00000177:03515104_00001002_00000000 [ 87 ipp] (110 ins) at [0x445306f0:0x445308a8] in 538806 ns
I/ActivityManager(   61): Start proc org.eoti.android.test.apkextensions.server for service org.eoti.android.test.apkextensions.server/.TheServer: pid=554 uid=10045 gids={1015}
E/Client1 (  546): Server bound
I/ActivityManager(   61): Displayed org.eoti.android.test.apkextensions.client1/.Client1Activity: +1s140ms (total +10h57m29s956ms)
D/Client1 (  546): Server connected
D/TheServer(  554): Registration received:  org.eoti.android.test.apkextensions.client1.Client1Activity
D/Client1 (  546): Registered org.eoti.android.test.apkextensions.client1.Client1Activity
D/Client1 (  546): Running tests...
D/Client1 (  546): Tests done...
D/TheServer(  554): Registration removed: org.eoti.android.test.apkextensions.client1.Client1Activity
E/Client1 (  546): Server unbound


If you'd like to recompile everything at once, you can also use a top-level POM:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>org.eoti.android.test.apkextensions</groupId>
    <artifactId>pom</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
    <name>extension test</name>

    <modules>
        <module>API</module>
        <module>TheServer</module>
        <module>Client1</module>
  </modules>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <phase>package</phase>
                        <configuration>
                            <includeScope>runtime</includeScope>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
   

Just make sure that if you do, you update it whenever adding new modules.  IntelliJ will generally do that for you. Not sure about Eclipse.


So far, so good.  Now, let's try upgrading to a new version :)

Next up, AIDL Step 4: Upgrading the API

AIDL Step 2: Creating the Service

malachi@onyx:~/work/apkextensions$ mvn archetype:generate -DarchetypeCatalog=http://repository-malachid.forge.cloudbees.com/public-snapshot/archetype-catalog.xml
[INFO] ------------------------------------------------------------------------
[INFO] Building extension test 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] >>> maven-archetype-plugin:2.2:generate (default-cli) @ pom >>>
[INFO]
[INFO] <<< maven-archetype-plugin:2.2:generate (default-cli) @ pom <<<
[INFO]
[INFO] --- maven-archetype-plugin:2.2:generate (default-cli) @ pom ---
[INFO] Generating project in Interactive mode
[INFO] No archetype defined. Using maven-archetype-quickstart (org.apache.maven.archetypes:maven-archetype-quickstart:1.0)
Choose archetype:
1: http://repository-malachid.forge.cloudbees.com/public-snapshot/archetype-catalog.xml -> org.eoti.kryten:kryten-archetype (kryten-archetype)
2: http://repository-malachid.forge.cloudbees.com/public-snapshot/archetype-catalog.xml -> org.eoti.galatea:galatea-archetype (galatea-archetype)
3: http://repository-malachid.forge.cloudbees.com/public-snapshot/archetype-catalog.xml -> org.eoti.archtest:archtest-archetype (archtest-archetype)
Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): : 2
Define value for property 'groupId': : org.eoti.android.test.apkextensions.server
Define value for property 'artifactId': : TheServer
Define value for property 'version':  1.0-SNAPSHOT: :
Define value for property 'package':  org.eoti.android.test.apkextensions.server: :
Confirm properties configuration:
groupId: org.eoti.android.test.apkextensions.server
artifactId: TheServer
version: 1.0-SNAPSHOT
package: org.eoti.android.test.apkextensions.server
 Y: :
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating project from Archetype: galatea-archetype:1.1-SNAPSHOT
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: org.eoti.android.test.apkextensions.server
[INFO] Parameter: artifactId, Value: TheServer
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: package, Value: org.eoti.android.test.apkextensions.server
[INFO] Parameter: packageInPathFormat, Value: org/eoti/android/test/apkextensions/server
[INFO] Parameter: package, Value: org.eoti.android.test.apkextensions.server
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: groupId, Value: org.eoti.android.test.apkextensions.server
[INFO] Parameter: artifactId, Value: TheServer
[WARNING] Don't override file /home/malachi/work/apkextensions/TheServer/src/main/android/res/values/strings.xml
[WARNING] Don't override file /home/malachi/work/apkextensions/TheServer/src/main/android/res/layout/main.xml
[INFO] project created from Archetype in dir: /home/malachi/work/apkextensions/TheServer
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 22.380s
[INFO] Finished at: Sat Feb 18 07:13:45 PST 2012
[INFO] Final Memory: 11M/245M
[INFO] ------------------------------------------------------------------------


In TheServer/pom.xml make whatever keysigning changes you need (or remove the keysigning as we did in the previous step)
and then add the following dependency:
        <dependency>
            <groupId>org.eoti.android.test.apkextensions.api</groupId>
            <artifactId>API</artifactId>
            <version>1.0-SNAPSHOT</version>
            <type>apklib</type>
        </dependency>

Create TheServer/src/main/java/org/eoti/android/test/apkextensions/server/TheServer.java
package org.eoti.android.test.apkextensions.server;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import org.eoti.android.test.apkextensions.api.v1.IServer;
import org.eoti.android.test.apkextensions.api.v1.Registration;

import java.util.concurrent.ConcurrentHashMap;

public class TheServer extends Service
{
    private static final String TAG = TheServer.class.getSimpleName();
    private ConcurrentHashMap<String, IBinder> binders = new ConcurrentHashMap<String, IBinder>();
    private ConcurrentHashMap<String, Registration> registrations = new ConcurrentHashMap<String, Registration>();

    private IBinder createV1Binder()
    {
        return new IServer.Stub(){
            @Override
            public void register(Registration registration) throws RemoteException {
                if(registration == null) throw new RemoteException(); // API15 required for 'new RemoteException(string)'
                registrations.put(registration.getName(), registration);
                Log.d(TAG, "Registration received:  " + registration.getName());
            }

            @Override
            public void unregister(String registrationName) throws RemoteException {
                if(registrationName == null)  throw new RemoteException(); // API15 required for 'new RemoteException(string)'
                registrations.remove(registrationName);
                Log.d(TAG, "Registration removed: " + registrationName);            }
        };
    }
  
    public IBinder onBind(Intent intent) {
        if(intent == null) return null;
        String action = intent.getAction();
        if(action == null) return null;
        IBinder binder = binders.get(action);
        if(binder != null)
            return binder;
      
        if(IServer.class.getName().equals(action))
            binder = createV1Binder();
      
        if(binder != null)
            binders.put(action, binder);
      
        return binder;
    }

    @Override
    public void onDestroy() {
        registrations.clear();
        super.onDestroy();
    }
}


in TheServer/src/AndroidManifest.xml
replace:
<service android:name=".TheServer"/>
with: 
       <service android:name=".TheServer">
            <intent-filter>
                <action android:name="org.eoti.android.test.apkextensions.api.v1.IServer"/>
            </intent-filter>
        </service>


Make sure it compiles...
malachi@onyx:~/work/apkextensions$ cd TheServer
malachi@onyx:~/work/apkextensions/TheServer$ mvn clean install

Next up, AIDL Step 3: Creating the Client

AIDL Step 1: Creating the API

malachi@onyx:~/work/apkextensions$ mvn archetype:generate -DarchetypeCatalog=http://repository-malachid.forge.cloudbees.com/public-snapshot/archetype-catalog.xml
[INFO] Scanning for projects...
[INFO]                                                                       
[INFO] ------------------------------------------------------------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] >>> maven-archetype-plugin:2.2:generate (default-cli) @ standalone-pom >>>
[INFO]
[INFO] <<< maven-archetype-plugin:2.2:generate (default-cli) @ standalone-pom <<<
[INFO]
[INFO] --- maven-archetype-plugin:2.2:generate (default-cli) @ standalone-pom ---
[INFO] Generating project in Interactive mode
[INFO] No archetype defined. Using maven-archetype-quickstart (org.apache.maven.archetypes:maven-archetype-quickstart:1.0)
Choose archetype:
1: http://repository-malachid.forge.cloudbees.com/public-snapshot/archetype-catalog.xml -> org.eoti.kryten:kryten-archetype (kryten-archetype)
2: http://repository-malachid.forge.cloudbees.com/public-snapshot/archetype-catalog.xml -> org.eoti.galatea:galatea-archetype (galatea-archetype)
3: http://repository-malachid.forge.cloudbees.com/public-snapshot/archetype-catalog.xml -> org.eoti.archtest:archtest-archetype (archtest-archetype)
Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): : 2
Define value for property 'groupId': : org.eoti.android.test.apkextensions.api
Define value for property 'artifactId': : API
Define value for property 'version':  1.0-SNAPSHOT: :
Define value for property 'package':  org.eoti.android.test.apkextensions.api: :
Confirm properties configuration:
groupId: org.eoti.android.test.apkextensions.api
artifactId: API
version: 1.0-SNAPSHOT
package: org.eoti.android.test.apkextensions.api
 Y: :
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating project from Archetype: galatea-archetype:1.1-SNAPSHOT
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: org.eoti.android.test.apkextensions.api
[INFO] Parameter: artifactId, Value: API
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: package, Value: org.eoti.android.test.apkextensions.api
[INFO] Parameter: packageInPathFormat, Value: org/eoti/android/test/apkextensions/api
[INFO] Parameter: package, Value: org.eoti.android.test.apkextensions.api
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: groupId, Value: org.eoti.android.test.apkextensions.api
[INFO] Parameter: artifactId, Value: API
[WARNING] Don't override file /home/malachi/work/apkextensions/API/src/main/android/res/values/strings.xml
[WARNING] Don't override file /home/malachi/work/apkextensions/API/src/main/android/res/layout/main.xml
[INFO] project created from Archetype in dir: /home/malachi/work/apkextensions/API
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1:20.979s
[INFO] Finished at: Sat Feb 18 06:56:29 PST 2012
[INFO] Final Memory: 9M/245M
[INFO] ------------------------------------------------------------------------


delete API/src/main/java/org/eoti/android/test/apkextensions/api/APIActivity.java
remove that activity from API/src/AndroidManifest.xml

create an API/src/main/java/org/eoti/android/test/apkextensions/api/v1/Registration.java
package org.eoti.android.test.apkextensions.api.v1;

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

public class Registration
implements Parcelable
{
    private String name = "unset";

    public static final Creator<Registration> CREATOR = new Creator<Registration>()
    {
        @Override
        public Registration createFromParcel(Parcel parcel) {
            return new Registration(parcel);
        }

        @Override
        public Registration[] newArray(int size) {
            return new Registration[size];
        }
    };

    public Registration()
    {

    }

    public Registration(Parcel in)
    {
        readFromParcel(in);
    }

    public void setName(String name){this.name = name;}
    public String getName(){return name;}

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel out, int flags) {
        out.writeString(getName());
    }

    public void readFromParcel(Parcel in)
    {
        setName(in.readString());
    }
}

create an API/src/main/java/org/eoti/android/test/apkextensions/api/v1/Registration.aidl
package org.eoti.android.test.apkextensions.api.v1;

parcelable Registration;


create an API/src/main/java/org/eoti/android/test/apkextensions/api/v1/IServer.aidl
package org.eoti.android.test.apkextensions.api.v1;

import org.eoti.android.test.apkextensions.api.v1.Registration;

interface IServer
{
    void register(inout Registration registration);
    void unregister(in String registrationName);
}
The 'inout' bit specifies that the object could be modified by the server. Not really necessary in this particular case, but I can see where it might be useful for registration type objects (adding shared keys to it, expiration dates, etc) so I put it in.

in API/pom.xml
change:  
<packaging>apk</packaging>
to:  
<packaging>apklib</packaging>

Remove these executions:
                   
                    <execution>
                        <id>android-undeploy</id>
                        <phase>pre-clean</phase>
                        <goals>
                            <goal>undeploy</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>android-deploy</id>
                        <phase>install</phase>
                        <goals>
                            <goal>deploy</goal>
                        </goals>
                    </execution>
                   
                       
                   
                   
also, make sure to either setup a keystore or, as in this case, remove the signing by removing the following:
                    <sign>
                        <debug>false</debug>
                    </sign>
and:
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jarsigner-plugin</artifactId>
                <version>1.2</version>
                <executions>
                    <execution>
                        <id>signing</id>
                        <goals>
                            <goal>sign</goal>
                        </goals>
                        <phase>package</phase>
                        <inherited>true</inherited>
                        <configuration>
                            <archiveDirectory></archiveDirectory>
                            <includes>
                                <include>target/*.apk</include>
                            </includes>
                            <!-- see http://developer.android.com/guide/publishing/app-signing.html -->
                            <keystore>android.keystore</keystore>
                            <storepass>YOUR_STOREPASS_HERE</storepass>
                            <keypass>YOUR_KEYPASS_HERE</keypass>
                            <alias>YOUR_ALIAS_HERE</alias>
                        </configuration>
                    </execution>
                </executions>
            </plugin>


At this point, you can verify that it compiles. 
Make sure that you have the emulator running or device connected and compile it:
malachi@onyx:~/work/apkextensions$ cd API
malachi@onyx:~/work/apkextensions/API$ mvn clean install


Next up, AIDL Step 2: Creating the Service