Take a look at “SANEDroid”

12 Jun

https://play.google.com/store/apps/details?id=com.sane.droid

I finally got around to publishing this, please enjoy.


Get it on Google Play

2D barcodes put to good use… The Automated RSVP

1 Feb

Just over a year ago my wife and I were married, and whilst my contribution to the well organised and executed event was insignificant in contrast to my wife’s efforts, I feel compelled to write a little about what I got up to in preparation for the big day.

In this modern age it is quite easy for one to make it through day to day life without having to use more traditional services such as post mail to communicate with others from afar. Email, Skype, and other Telecommunications have swamp my generation and so knowledge such as the whereabouts of the nearest post box to where I live is something that I do not care nor have the requirement to commit to memory. So whenever I receive an invitation to a wedding with accompanying RSVP card, even if the card has a postage paid stamp, it rarely makes the return voyage (with an email or phone call generally confirming my RSVP). So this got me thinking and since my wife had already compiled a “wedsite” to provide our guests with information pertaining to our wedding, I thought I could leverage the site and take it one step further making it a little easier for our guests travelling from afar to RSVP.

My thought was this… wouldn’t it be nice if our guests could simply RSVP over the Internet, and furthermore, wouldn’t it be cool if they could use their smart phone to do so. So I whipped up some software that saw the encoding of each of our guests names into their own unique RSVP cards using 2D barcode technology (using something similar to this https://github.com/mubeta06/python/tree/master/libqrencode_ctypes_bindings). In actual fact I encoded a url with the guest’s names forming the query string of the url, the idea being that our guests would scan the barcode on their smart phone and open the link on the browser on their phone. From there it was a piece of cake to have a little CGI script running on our wedsite that would then prepare the RSVP page dynamically prompting the guests for their RSVP and any dietary requirements. For a little extra polish I even added a little javascript sugar that gave our guests the option of being emailed an iCalendar event that they could post to the calendar of their smart phone containing all the relevant details.

Here is an example RSVP card.
RSVP Card

Please note the domain name has long since expired, but here is some example screen shots of the CGI pages that were being served up.
Dynamically generated RSVP page

RSVP Confirmation

Who said 2D barcodes were useless… ?

Android Webserver in 10 minutes

29 Jan

Want to know how to turn your Android device into a web server without compiling a single line of code and, moreover, without the burden of the Android SDK? Well as it turns out provided you have Scripting Layer for Android (SL4A) installed on your device along with python interpreter, python for android (PY4A) you can utilise built-in BaseHTTPServer to serve up pages on your very own pocket web server. Incorporate a dynamic DNS service such as dyndns via their official play store app and Bob’s your father’s brother. Furthermore, to make things a little more interesting with some very minor modifications to CGIHTTPServer.CGIHTTPRequestHandler you can provide your server with a custom handler and make it CGI functional. The latter provides access to device API’s through the SL4A facade via script invocation over the Internet.

The only remotely tricky part of all this is the aforementioned modifications required to provide a custom Android CGI HTTP Handler. The default CGIHTTPRequestHandler implementation attempts to fork a new python process to deal with each CGI request, which whilst is fine on a regular operating system, on the Android OS such subprocessing is not permitted. So to circumvent the situation one can modify the CGIHTTPRequestHandler to execute the CGI script using the same python process running the web server and handler.

I have posted my implementation of this customisation below. The most significant changes begin around line 172 with line 176 providing the magic to import the CGI script runtime. So if you add the below python script to your sl4a scripts directory and create a /sl4a/cgi-bin directory to house your CGI scripts you can start the script up as a service and you are all set.

#!/usr/bin/python
# coding: utf-8 
# /sdcard/sl4a/scripts/webserver.py

import os
import select
import sys
import urllib

import BaseHTTPServer
import CGIHTTPServer


class DroidCGIHandler(CGIHTTPServer.CGIHTTPRequestHandler):

    """Subclassing of CGIHTTPRequestHandler specifically for the Android
    scripting environment to support limited permissions regarding launching
    cgi subprocess. This Handler simply imports the module using the 
    interpretter instance running the webserver.
    """

    def run_cgi(self):
        """Execute a CGI script."""
        path = self.path
        dir, rest = self.cgi_info

        i = path.find('/', len(dir) + 1)
        while i >= 0:
            nextdir = path[:i]
            nextrest = path[i+1:]

            scriptdir = self.translate_path(nextdir)
            if os.path.isdir(scriptdir):
                dir, rest = nextdir, nextrest
                i = path.find('/', len(dir) + 1)
            else:
                break

        # find an explicit query string, if present.
        i = rest.rfind('?')
        if i >= 0:
            rest, query = rest[:i], rest[i+1:]
        else:
            query = ''

        # dissect the part after the directory name into a script name &
        # a possible additional path, to be stored in PATH_INFO.
        i = rest.find('/')
        if i >= 0:
            script, rest = rest[:i], rest[i:]
        else:
            script, rest = rest, ''

        scriptname = dir + '/' + script
        scriptfile = self.translate_path(scriptname)
        if not os.path.exists(scriptfile):
            self.send_error(404, "No such CGI script (%r)" % scriptname)
            return
        if not os.path.isfile(scriptfile):
            self.send_error(403, "CGI script is not a plain file (%r)" %
                            scriptname)
            return
        ispy = self.is_python(scriptname)
        if not ispy:
            if not (self.have_fork or self.have_popen2 or self.have_popen3):
                self.send_error(403, "CGI script is not a Python script (%r)" %
                                scriptname)
                return
            if not self.is_executable(scriptfile):
                self.send_error(403, "CGI script is not executable (%r)" %
                                scriptname)
                return

        # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html
        # XXX Much of the following could be prepared ahead of time!
        env = {}
        env['SERVER_SOFTWARE'] = self.version_string()
        env['SERVER_NAME'] = self.server.server_name
        env['GATEWAY_INTERFACE'] = 'CGI/1.1'
        env['SERVER_PROTOCOL'] = self.protocol_version
        env['SERVER_PORT'] = str(self.server.server_port)
        env['REQUEST_METHOD'] = self.command
        uqrest = urllib.unquote(rest)
        env['PATH_INFO'] = uqrest
        env['PATH_TRANSLATED'] = self.translate_path(uqrest)
        env['SCRIPT_NAME'] = scriptname
        if query:
            env['QUERY_STRING'] = query
        host = self.address_string()
        if host != self.client_address[0]:
            env['REMOTE_HOST'] = host
        env['REMOTE_ADDR'] = self.client_address[0]
        authorization = self.headers.getheader("authorization")
        if authorization:
            authorization = authorization.split()
            if len(authorization) == 2:
                import base64, binascii
                env['AUTH_TYPE'] = authorization[0]
                if authorization[0].lower() == "basic":
                    try:
                        authorization = base64.decodestring(authorization[1])
                    except binascii.Error:
                        pass
                    else:
                        authorization = authorization.split(':')
                        if len(authorization) == 2:
                            env['REMOTE_USER'] = authorization[0]
        # XXX REMOTE_IDENT
        if self.headers.typeheader is None:
            env['CONTENT_TYPE'] = self.headers.type
        else:
            env['CONTENT_TYPE'] = self.headers.typeheader
        length = self.headers.getheader('content-length')
        if length:
            env['CONTENT_LENGTH'] = length
        referer = self.headers.getheader('referer')
        if referer:
            env['HTTP_REFERER'] = referer
        accept = []
        for line in self.headers.getallmatchingheaders('accept'):
            if line[:1] in "\t\n\r ":
                accept.append(line.strip())
            else:
                accept = accept + line[7:].split(',')
        env['HTTP_ACCEPT'] = ','.join(accept)
        ua = self.headers.getheader('user-agent')
        if ua:
            env['HTTP_USER_AGENT'] = ua
        co = filter(None, self.headers.getheaders('cookie'))
        if co:
            env['HTTP_COOKIE'] = ', '.join(co)
        # XXX Other HTTP_* headers
        # Since we're setting the env in the parent, provide empty
        # values to override previously set values
        for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH',
                  'HTTP_USER_AGENT', 'HTTP_COOKIE', 'HTTP_REFERER'):
            env.setdefault(k, "")
        os.environ.update(env)

        self.send_response(200, "Script output follows")

        decoded_query = query.replace('+', ' ')

        if self.have_fork:
            # Unix -- fork as we should
            args = [script]
            if '=' not in decoded_query:
                args.append(decoded_query)
            nobody = CGIHTTPServer.nobody_uid()
            self.wfile.flush() # Always flush before forking
            pid = os.fork()
            if pid != 0:
                # Parent
                pid, sts = os.waitpid(pid, 0)
                # throw away additional data [see bug #427345]
                while select.select([self.rfile], [], [], 0)[0]:
                    if not self.rfile.read(1):
                        break
                if sts:
                    self.log_error("CGI script exit status %#x", sts)
                return
            # Child
            try:
                try:
                    os.setuid(nobody)
                except os.error:
                    pass
                os.dup2(self.rfile.fileno(), 0)
                os.dup2(self.wfile.fileno(), 1)
                #hack to import python scripts due to permission restrictions
                #regarding launching another interpreter instance                                               
                root, ext = os.path.splitext(scriptfile)
                if ext == '.py':
                    root, script = os.path.split(root)
                    sys.path.append(root)
                    __import__(script)
                    os._exit(0)
                else:
                    os.execve(scriptfile, args, os.environ)
            except:
                self.server.handle_error(self.request, self.client_address)
                os._exit(127)

