Search This Blog

18 February 2012

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

23 January 2012

Reverse USB Tethering

I decided to give reverse usb tethering a shot. I don't know which devices easily support this, but I assume you have to be root. In my particular case, I was using an ICS device.  These steps could easily screw up your existing cell routing - so make sure your willing to risk it.

Step 0: Make sure the device is booted and connected via USB

Step 1: Enable tethering on the device side
  • Go to Settings
  • Under the Wireless & Networks, click on More
  • Click on Tethering & portable hotspot
  • Enable USB Tethering
Step 2: Get the IP Address of the device
malachi@onyx:~$ adb shell
root@android:/ # netcfg
lo       UP                                   127.0.0.1/8   0x00000049 00:00:00:00:00:00
dummy0   DOWN                                   0.0.0.0/0   0x00000082 62:bc:02:e1:d5:ea
rmnet0   DOWN                                   0.0.0.0/0   0x00000000 00:00:00:00:00:00
rmnet1   DOWN                                   0.0.0.0/0   0x00000000 00:00:00:00:00:00
rmnet2   DOWN                                   0.0.0.0/0   0x00000000 00:00:00:00:00:00
rmnet3   DOWN                                   0.0.0.0/0   0x00000000 00:00:00:00:00:00
rmnet4   DOWN                                   0.0.0.0/0   0x00000000 00:00:00:00:00:00
rmnet5   DOWN                                   0.0.0.0/0   0x00000000 00:00:00:00:00:00
rmnet6   DOWN                                   0.0.0.0/0   0x00000000 00:00:00:00:00:00
rmnet7   DOWN                                   0.0.0.0/0   0x00000000 00:00:00:00:00:00
sit0     DOWN                                   0.0.0.0/0   0x00000080 00:00:00:00:00:00
rndis0   UP                              192.168.42.129/24  0x00001043 da:02:11:22:14:af
Step 3: Get the matching IP Address of the workstation
malachi@onyx:~$ ifconfig -a usb0
usb0      Link encap:Ethernet  HWaddr e2:31:b5:d7:f3:7b 
          inet addr:192.168.42.74  Bcast:192.168.42.255  Mask:255.255.255.0
          inet6 addr: fe80::e031:b5ff:fed7:f37b/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:1990 errors:0 dropped:0 overruns:0 frame:0
          TX packets:1188 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:181334 (181.3 KB)  TX bytes:657452 (657.4 KB)
Step 4: Ping the device from the workstation
malachi@onyx:~$ ping 192.168.42.129
PING 192.168.42.129 (192.168.42.129) 56(84) bytes of data.
64 bytes from 192.168.42.129: icmp_req=1 ttl=64 time=1.01 ms
64 bytes from 192.168.42.129: icmp_req=2 ttl=64 time=0.605 ms

Step 5: Ping the workstation from the device
root@android:/ # ping 192.168.42.74
PING 192.168.42.74 (192.168.42.74) 56(84) bytes of data.
64 bytes from 192.168.42.74: icmp_seq=1 ttl=64 time=0.427 ms
64 bytes from 192.168.42.74: icmp_seq=2 ttl=64 time=0.397 ms
Step 6: Add workstation as the default gateway for the device
root@android:/ # route add default gw 192.168.42.74 dev rndis0
Step 7: Get a list of DNS servers from the workstation
malachi@onyx:~$ cat /etc/resolv.conf
nameserver 172.29.224.11
nameserver 8.8.8.8
Step 8: Add them to the device
root@android:/ # setprop net.dns1 172.29.224.11
root@android:/ # setprop net.dns2 8.8.8.8
Step 9: Setup forwarding from usb0 to eth1 on the workstation
[ most people probably use eth0. Just use ifconfig to check which one is active]

malachi@onyx:~$ sudo su - root
[sudo] password for malachi:
root@onyx:~# echo 1 > /proc/sys/net/ipv4/ip_forward
root@onyx:~# exit
malachi@onyx:~$ sudo iptables --flush -t nat
malachi@onyx:~$ sudo iptables --table nat --append POSTROUTING --out-interface eth1 -j MASQUERADE
malachi@onyx:~$ sudo iptables --append FORWARD --in-interface usb0 -j ACCEPT
Step 10: ping google ;)

