Tuesday, November 16, 2010

First Android experiment - SMSSender.py

Tools

This is my first post on this blog and first experiment in scripting for Android. For this script I'll be using  Scripting Layer for Android (SL4A) as the layer , including Python for Android. Found a lot of help on the SL4A API and on the Android Scripting Google Group. One more thing is being used: the OpenIntent File Explorer which is a ... File Explorer, that can be launched via an intent.
The idea here is to create a simple script that sends a series of personalized SMS. The purpose is an actual informative sms for my clients (who happen to be students). For example, I want to let Antony, Bob and Claire know which seat number they have been allocated for their exams, so I wish to sms:
  • Dear Antony, your seat is A1
  • Dear Bob, your seat is M4
  • Dear Claire, your seat is M2
So it's basically a "mail merge" from a CSV into a text, then send the SMSs

    Basic flow

    0. Have a config file that handles a few options including the phone prefix in case the CSV didn't use proper international code e.g. "+49"
    1. Start a file explorer activity to select a CSV file
    2. Parse the CSV file to find the fields of the merge (first row of the csv)
    3. Ask the user which column corresponds to the phone number
    4. Ask the user whether they want to get the template text from a file or type it
    5. Get the template text and validate that it contains all the fields found in CSV (except phone number)
    6. Merge and send each SMS
    7. [for fun] Let Android speak out after it has sent a certain number of SMS and at the end of the sending process

    Files


    Here are the SMSSender.py + ./etc/ directory + SMSSender.conf compiled into a zip file to be extracted into /sdcard/sl4a/scripts. Another zip file contains a sample text and a sample csv that can be uploaded to the Android's sdcard and then picked from the script.
    Regarding the sample csv and text, here are a few notes to ensure that those files will work

    • CSV:
      • File should be a CSV
      • Separator must correspond to the separator found in the config file, or "," if no config file
      • Delimiter must correspond to the delimiter found in the config file, or " if no config file
    • Text:
      • Whether the text is given through a text file or typing on the phone, all fields (except phone number) need to be present in the text inside curly brackets. This is because we are using Python's String.format() to perform the merge . E.g. if the fields are Name and Seat, the text must contain {Name} and {Seat}

    Flow with screenshots


    Use SL4A to start the script

    File Explorer starts, browse to the csv file
    Fields are recognized from first line of csv file
    Choose how to input the message text.

    Choose a text file where the message text resides

    A bit of code explanation

    Since this is intended to be proposed to the SL4A team as a tutorial or at least an example of what the great scripting layer can do, let's discuss the parts of the code that are related to the Android scripting side. The rest of the code is Python programming, maybe it is not necessary to go through all of it.

    Calling the File Explorer via intent

    First time the instance of Android() is used is here (line ~254):
    map = droid.startActivityForResult( "org.openintents.action.PICK_FILE", None, None, {"org.openintents.extra.TITLE":"Choose file to import", "org.openintents.extra.BUTTON_TEXT" :"Choose"})
    filename = map.result["data"].replace( "file://", "" )

    This is the part where we choose the file from the File Explorer. It is based on the JS example by Frank Westlake found on Google Groups. I thought calling an intent would make things difficult but not at all. What happens is that the application corresponding to the intent action: org.openintents.action.PICK_FILE is launched by startActivityForResult and our script waits for that action to be completed, upon which Android() returns an instance of Result. The Result object has several attributes, in particular a "result" one, which is a dictionary, containing the key "data":pathtofilechosen.

    Note that if the "Back" button is pressed, map.result will be None (in Python).
    Also note that the choice of variable name map has nothing to do with the Python map function (will change that variable name in the future).
    A nice list of intents can be found at the OpenIntents website.
    Another note on this, I've recently tried to create a UI for this application (see TODO) and found out that creating a WebView which uses Javascript to start an event in Python, whereby the event starts a File Explorer, and Python then sends the result back to JS works painlessly. Exciting!

    This call of intent is used again in the SMSMerger class when prompting the user for a template text from file and it works the same way.

    Creating a single choice dialog and wait for result

    Another Android scripting part is the few times where we create a dialog using:
    selectedItem = 0
    droid.dialogCreateAlert("Get template") droid.dialogSetPositiveButtonText( "Ok" ) droid.dialogSetSingleChoiceItems(["From file","Type message"], selectedItem)
    droid.dialogShow()
     # Wait for response
    droid.dialogGetResponse()
    selectedItem = droid.dialogGetSelectedItems().result[0]

    This is the way to create a dialog which contains a list of options taken from a python list e.g. ["From file","Type message"]
    droid.dialogShow() shows the actual dialog. droid.dialogGetResponse() is the part that stops the script until a response has been given by the dialog.

    Actual sending SMS

    The next Android part is the use of the SMS facade. But there is not much to say here, it's SO incredibly straightforward with the SL4A layer... droid.smsSend( destination, message )

    Text to speech facade

    Finally, I used the text-to-speech facade - for fun. I'm not sure whether my speakAndWait function is necessary or if there might be an event that recognizes that the tts is done speaking (comments or suggestions on this are very welcome), but last time I tried to make Android speak two sentences without waiting, I would get only the beginning of the first sentence, then silence. This little function starts the ttsSpeak and then lets the python script wait until ttsIsSpeaking becomes false (actually ttsIsSpeaking returns a tuple, whereby the second element is True or False).
    def speakAndWait( self, text ):
     self.droid.ttsSpeak( text )
     while self.droid.ttsIsSpeaking()[1] is True:
      time.sleep( 1 )

    Conclusion

    That's all for my first sample of python scripting in Android. Out of the whole code, 95% is just regular Python. The Android related parts are incredibly straightforward with a really excellent surprise as to how easy it is to call an external action via intent and to get its result. Hope the last version I uploaded doesn't include too many bugs :-/

    TODO

    A lot of things are still to improve, but mainly planning:
    • create a user interface, probably using a WebView
    • test thoroughly ( I admit I haven't tried every possible scenario)
    • handle issues better - avoid exiting too easily
    • use python logger instead of just writing to file

    1 comment:

    1. Sounds cool
      I'm yet to find a mail merge app for android SMS, and I'd love to see one.
      Even the group SMS personalising apps all have their bugs.

      mail merge to SMS from csv on android = halelujah :)

      ReplyDelete