Monday, October 3, 2011

AIR extensions in Java for Android Part 1 -- Creating the Java code

To create the Java part of an AIR native extension, you implement the FREExtension interface, create a context class extending FREContext, and create one or more functions by implementing the FREFunction interface. Along the way, you'll become friendly with the FREObject class. Finally, you export the code as a Jar file.




I'm using Flash Builder/Eclipse with the Android plugin and Java tools installed.

Starting with the Java part of the extension:
Create a new Android project
-Pick the appropriate Android or Google SDK 2.2+ if you pick an SDK that is higher than that required by AIR (i.e. >2.2) you will need to provide a mechanism for failing gracefully on devices with an OS that doesn't have that API level.
//Question: How to fail gracefully in this situation
-Don't create an activity (uncheck checkbox)

When the project is created, open the project properties.
Add the AIR extension library, FlashRuntimeExtensions.jar from your AIR 3+ SDK in folder /yourpath/AIR3/lib/android/ as an external Jar
(Properties>Java Build Path > Add external JARs)

Create the extension class:
Implement FREExtension 
The only function that HAS to do anything is the createContext() function. This function creates the contexts provided by the extension on demand. It is called by the runtime when the ActionScript part of the extension new ups an ExtensionContext.

The string passed to the createContext() function can be used by your extension code to decide what kind of context to create. It is really up to you, the runtime just sees the value as an arbitrary string. For example, your extension could provide different contexts for different purposes and use this string to tell which one is wanted by the app.

Since we haven't defined a context class yet, we'll have to skip ahead for a moment to the FREContext class. But read the following notes:

initialize() is called by the runtime and can be used to create any persistent resources needed by the extension. However, not every extension will need to do anything in this function.

dispose(), also called by the runtime,  can be used to cleanup any resources created by initialize(). Again, not every extension will have resources to dispose.

Create a context class
Add a new class to the project that extends FREContext.

Your class will have a constructor, a dispose() method, and the key part: getFunctions().
The getFunctions() method creates a Java Map object that contains all the functions which can be called by the ActionScript part of the extension. The map contains a string identifier as the map key and the constructor of a FREFunction instance. Since we haven't created any functions yet, let's skip ahead again to create an FREFunction class.

Create a function class:
Add a new class to the project that implements the FREFunction interface.
The FREFunction has one method that you must implement: call(). The call() method does the actual work for your extension, so this is where things get interesting. The call() method has two parameters: a reference to the Context object that was used to call this function and an array of arguments. The function returns an FREObject, which is the base class for passing data between the Java and the ActionScript parts of your extension.

So let's examine a real FREFunction. In the following example, the function logic is about as simple as you can get. The function takes a single, boolean value and returns the inverse:

package com.example;

import com.adobe.fre.FREContext;
import com.adobe.fre.FREFunction;
import com.adobe.fre.FREInvalidObjectException;
import com.adobe.fre.FREObject;
import com.adobe.fre.FRETypeMismatchException;
import com.adobe.fre.FREWrongThreadException;

import android.util.Log;

public class ExchangeBooleanFunction implements FREFunction {
    public static final String NAME = "exchangeBooleanFunction";
    private String tag;
   
    public FREObject call(FREContext arg0, FREObject[] arg1) {
        DataExchangeContext ctx = (DataExchangeContext) arg0;
        tag = ctx.getIdentifier() + "." + NAME;
        Log.i( tag, "Invoked " + NAME );
        FREObject returnValue = null;
       
        try {
           
            Boolean value = arg1[0].getAsBool();
            returnValue = FREObject.newObject( !value );//Invert the received value
           
        } catch (IllegalStateException e) {
            Log.d(tag, e.getMessage());
            e.printStackTrace();
        } catch (FRETypeMismatchException e) {
            Log.d(tag, e.getMessage());
            e.printStackTrace();
        } catch (FREInvalidObjectException e) {
            Log.d(tag, e.getMessage());
            e.printStackTrace();
        } catch (FREWrongThreadException e) {
            Log.d(tag, e.getMessage());
            e.printStackTrace();
        }
       
        return returnValue;
    }

}

There are a couple of things going on, even in this simple function, but the meat of the logic is implemented with these two lines:

            Boolean value = arg1[0].getAsBool();
            returnValue = FREObject.newObject( !value );//Invert the received value

First, the argument to the function, which is expected to be a boolean is read from the arg1 array. Since arg1 is an array of FREObjects, we use the getASBool() method of FREObject to read the data provided by the ActionScript caller into a Java Boolean value. We have to returnn the data as an FREObject, so we use the FREObject factory method, newObject(), with a Boolean argument to create the return value containing the negated truth value. FREObjects are essentially the Java interface for accessing ActionScript objects (and primitive values).