malachi@onyx:~$ adb shell
root@android:/ # ping google.com
PING google.com (74.125.127.99) 56(84) bytes of data.
64 bytes from pz-in-f99.1e100.net (74.125.127.99): icmp_seq=1 ttl=51 time=11.8 ms
64 bytes from pz-in-f99.1e100.net (74.125.127.99): icmp_seq=2 ttl=51 time=11.9 ms
And finally, check things like the Browser...






03 January 2012

Fragments

Well, I've been needing to look into Fragments for awhile now...

First, to generate a blank project...

malachi@onyx:~/work$ 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.fragments
Define value for property 'artifactId': : FragmentTest
Define value for property 'version':  1.0-SNAPSHOT: :
Define value for property 'package':  org.eoti.android.test.fragments: :
Confirm properties configuration:
groupId: org.eoti.android.test.fragments
artifactId: FragmentTest
version: 1.0-SNAPSHOT
package: org.eoti.android.test.fragments
 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.fragments
[INFO] Parameter: artifactId, Value: FragmentTest
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: package, Value: org.eoti.android.test.fragments
[INFO] Parameter: packageInPathFormat, Value: org/eoti/android/test/fragments
[INFO] Parameter: package, Value: org.eoti.android.test.fragments
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: groupId, Value: org.eoti.android.test.fragments
[INFO] Parameter: artifactId, Value: FragmentTest
[WARNING] Don't override file /home/malachi/work/FragmentTest/src/main/android/res/values/strings.xml
[WARNING] Don't override file /home/malachi/work/FragmentTest/src/main/android/res/layout/main.xml
[INFO] project created from Archetype in dir: /home/malachi/work/FragmentTest
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 21.579s
[INFO] Finished at: Tue Jan 03 08:30:32 PST 2012
[INFO] Final Memory: 8M/245M
[INFO] ------------------------------------------------------------------------
malachi@onyx:~/work$ cd FragmentTest/
malachi@onyx:~/work/FragmentTest$ tree
.
├── pom.xml
└── src
    ├── AndroidManifest.xml
    └── main
        ├── android
        │   └── res
        │       ├── drawable
        │       │   └── icon.png
        │       ├── layout
        │       │   └── main.xml
        │       └── values
        │           └── strings.xml
        └── java
            └── org
                └── eoti
                    └── android
                        └── test
                            └── fragments
                                └── FragmentTestActivity.java

13 directories, 6 files


