Opening files in external apps from AIR on Android is not as trivial as it would seem.
Here’s the problem: AIRs mobile profile does not support File.openWithDefaultApplication(). I know, this is retarded.
I tried countless approaches to this seemingly trivial problem, including using StageWebView, server-side conversion of the PDFs to SWF together with SWFLoader, and various combinations of these. Nothing would work to my satisfaction.
Eventually I came across a method of extending an AIR app with native Android OS functionality. That way it is possible to open files with Android’s native Intent class. Here’s the steps I took to accomplish this:
Please be aware that I am not very familiar with Android development (this is part of the reason why I choose AIR in the first place, doh), so there may be better ways to accomplish this. However, the steps below worked for me.
Preparations
- Set up Eclipse with Android SDK: http://www.vogella.de/articles/Android/article.html#installation_eclipse
- Set up a project in Eclipse: http://www.jamesward.com/2011/05/11/extending-air-for-android/ (Steps 1–10)
- In AndroidManifest.xml, Add at least the following permissions:
android.permission.INTERNET android.permission.WRITE_EXTERNAL_STORAGE
Code
If you followed James Ward’s terrific guide (many thanks!) up until to step 10 you should now have a MainApp.java file in your project. Put this inside it: (remember to change package to whatever you use)
package com.mypackage;
import air.app.AppEntry;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import android.os.Looper;
import android.util.Log;
public class MainApp extends AppEntry {
private boolean stopped=false;
private Thread serverThread;
private ServerSocket ss;
@Override
public void onCreate(Bundle arg0) {
super.onCreate(arg0);
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)
{
System.out.println("test test");
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)
{
// Open PDF
case 1:
String path = _in.readLine();
// Remove junk in the beginning of the string. Not sure if this is necessary in every case.
path = path.substring(2);
// Contruct the Intent
File file = new File(path);
Intent i = new Intent(Intent.ACTION_VIEW);
i.setDataAndType(Uri.fromFile(file), "application/pdf");
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
// Try to start the intent activity. If no activity is found, tell the user via an alert.
try {
startActivity(i);
}
catch (ActivityNotFoundException e) {
messageHandler.sendMessage(Message.obtain(messageHandler, 0));
}
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();
}
@Override
public void onDestroy() {
stopped=true;
try {
ss.close();
} catch (IOException e) {}
serverThread.interrupt();
try {
serverThread.join();
} catch (InterruptedException e) {}
}
// This is a handler for displaying alerts
private Handler messageHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch(msg.what) {
case 0:
AlertDialog alertDialog = new AlertDialog.Builder(MainApp.this).create();
alertDialog.setTitle("No PDF viewer found");
alertDialog.setMessage("Please install one from the Market.");
alertDialog.setButton("OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
return;
} });
alertDialog.show();
break;
}
}
};
}
This code will recieve a path from AIR via the socket, and open the file with the preferred application using the Intent class. If a capable app is not installed on the device, an alert will inform the user.
Ok, so that’s the Android part. Now for the AIR part.
Add this code to your AIR app, where appropriate:
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("/sdcard/MyApp/pdfdocument.pdf");
(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 ');
});
And that’s it! Took me two freaking days to figure this stuff out.
I hope this post will save someone else from wasting a bunch of time.