Saturday, June 04, 2011

running embedded Jetty in Android app

I expect that we'll see this more commonly as more Android-based hardware devices come out other than mobile phones. In this particular case, the Jetty server is intended to drive a dynamic Web-based management tool for an application running on an Android device. I need to get a proper Web app project set up that pre-compiles the JSPs before they get to the Android device but this post is intended to show what libraries are required and how to set up Jetty to run on the Android device. I'm using a regular Android emulator out of Eclipse and a G1 running Cyanogen (API 8) for testing at this point.

General Stuff


I had to use Jetty version 7.3.0.v20110203 to avoid XML validation feature that causes troubles on Android 2.2 devices.

I merged JARs from the Jetty download into a single one to add to my app that I called jetty.jar. For example, here's a little shell script I used for my WAR Deployed Web App.


jar xf ../jetty-continuation-*.jar
jar xf ../jetty-http-*.jar
jar xf ../jetty-io-*.jar
jar xf ../jetty-security-*.jar
jar xf ../jetty-server-*.jar
jar xf ../jetty-servlet-*.jar
jar xf ../jetty-util-*.jar
jar xf ../jetty-webapp-*.jar
jar xf ../jetty-xml-*.jar
jar xf ../servlet-api-2.5.jar
jar cf ~/Documents/ECLIPSE_PROJECT_PATH/libs/jetty.jar *


I see that a work-around for the IPv4 / IPv6 issue is still required. I used

// work-around for Android defect 9431
System.setProperty("java.net.preferIPv4Stack", "true");
System.setProperty("java.net.preferIPv6Addresses", "false");


Since the emulator uses slirp for the guest networking, you have to set up port forwarding from the host. For the G1, I used the real address of the phone (shows up in the status bar). So for the emulator I'd point my browser at http://localhost:8080/ but from my G1 I'd point at http://10.1.1.8:8080/ (of couse, this IP is local to my LAN).

adb -e forward tcp:8080 tcp:8080


Simple Web App


Just to make sure that I could start a server and have it listening as expected, I deployed a simple static app.

This is the list of JARs that I had to put into my app.

  • jetty-continuation-7.3.0.v20110203.jar

  • jetty-http-7.3.0.v20110203.jar

  • jetty-io-7.3.0.v20110203.jar

  • jetty-server-7.3.0.v20110203.jar

  • jetty-util-7.3.0.v20110203.jar

  • servlet-api-2.5.jar



Here is the Jetty server clip I put in my app onCreate

webServer = new Server(8080);

Handler handler = new AbstractHandler() {
public void handle(String target, Request request, HttpServletRequest servletRequest,
HttpServletResponse servletResponse) throws IOException, ServletException {
servletResponse.setContentType("text/html");
servletResponse.setStatus(HttpServletResponse.SC_OK);
servletResponse.getWriter().println("<h1>Hello World</h1>");
((Request) request).setHandled(true);
}
};
webServer.setHandler(handler);

try {
webServer.start();
Log.d(LOG_TAG, "started Web server @ " + getPublicInetAddress());

NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
Notification notification = new Notification( R.drawable.web_server_icon, "WebServer", System.currentTimeMillis());
notification.flags |= Notification.FLAG_ONGOING_EVENT; // Notification.FLAG_NO_CLEAR;
Intent notificationIntent = new Intent(this, MyApp.class);
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
notification.setLatestEventInfo(getApplicationContext(), "Web Server" , "Web Server Listening @ " + getPublicInetAddress(), contentIntent);
notificationManager.notify(NOTIFICATION_ID_WEB_SERVER, notification);
}
catch (Exception e) {
Log.d(LOG_TAG, "unexpected exception starting Web server: " + e);
}


WAR Deployed Web App


The next step was to deploy an external servlet via a WAR file. This turned out to be a lot more difficult than I imagined because of checkin r2770 in Jetty-7. It turns out that an XmlParser validation feature was added to Jetty that Android 2.2 doesn't support. Once I moved to a version of Jetty older than that revision I was able to move forward.

This is the list of JARs that I had to put into my app.

  • jetty-continuation-7.3.0.v20110203.jar

  • jetty-http-7.3.0.v20110203.jar

  • jetty-io-7.3.0.v20110203.jar

  • jetty-security-7.3.0.v20110203.jar

  • jetty-server-7.3.0.v20110203.jar

  • jetty-servlet-7.3.0.v20110203.jar

  • jetty-util-7.3.0.v20110203.jar

  • jetty-webapp-7.3.0.v20110203.jar

  • jetty-xml-7.3.0.v20110203.jar

  • servlet-api-2.5.jar



The code:

webServer = new Server(8080);

WebAppContext webapp = new WebAppContext();
webapp.setContextPath("/");
webapp.setTempDirectory(new File("/sdcard/my-work/"));
webapp.setWar("/sdcard/my-webapps/my-ROOT.war");
webServer.setHandler(webapp);