While it should be possible to use the Fragments with pre-HONEYCOMB (http://android-developers.blogspot.com/2011/03/fragments-for-all.html); but for now, let's just try it in the ICS emulator.

Based on the available jars in search.maven.org (http://search.maven.org/#artifactdetails|com.google.android|android|4.0.1.2|jar) let's set the version of the Android dependency in the pom to 4.0.1.2.

Also, change the <sdk><platform>10</platform></sdk> to <sdk><platform>14</platform></sdk>.

For now, comment out the maven-jarsigner-plugin as that requires setting up a keystore.
You'll also need to comment out the <sign /> portion of the android-maven-plugin.

Start up an ICS emulator and try 'mvn clean install' just to make sure we are able to grab the dependencies.
Launch the FragmentTest and you should see your hello world.

Let's move on to adding fragments... this part is derived from http://www.vogella.de/articles/Android/article.html#fragments_tutorial
I've modified their instructions quite a bit to fit into our Maven structure as well as to make things more clear (they had ListFragment extends ListFragment which tends to confuse people).

First, copy src/main/android/res/layout/main.xml to src/main/android/res/layout/detail.xml
On the new detail.xml, give the TextView an id:
android:id="@+id/ftDetailText"

Now, we'll deal the with portrait layout for FragmentTestActivity:
Replace the TextView in the layout/main.xml with:
    <fragment
        android:id="@+id/ftListFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        class="org.eoti.android.test.fragments.FTListFragment"
    />

Now, let's modify it for landscape...

Copy src/main/android/res/layout/main.xml to src/main/android/res/layout-land/main.xml [note: you will have to create the layout-land directory]

Now, for the layout-land/main.xml:
1. change the LinearLayout to Horizontal:
android:orientation="horizontal"
2. copy/paste the fragment so you now have 2 of them
3. for the first one. let's change the width to:
android:layout_width="300dip"
4. for the second one, change the id to:
android:id="@+id/ftDetailFragment"
5. Change the class to
class="org.eoti.android.test.fragments.FTDetailFragment"



Note: I changed the name of these classes so it would be obvious that these are our classes, and not some default Android implementation. Perhaps a redundant point since we also set the package name.


We'll need to have a separate activity for the detail portion in portrait mode, so let's create the layout for that.
Copy src/main/android/res/layout-land/main.xml to src/main/android/res/layout/details.xml [note: plural details not detail]

For the new details.xml:
1. change the LinearLayout back to Vertical:
android:orientation="vertical"
2. remove the ftListFragment <fragment />

We already have our main FragmentTestActivity.  We are missing our DetailActivity. Let's do some more copy/paste.
Copy FragmentTestActivity.java to DetailActivity.java (in the same directory)
Let's edit DetailActivity.java:
1. Change the class name to DetailActivity [I'd change the TAG to match]
2. change the setContentView to R.layout.details
3. add the following just after setContentView:
        Bundle extras = getIntent().getExtras();
        if (extras != null) {
            TextView view = (TextView) findViewById(R.id.ftDetailText);
            view.setText(System.getProperty(extras.getString("key")));
        }
4. Make sure to add the import android.widget.TextView;

We've defined two fragments in the layouts.  We need to create those.
First, we'll create our src/main/java/org/eoti/android/test/fragments/FTDetailFragment.java
This one is pretty simple:

package org.eoti.android.test.fragments;

import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class FTDetailFragment extends Fragment
{
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.detail, container,  false);
    }

    public void setText(String key)
    {
        ((TextView)getView().findViewById(R.id.ftDetailText)).setText(System.getProperty(key));
    }

}


Next, we'll create our src/main/java/org/eoti/android/test/fragments/FTListFragment.java
This one isn't much more difficult:

package org.eoti.android.test.fragments;

import android.*;
import android.R;
import android.app.ListFragment;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;

import java.util.ArrayList;

public class FTListFragment extends ListFragment
{
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        ArrayList keys = new ArrayList(System.getProperties().keySet());
        ArrayAdapter adapter = new ArrayAdapter(getActivity(), R.layout.simple_list_item_1, keys);
        setListAdapter(adapter);
    }

    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        String key = getListAdapter().getItem(position).toString();
        FTDetailFragment fragment = (FTDetailFragment)getFragmentManager().findFragmentById(org.eoti.android.test.fragments.R.id.ftDetailFragment);
        if(fragment != null)
        {
            fragment.setText(key);
        }else{
            Intent intent = new Intent(getActivity().getApplicationContext(), DetailActivity.class);
            intent.putExtra("key", key);
            startActivity(intent);
        }
    }
}

Last, but not least, we need to add the other activity to your AndroidManifest. Put this inside your <application /> tag.
          <activity android:name=".DetailActivity" />



mvn clean install

Launch the application (in portrait mode, I assume)
Select something. You should see a new activity popup with the value of the property you chose.
Hit back, and try it a few times.
When you are done, rotate the emulator using CTRL-F11.  I'm not sure about anyone else, but I have to hit it twice quickly or it won't rotate.
Now, when you select an item on the left, the answer is on the right.
When you are done playing with it, CTRL-F12 [twice in my case] to go back to portrait.

UPDATE: If you wish to use it with older devices...
First, I used the  maven-android-sdk-deployer to create the maven-compliant compat library for me.

        <dependency>
            <groupId>android.support</groupId>
            <artifactId>compatibility-v13</artifactId>
            <version>r6</version>
        </dependency>

This allows older versions of Android to use the Fragments. In my case, I tested against an Android 2.3.3 emulator.

I made no other changes to the pom.  IE: I compiled against a newer SDK and deployed against both.

Other changes to make:
  1. make FTDetailFragment extend android.support.v4.app.Fragment
  2. make FTListFragment extend android.support.v4.app.ListFragment
  3. make DetailActivity extend android.support.v4.app.FragmentActivity
  4. make FragmentTestActivity extend android.support.v4.app.FragmentActivity
  5. In FTListFragment, instead of using getFragmentManager you need to call getSupportFragmentManager.  This is part of the FragmentActivity, so you will need to do something like:
((FragmentActivity)getActivity()).getSupportFragmentManager()...

mvn clean install and it should all work... close down the 2.3.3 emulator, start up an ICS emulator and.. it will still work ;)