Another thing worth pointing out, is the use of the Android Log class. The Log class lets you log strings to the Android system log, which can be viewed at run time using the adb logcat. ActionScript trace statements are automatically sent to the log, but in Java, android.utils.Log is the way to do it. I've set up a way to pass extension and context names to the function, so I don't hard code the log tag in each function.   I've also created a NAME constant for the function, which is used both for the Log tag and for the context function map -- which brings us back to our FREContext implementation. Now that we have a function we can finish our context object.

Add the function to the context
To make the function available to the ActionScript part of the extension, it has to be in the Java Map object returned by the context getFunctions() method:

    @Override
    public Map<String, FREFunction> getFunctions() {
        Map<String, FREFunction> functionMap = new HashMap<String, FREFunction>();

        functionMap.put( ExchangeBooleanFunction.NAME, new ExchangeBooleanFunction() );
       
        return functionMap;
    }

This code creates a HashMap, and inserts the function using the function name and a new instance of the function class. The name string is used by the ActionScript extension code to invoke the function.

The entire context class looks like this:

package com.example;

import java.util.HashMap;
import java.util.Map;

import android.util.Log;

import com.adobe.fre.FREContext;
import com.adobe.fre.FREFunction;

public class DataExchangeContext extends FREContext {
    private static final String CTX_NAME = "DataExchangeContext";
    private String tag;
   
    public DataExchangeContext( String extensionName ) {
        tag = extensionName + "." + CTX_NAME;
        Log.i(tag, "Creating context");
    }

    @Override
    public void dispose() {
        // TODO Auto-generated method stub

    }

    @Override
    public Map<String, FREFunction> getFunctions() {
        Map<String, FREFunction> functionMap = new HashMap<String, FREFunction>();

        functionMap.put( ExchangeBooleanFunction.NAME, new ExchangeBooleanFunction() );
       
        return functionMap;
    }
   
    public String getIdentifier()
    {
        return tag;
    }

}

In addition to putting our lone function into the Map object, I've added some support for logging, as explained in the section on functions, above.

And now that the context class is complete, we can finish of the implementation of FREExtension, too.

Adding the Context to the extension
To add the context to the extension, we just have to return an instance when the createContext() method is called:

    private static final String EXT_NAME = "DataExchangeExtension";
    private DataExchangeContext context = new DataExchangeContext(EXT_NAME);

    public FREContext createContext(String arg0) {
        return context;
    }

Since our extension only uses a single context, I create the context when the extension object is created. My createContext() function always returns the same instance. There's more than one way to do it. For example, if each context is associated with a particular data object on the ActionScript side, you could create a new instance every time createContext() is called. You can also create different types of context objects for different purposes. In this case, use arg0 parameter to pass the type of context wanted by the ActionScript code.

The full extension class looks like this:

package com.example;

import com.adobe.fre.FREContext;
import com.adobe.fre.FREExtension;

public class DataExchangeExtension implements FREExtension {
    private static final String EXT_NAME = "DataExchangeExtension";
    private DataExchangeContext context = new DataExchangeContext(EXT_NAME);

    public FREContext createContext(String arg0) {
        return context;
    }

    public void dispose() {
        // nothing to dispose for this example
    }

    public void initialize() {
        // nothing to initialize for this example
    }

}

Compiling the code as a JAR file
The Android perspective in Eclipse doesn't support jarring the files. But you can still do it, you just have to find the buried export as jar function:
File > Export
Java > Jar file
Click Next
Select your project under resources to include. (Don't include the Android res folder in the jar file, put them in the ANE folder later.)
Enter a name for the JAR file
Click Finish

You can create the Jar file in the ANE (that's AIR Native Extension, if I forgot to mention it earlier) folder. It should go into a subfolder for the Android platform. You will also put ActionScript ANE library.swf file and the Android res folder in this subdirectory (put the res folder here if you are using resources.) 

3 comments:

  1. Really helpful, thanks a lot. ;)

    ReplyDelete
  2. Thanks a lot, Rough.
    when I export jar file, if my native extension java codes use another android library file and some config file from assets floder, should I include those assets and android library project (.so file) at the same time when I export jar file, or I should do it when i package ane fiel?

    ReplyDelete
    Replies
    1. I figured it out, need copy the .so project into the native android folder, names as libs.
      /libs/armeabi-v7a/*.so.
      Thanks for your blog.

      Delete

Note: Only a member of this blog may post a comment.