try {
webServer.start();
Log.d(LOG_TAG, "started Web server @ " + getPublicInetAddress());

NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
Notification notification = new Notification( R.drawable.web_server_icon, "WebServer", System.currentTimeMillis());
notification.flags |= Notification.FLAG_ONGOING_EVENT; // Notification.FLAG_NO_CLEAR;
Intent notificationIntent = new Intent(this, MyApp.class);
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
notification.setLatestEventInfo(getApplicationContext(), "Web Server" , "Web Server Listening @ " + getPublicInetAddress(), contentIntent);
notificationManager.notify(NOTIFICATION_ID_WEB_SERVER, notification);
}
catch (Exception e) {
Log.d(LOG_TAG, "unexpected exception starting Web server: " + e);
}


The my-work directory gets created fine. I need to push the WAR file out: for the emulator I used adb push ../my-ROOT.war sdcard/my-webapps

Next Steps



  • I need to submit a patch to Jetty to get a try / catch around the XmlParser validation feature that is causing trouble so we can use more recent versions.

  • I need to set up JSP pre-compiling in my Web project so I can add JSPs to make developing the UI easier.

  • I need to dig into the Android device and figure out how to set the low-port listen capability. Right now, I have to listen on the 1024+ port range so I use the common port 80. For a real device, I need to change this to port 80 but that requires a Linux capability change - probably in device boot parameters.



Here's the change that I think needs to be dis-armed a bit.

$ svn diff -c 2770 XmlParser.java
Index: XmlParser.java
===================================================================
--- XmlParser.java (revision 2769)
+++ XmlParser.java (revision 2770)
@@ -107,6 +107,7 @@
_parser.getXMLReader().setFeature("http://xml.org/sax/features/validation", validating);
_parser.getXMLReader().setFeature("http://xml.org/sax/features/namespaces", true);
_parser.getXMLReader().setFeature("http://xml.org/sax/features/namespace-prefixes", false);
+ _parser.getXMLReader().setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", validating);
}
catch (Exception e)
{

6 comments:

darkh said...

I want to include jetty into my android app but I stubled on a problem with logging.

In my application for logging I use Microlog4android.

When I try run new Server(port) it gives me an exception:
07-14 12:54:41.103: ERROR/AndroidRuntime(12381): Caused by: java.lang.UnsupportedOperationException: debug(String, Object[]) is not implemented yet

Did you have any problems with logging?

Andrew said...

Any progress on this? Did Jetty accept your patch? Could i-jetty be easier to embed?

http://code.google.com/p/i-jetty/

Giorgio said...

Hi!
I've succesfully deployed Jetty on Andrid using your code.
Now I'm trying to add Jersey to Jetty.
Unfortunately I'm getting the following exception: com.sun.jersey.api.container.ContainerException: No WebApplication provider is present

Any idea? THANKS!

Anonymous said...

Following your example i got this exception:

Could not find class 'org.eclipse.jetty.server.Server', referenced from method com.example.jettydemoapp.JettyMainActivity.onCreate
FATAL EXCEPTION: main
java.lang.NoClassDefFoundError: org.eclipse.jetty.server.Server

I've added your jars to libs folder and build path.

gaurav said...

i always get this error

12-12 17:04:20.880: E/AndroidRuntime(16284): FATAL EXCEPTION: main
12-12 17:04:20.880: E/AndroidRuntime(16284): java.lang.NoClassDefFoundError: org.eclipse.jetty.server.Server
12-12 17:04:20.880: E/AndroidRuntime(16284): at com.example.jettyapplicationserver.MainActivity.onCreate(MainActivity.java:33)
12-12 17:04:20.880: E/AndroidRuntime(16284): at android.app.Activity.performCreate(Activity.java:5369)
12-12 17:04:20.880: E/AndroidRuntime(16284): at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1104)
12-12 17:04:20.880: E/AndroidRuntime(16284): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2267)
12-12 17:04:20.880: E/AndroidRuntime(16284): at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2359)
12-12 17:04:20.880: E/AndroidRuntime(16284): at android.app.ActivityThread.access$700(ActivityThread.java:165)
12-12 17:04:20.880: E/AndroidRuntime(16284): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1326)
12-12 17:04:20.880: E/AndroidRuntime(16284): at android.os.Handler.dispatchMessage(Handler.java:99)
12-12 17:04:20.880: E/AndroidRuntime(16284): at android.os.Looper.loop(Looper.java:137)
12-12 17:04:20.880: E/AndroidRuntime(16284): at android.app.ActivityThread.main(ActivityThread.java:5455)
12-12 17:04:20.880: E/AndroidRuntime(16284): at java.lang.reflect.Method.invokeNative(Native Method)
12-12 17:04:20.880: E/AndroidRuntime(16284): at java.lang.reflect.Method.invoke(Method.java:525)
12-12 17:04:20.880: E/AndroidRuntime(16284): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1187)
12-12 17:04:20.880: E/AndroidRuntime(16284): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)
12-12 17:04:20.880: E/AndroidRuntime(16284): at dalvik.system.NativeStart.main(Native Method)

Stu said...

So I got this to work as well, and work it does.
But the one great function it can not support on android is compiling the jsp. Which takes away lots of its usefulness.

Android phones run dalvik, not native java byte code and there's nothing to convert the jsp's java code into dalvik. so jsps just don't work.

But it did get as far as creating the .java file from the .jsp.
Pretty impressive.