An Android Server

Source code can be found here: github.com/chrishulbert/AndroidService

Summary: To make a persistent Android server, you'll need a Wake Lock, a Wifi Lock, and a Foreground Service.

How to use an Android phone as a [web] server

For a change of pace, I've been experimenting with using cheap Android phones as servers. I've been toying with the idea of getting some Raspberry Pis for various uses, however they're quite expensive in Australia, and cheap android phones go for $20 a pop. So i've been playing with them, to see what I could come up with.

So I wanted an android phone that acts as a web server, getting content from a Samba server. The Samba server in my case is my router, which exposes any USB drive via Samba. I wanted it to serve up via HTTP, so that I could use it to play movies etc from. And i got it to work!

You will likely be reading this article for the information about how to run a long-running Android server mainly, and will likely be less interested in my Samba stuff. So I'll skim over that.

NanoHTTPD

I recommend using NanoHTTPD, it's a simple embeddable Java web server. Simply download its JAR, go to the project view, drag it into MyApp/app/libs, right click it and select the option to include it in your project via gradle.

Next, create a subclass of NanoHTTPD for your server. The only interesting method you need to override is serve. For example, here is some stuff I've been doing with it, but of course yours will vary significantly depending on what you actually want to do:

@Override
public Response serve(IHTTPSession session) {
    Log.d("MyHTTPD", "Serve called...");

    String uri = session.getUri();
    if (uri.equals("/api/foo")) {
        try {
            SmbFile smbFile = new SmbFile("smb://admin:[email protected]/usb1_1/");
            JSONArray list = new JSONArray();
            for (String file: smbFile.list()) {
                list.put(file);
            }
            String json = list.toString(1);
            return newFixedLengthResponse(json);
        } catch (Exception e) {
            return newFixedLengthResponse("<html><body>Error: " + e.toString());
        }
    } else {

        // Browse.
        try {
            SmbFile smbFile = new SmbFile("smb://admin:[email protected]/usb1_1" + uri);
            if (smbFile.isDirectory()) {
                String html = "<html><body>";
                // ListFiles seems to be really slow - does that matter once we're up and running?
                for (SmbFile file : smbFile.listFiles()) {
                    html += "<p><a href='" + file.getName() + "'>" + file.getName() + "</a>";
                    if (file.isDirectory()) {
                        html += "<a href='" + file.getName() + "hls.m3u8'>HLS</a>";
                    }
                    html += "</p>";
                }
                return newFixedLengthResponse(html);

            } else if (smbFile.isFile()) {
                String mime = "application/octet-stream";
                String name = smbFile.getName();
                if (name.endsWith(".m3u8")) {
                    mime = "application/x-mpegURL";
                } else if (name.endsWith(".ts")) {
                    mime = "video/MP2T";
                }
                Log.d("MyMedia", "serving: " + name + "; mime: " + mime);
                return newChunkedResponse(Response.Status.OK, mime, smbFile.getInputStream());
            } else {
                return newFixedLengthResponse("<html><body>Not a file or dir, uri: " + uri);
            }
        } catch (Exception e) {
            return newFixedLengthResponse("<html><body>Error: " + e.toString());
        }
    }
}

You've now got your NanoHTTPD server created. It's not android-specific code at all at this stage, next we need to make it work as an Android service.

Service

Next make a subclass of android.app.Service, to fit into Android's ecosystem. This is responsible for the following:

  • Starting the NanoHTTPD service
  • Keeping the CPU awake
  • Keeping the Wifi awake
  • Registering as a 'foreground service' so that Android doesn't simply shut us down after a while.

It looks like this:

public class MyHttpService extends Service {

    public Context context = this;
    public Handler handler = null;
    public static Runnable runnable = null;
    PowerManager powerManager;
    PowerManager.WakeLock wakeLock;
    WifiManager.WifiLock wifiLock;

    private MyHTTPD httpd;

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // Start the httpd.
        try {
            httpd = new MyHTTPD();
            httpd.start();
        } catch (IOException e) {
            e.printStackTrace();
            Toast.makeText(this, "Service failed to start.", Toast.LENGTH_LONG).show();
        }

        // Keep the CPU awake (but not the screen).
        powerManager = (PowerManager)getSystemService(Context.POWER_SERVICE);
        wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Httpd");
        wakeLock.acquire();

        // Keep the WIFI turned on.
        WifiManager wm = (WifiManager)context.getSystemService(Context.WIFI_SERVICE);
        wifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Httpd");
        wifiLock.acquire();

        // Become a foreground service:
        // http://developer.android.com/guide/components/services.html#Foreground
        // https://android.googlesource.com/platform/development/+/master/samples/ApiDemos/src/com/example/android/apis/app/ForegroundService.java
        PendingIntent contentIntent = PendingIntent.getActivity(this, 0, new Intent(this, DashboardActivity.class), 0);
        // Set the info for the views that show in the notification panel.
        Notification notification = new Notification.Builder(this)
                .setSmallIcon(R.drawable.ic_sync_black_24dp)  // the status icon
                .setTicker("My service")  // the status text
                .setWhen(System.currentTimeMillis())  // the time stamp
                .setContentTitle("Http")  // the label
                .setContentText("My service")  // the contents of the entry
                .setContentIntent(contentIntent)  // The intent to send when clicked
                .build();
        startForeground(1, notification);

        return Service.START_STICKY;
    }

    @Override
    public void onDestroy() {
        stopForeground(true);
        wakeLock.release();
        wifiLock.release();
        httpd.stop();
    }

}

Potential ARP issues

I've had issues with my server dropping off the network intermittently then coming back hours later. I've been advised this is a bug in the Android kernel not performing a proper ARP advertisement, and have been told to either try a different device, or a newer Android version, or a static IP address, or anything else. You may want to watch out for this. In the end this problem defeated my plans for an Android server but hopefully your mileage may vary.

Main activity

You'll need an activity for your main UI. (iOS friends: an activity is a view controller). In your activity, add a button, and give it a click handler in the layout xml like so: android:onClick="onStartHttpServer".

In your activity class, you'll need to implement the aforementioned method to start our web service. It'll look like so:

public void onStartHttpServer(View v) {
    Intent intent = new Intent(this, MyHttpService.class);
    startService(intent);
}

Thanks for reading! And again, you can find the source here: Source code can be found here: github.com/chrishulbert/AndroidService . Hope you found this helpful, or at least mildly interesting in the case of my typical iOS readers!

Thanks for reading! And if you want to get in touch, I'd love to hear from you: chris.hulbert at gmail.