ADDRESS = ''
PORT = 8080

handler = DroidCGIHandler
handler.cgi_directories = ['/cgi-bin']
httpd = BaseHTTPServer.HTTPServer((ADDRESS, PORT), handler)
print "serving at port", PORT
httpd.serve_forever()

For a bit of fun I threw the following little CGI script together. It serves up a page which indicates the device’s last know position on a google map mark up.

#!/usr/bin/python
# coding: utf-8 
# /sdcard/sl4a/cgi-bin/droidcgi.py

import cgi
import datetime

import android


form = cgi.FieldStorage()
print 'Content-Type: text/html\n'

print """<html>
<head>
<title>Mat's Droid</title>
</head>"""

for field in form:
    print form.getvalue(field)

droid = android.Android()
location = droid.getLastKnownLocation().result
print 'This page is being served from my Droid. My last known position is'
print 'Latitude %f, Longitude %f' % (location['network']['latitude'],
                                        location['network']['longitude'])
time = datetime.datetime.fromtimestamp(int(str(location['network']['time'])[:10]))
print 'Last Updated %s' % time.isoformat(' ')
print '<br>'
print '<img src="http://maps.googleapis.com/maps/api/staticmap?center=%f,%f&zoom=17&size=400x400&maptype=hybrid&markers=color:blue%%7Clabel:A%%7C%f,%f&sensor=true"/>' % (location['network']['latitude'],
                                        location['network']['longitude'],
                                        location['network']['latitude'],
                                        location['network']['longitude'])
print '</html>'

Check it out.

My Droid Webserver

My Droid Webserver

Got ya: If your service provider proxies your Internet connection you will likely not be able to connect from the Internet to your device. And if you want it to work at home via wireless simply setup your router to forward 8080 traffic onto your device.

Enjoy!