Extending AIR for Android
*** The following is totally unsupported by Adobe ***
*** UPDATE: Adobe has officially added native extensions to AIR. I highly recommend you use that approach instead of mine. ***
Adobe AIR provides a consistent platform for desktop and mobile apps. While consistency is very important there are times when developers need to extend beyond the common APIs. This article will walk you through how to integrate AIR for Android applications with other native APIs and functionality in the Android SDK. It covers three common use cases for native extensibility: System Notifications, Widgets, and Application Licensing.
If you’d like to follow along you will need the following prerequisites:
- [Adobe Flash Builder 4.5][1](which includes the Flex 4.5 SDK and AIR 2.6 SDK)
- [Android SDK][2]
- [Android Eclipse Plugin][3]
Before getting started, a little background will help. Android applications are distributed as APK files. An APK file contains the Dalvik executable (dex), which will run on an Android device inside the Dalvik VM. The Android SDK compiles a Java-like language to dex.
AIR for Android applications are also distributed as APK files. Inside of these APK files is a small bit of dex that bootstraps the AIR for Android runtime, which then loads and runs the SWF file that is also inside of the APK. The actual dex class that bootstraps the AIR application is dynamically generated by the adt tool in the AIR SDK. The class is named AppEntry and its package name depends on the AIR application ID, but it always begins with “air”. The AppEntry class checks for the existence of the AIR runtime and then launches the AIR application. The Android descriptor file in an AIR APK specifies that the main application class is the AppEntry class.
To extend AIR for Android applications to include native APIs and Android SDK functionality, you start by creating a SWF file using Flex and then copy that SWF file, the dex classes for AIR for Android, and the required resources into a standard Android project. By using the original AppEntry class you can still bootstrap the AIR application in the Android project but you can extend that class to gain a startup hook.
- To get started, download a package with the required dependencies for extending AIR for Android:
[http://www.jamesward.com/downloads/extending\_air\_for\_android-flex\_4\_5-air\_2\_6-v\_1.zip][4]
- Next, create a regular Android project in Eclipse (do not create an Activity yet):
<img src="http://www.jamesward.com/wp/uploads/2011/05/new_android_project.jpg" alt="" title="New Android Project" width="613" height="905" class="alignnone size-full wp-image-2326" srcset="https://www.jamesward.com/uploads/2011/05/new_android_project.jpg 613w, https://www.jamesward.com/uploads/2011/05/new_android_project-203x300.jpg 203w" sizes="(max-width: 613px) 100vw, 613px" />
- Copy all of the files from the zip file you downloaded into the root directory of the newly created Android project. You will need to overwrite the existing files and update the launch configuration (if Eclipse asks you to).
- Delete the “res/layout” directory.
- Add the airbootstrap.jar file to the project’s build path. You can do that by right-clicking on the file, then select Build Path and then Add to Build Path.
- Verify that the project runs. You should see “hello, world” on your Android device. If so, then the AIR application is properly being bootstrapped and the Flex application in assets/app.swf is correctly being run. At this point if you do not need any custom startup hooks then you can simply replace the assets/app.swf file with your own SWF file (but it must be named app.swf). If you do need a custom startup hook then simply create a new Java class named “MainApp” that extends the air.app.AppEntry class.
<img src="http://www.jamesward.com/wp/uploads/2011/05/new_android_class.png" alt="" title="New Android Class" width="652" height="720" class="alignnone size-full wp-image-2327" srcset="https://www.jamesward.com/uploads/2011/05/new_android_class.png 652w, https://www.jamesward.com/uploads/2011/05/new_android_class-271x300.png 271w" sizes="(max-width: 652px) 100vw, 652px" /></li>
* Override the onCreate() method and add your own startup logic before super.onCreate() is called (which loads the AIR app). Here is an example: ```actionscript
package com.jamesward;
import air.app.AppEntry; import android.os.Bundle;
public class MainApp extends AppEntry {
@Override
public void onCreate(Bundle arg0) {
System.out.println("test test");
super.onCreate(arg0);
}
}
* Open the AndroidManifest.xml descriptor file and tell it to use the new MainApp class instead of the original AppEntry class. First change the package to be the same as your MainApp’s package: ```xml
<manifest package="com.jamesward" android:versionCode="1000000" android:versionName="1.0.0"
xmlns:android="http://schemas.android.com/apk/res/android">
Also update the activity to use the MainApp class (make sure you have the period before the class name):
```xml
<activity android:name=".MainApp"
You can also add any other permissions or settings you might need in the AndroidManifest.xml file.</li>
* Save the changes and, when Eclipse prompts you, update the launch configuration.
* Run the application and you should again see “hello, world”. This time, however, in LogCat (command line tool or Eclipse view) you should see the “test test” output. Now that you have a startup hook, you can do some fun stuff!</ol>
**System Notifications and Services**
AIR for Android applications don’t yet have an API to do Android system notifications. But you can add system notifications to your AIR for Android application through a startup hook. In order for the AIR application to communicate with the native Android APIs you must provide a bridge for the communication. The simplest way to create that bridge is using a network socket. The Android application can listen for data on the socket and then read that data and determine if it needs to display a system notification. Then the AIR application can connect to the socket and send the necessary data. This is a pretty straightforward example but some security (for instance a key exchange) should be implemented to insure that malicious apps don’t discover and abuse the socket. Also some logic to determine which socket should be used would likely be necessary.
1. Inside the application section add a new Android Service:
```xml
<service android:enabled="true" android:name="TestService" />
```</li>
2. Since this example uses a Socket you will also need to add the INTERNET permission:
```xml
<uses-permission android:name="android.permission.INTERNET"/>
```</li>
3. You might also want to enable the phone to vibrate when there is a new notification. If so add that permission as well: ```xml
<uses-permission android:name="android.permission.VIBRATE"/>
4. Save your changes to AndroidManifest.xml.
5. Next, create the background Java Service class, called TestService. This service will listen on a socket and when necessary, display an Android Notification: ```java
package com.jamesward;
import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket;
import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.os.IBinder; import android.os.Looper; import android.util.Log;
public class TestService extends Service { private boolean stopped=false; private Thread serverThread; private ServerSocket ss;
@Override public IBinder onBind(Intent intent) { return null; }
@Override public void onCreate() { super.onCreate();
Log.d(getClass().getSimpleName(), "onCreate");
serverThread = new Thread(new Runnable() {
public void run()
{
try
{
Looper.prepare();
ss = new ServerSocket(12345);
ss.setReuseAddress(true);
ss.setPerformancePreferences(100, 100, 1);
while (!stopped)
{
Socket accept = ss.accept();
accept.setPerformancePreferences(10, 100, 1);
accept.setKeepAlive(true);
DataInputStream _in = null;
try
{
_in = new DataInputStream(new BufferedInputStream(accept.getInputStream(),1024));
}
catch (IOException e2)
{
e2.printStackTrace();
}
int method =_in.readInt();
switch (method)
{
// notification
case 1:
doNotification(_in);
break;
}
}
}
catch (Throwable e)
{
e.printStackTrace();
Log.e(getClass().getSimpleName(), "Error in Listener",e);
}
try
{
ss.close();
}
catch (IOException e)
{
Log.e(getClass().getSimpleName(), "keep it simple");
}
}
},"Server thread");
serverThread.start();
}
private void doNotification(DataInputStream in) throws IOException { String id = in.readUTF(); displayNotification(id); }
@Override public void onDestroy() { stopped=true; try { ss.close(); } catch (IOException e) {} serverThread.interrupt(); try { serverThread.join(); } catch (InterruptedException e) {} }
public void displayNotification(String notificationString) { int icon = R.drawable.mp_warning_32x32_n; CharSequence tickerText = notificationString; long when = System.currentTimeMillis(); Context context = getApplicationContext(); CharSequence contentTitle = notificationString; CharSequence contentText = “Hello World!”;
Intent notificationIntent = new Intent(this, MainApp.class);
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
Notification notification = new Notification(icon, tickerText, when);
notification.vibrate = new long[] {0,100,200,300};
notification.setLatestEventInfo(context, contentTitle, contentText, contentIntent);
String ns = Context.NOTIFICATION_SERVICE;
NotificationManager mNotificationManager = (NotificationManager) getSystemService(ns);
mNotificationManager.notify(1, notification);
}
}
This service listens on port 12345. When it receives some data it checks if the first “int” sent is “1”. If so, it then creates a new notification using the next piece of data (a string) that is received over the socket.</li>
* Modify the MainApp Java class to start the service when the onCreate() method is called: ```java
@Override
public void onCreate(Bundle savedInstanceState)
{
try
{
Intent srv = new Intent(this, TestService.class);
startService(srv);
}
catch (Exception e)
{
// service could not be started
}
super.onCreate(savedInstanceState);
}
That is all you need to do in the Android application.</li>
* Next, create a Flex application that will connect to the socket and send the right data. Here is some sample code for my Notifier.mxml class, which I used to test the Android service: ```xml
<s:Application xmlns:fx=“http://ns.adobe.com/mxml/2009" xmlns:s=“library://ns.adobe.com/flex/spark”>
fx:Style @namespace s “library://ns.adobe.com/flex/spark”;
global {
fontSize: 32;
}
</fx:Style>
<s:layout> <s:VerticalLayout horizontalAlign=“center” paddingTop=“20”/> </s:layout>
<s:TextInput id=“t” text=“test test”/>
<s:Button label=“create notification”> <s:click>
var s:Socket = new Socket();
s.connect("localhost", 12345);
s.addEventListener(Event.CONNECT, function(event:Event):void {
trace('connected!');
(event.currentTarget as Socket).writeInt(1);
(event.currentTarget as Socket).writeUTF(t.text);
(event.currentTarget as Socket).flush();
(event.currentTarget as Socket).close();
});
s.addEventListener(IOErrorEvent.IO_ERROR, function(event:IOErrorEvent):void {
trace('error! ' + event.errorID);
});
s.addEventListener(ProgressEvent.SOCKET_DATA, function(event:ProgressEvent):void {
trace('progress ');
});
</s:click>
</s:Button>
</s:Application>
As you can see there is just a TextInput control that allows the user to enter some text. Then when the user clicks the Button the AIR for Android application connects to a local socket on port 12345, writes an int with the value of 1, writes the string that the user typed into the TextInput control, and finally flushes and closes the connection. This causes the notification to be displayed.</li>
* Now simply compile the Flex app and overwrite the assets/app.swf file with the new Flex application. Check out a [video demonstration][5] of this code.</ol>
**Widgets**
Widgets in Android are the mini apps that can be displayed on the home screen of the device. There is a fairly limited amount of things that can be displayed in Widgets. So unfortunately Widgets can’t be built with AIR for Android. However a custom application Widget can be packaged with an AIR for Android application. To add a Widget to an AIR for Android application you can use the default AppEntry class instead of wrapping it with another class (MainApp in my example). (It doesn’t, however, do any harm to keep the MainApp class there.) To add a Widget simply add its definition to the AndroidManifest.xml file, create the Widget with Java, and create a corresponding layout resource.
1. First define the Widget in the application section of the AndroidManifest.xml file: ```xml
<receiver android:name=".AndroidWidget" android:label="app">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider" android:resource="@xml/airandroidwidget" />
</receiver>
2. You need an XML resource that provides metadata about the widget. Simply create a new file named airandroidwidget.xml in a new res/xml directory with the following contents: ```xml
This tells the widget to use the main layout resource as the initial layout for the widget.</li>
* Create a res/layout/main.xml file that contains a simple text display: ```xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#ffffffff"
>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="hello"
/>
</LinearLayout>
Next, you’ll need to create the AppWidgetProvider class specified in the AndroidManifest.xml file.</li>
* Create a new Java class named AndroidWidget with the following contents: ```java
package com.jamesward;
import android.app.PendingIntent; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProvider; import android.content.Context; import android.content.Intent; import android.widget.RemoteViews; import com.jamesward.MainApp;
public class AndroidWidget extends AppWidgetProvider {
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)
{
final int N = appWidgetIds.length;
// Perform this loop procedure for each App Widget that belongs to this provider
for (int i=0; i<N; i++)
{
int appWidgetId = appWidgetIds[i];
Intent intent = new Intent(context, MainApp.class);
intent.setAction(Intent.ACTION_MAIN);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.main);
views.setOnClickPendingIntent(R.id.widget, pendingIntent);
appWidgetManager.updateAppWidget(appWidgetId, views);
}
}
}
This class will display the Widget when necessary and register a click handler that will open the MainApp application when the user taps on the Widget.</li>
* Run the application to verify that it works.
* Now you can add the widget to the home screen by holding down on the home screen and following the Widget wizard.
* Verify that tapping the widget launches the AIR application.</ol>
**Application Licensing**
Android provides APIs to help you enforce licensing policies for non-free apps in the Android Market. You might want to go [read up on Android Licensing][6] before you give this one a try.
To add Application Licensing to you AIR for Android application you first need to follow the steps outlined in the Android documentation. The broad steps are as follows:
1. Set up an Android Market publisher account
2. Install the Market Licensing Package in the Android SDK
3. Create a new LVL Android Library Project in Eclipse
4. Add a Library reference in the Android project to the LVL Android Library
5. Add the CHECK_LICENSE permission to your Android project’s manifest file: ```xml
<uses-permission android:name="com.android.vending.CHECK_LICENSE" />
After completing these set up steps, you are ready to update the MainApp Java class to handle validating the license:
```java
package com.jamesward;
import com.android.vending.licensing.AESObfuscator; import com.android.vending.licensing.LicenseChecker; import com.android.vending.licensing.LicenseCheckerCallback; import com.android.vending.licensing.ServerManagedPolicy;
import air.Foo.AppEntry; import android.os.Bundle; import android.os.Handler; import android.provider.Settings.Secure;
public class MainApp extends AppEntry {
private static final String BASE64_PUBLIC_KEY = "REPLACE WITH KEY FROM ANDROID MARKET PROFILE";
// Generate your own 20 random bytes, and put them here.
private static final byte[] SALT = new byte[] {
-45, 12, 72, -31, -8, -122, 98, -24, 86, 47, -65, -47, 33, -99, -55, -64, -114, 39, -71, 47
};
private LicenseCheckerCallback mLicenseCheckerCallback;
private LicenseChecker mChecker;
private Handler mHandler;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHandler = new Handler();
String deviceId = Secure.getString(getContentResolver(), Secure.ANDROID_ID);
mLicenseCheckerCallback = new MyLicenseCheckerCallback();
mChecker = new LicenseChecker(
this, new ServerManagedPolicy(this,
new AESObfuscator(SALT, getPackageName(), deviceId)),
BASE64_PUBLIC_KEY);
mChecker.checkAccess(mLicenseCheckerCallback);
}
private void displayFault() {
mHandler.post(new Runnable() {
public void run() {
// Cover the screen with a messaging indicating there was a licensing problem
setContentView(R.layout.main);
}
});
}
private class MyLicenseCheckerCallback implements LicenseCheckerCallback {
public void allow() {
if (isFinishing()) {
// Don't update UI if Activity is finishing.
return;
}
// Should allow user access.
}
public void dontAllow() {
if (isFinishing()) {
// Don't update UI if Activity is finishing.
return;
}
displayFault();
}
public void applicationError(ApplicationErrorCode errorCode) {
if (isFinishing()) {
// Don't update UI if Activity is finishing.
return;
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
mChecker.onDestroy();
}
}
Also add the following to a new res/layout/main.xml file in order to display an error when the license is denied:
```xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/license_problem"
/>
</LinearLayout>
The text to display uses a string resource named “license_problem”, which must be added to the res/values/strings.xml file:
```xml
When the application runs it will check for a valid license. If the license comes back as valid then the AIR application will start and run as usual. However, if there is an invalid license then the application will set the ContentView to the R.layout.main resource, which displays the error message defined in the “license_problem” resource. To simulate different responses you can change the “Test Response” in your Android Market profile.
**The Gory Details**
I’ve wrapped up a generated AppEntry class and its resources to make the process of extending AIR for Android fairly easy. If you are interested in seeing how that is done, I’ve posted all of the [source code on github][7].
Here is an overview of the procedure:
1. Use the AIR SDK to create an AIR for Android APK file.
2. Use the dex2jar utility to convert the AppEntry dex classes into a JAR file.
3. Pull the resource classes out of the JAR file so that they don’t conflict with the new resources.
4. Use apktool to extract the original resources out of the AIR for Android APK file.
5. Create a single ZIP file containing the airbootstap.jar file, resources, AndroidManifest.xml file, and assets.
Now you can simply copy and paste those dependencies into your Android project.
**Conclusion**
Hopefully this article has helped you to better understand how you can extend AIR for Android applications with Android APIs. There are still a number of areas where this method can be improved. For instance, I am currently working with the [Merapi Project][8] developers to get Merapi working with my method of extending AIR for Android. That will provide a better bridging technique for communicating between the AIR application and Android APIs. So stay tuned for more information about that. And let me know if you have any questions!
[1]: https://www.adobe.com/cfusion/tdrc/index.cfm?product=flash_builder
[2]: http://developer.android.com/sdk/index.html
[3]: http://developer.android.com/sdk/eclipse-adt.html
[4]: http://www.jamesward.com/downloads/extending_air_for_android-flex_4_5-air_2_6-v_1.zip
[5]: http://www.youtube.com/watch?v=HjDu66NOpuA
[6]: http://developer.android.com/guide/publishing/licensing.html
[7]: https://github.com/jamesward/extending_air_for_android
[8]: http://code.google.com/p/merapi/