Tuesday, January 28, 2014

Building Android apps with Python and Shotgun

Happy new year!!

Hi, a lot of work starting this 2014, but let's start with the first post of the year.

Our producers are always in the rush, calling clients, delivering projects, etc and all the time they are asking us for tools to manage projects on their smart-phones, so we think that some apps could also be useful for the artists when simple functionality is needed. So we did some research and found that is really easy to prototype Python apps in Android, and this post is for this...

Building Android apps with Python and Shotgun


For me, Android is the most flexible and configurable OS for smart-phones, and also has its own scripting layer thanks to the SL4A project that exposes the Android API to several languages like Python, Perl, JavaScript, and others.

For us, the important one is Python, this because the official Shotgun API is in this language.

So, let's start...

Firs of all you will need to download and install two apk's on your phone, one is the scripting layer for android and the second its the python interpreter, the installation order doesn't matter.

Logo
Scripting Layer for Android (SL4A)
http://android-scripting.googlecode.com/files/sl4a_r6.apk



Python Interpreter for Android (Py4A)
http://python-for-android.googlecode.com/files/PythonForAndroid_r6.apk

If you've never installed apk's before you maybe need to set your phone to allow the installation of non market apps (Google Play), this could be different in all the Android versions and distros of all the phones but commonly is like this:
  1. Go to Settings.
  2. Open Security.
  3. Click Unknown sources.
Once you have installed the apps, it's time to install the interpreter itself, this will be performed by the Python for android app, open it and select install. This will download all the Python modules and setup the interpreter.

When this is done, now you can open the SL4A app and see some sample python scripts that uses the Android Python API, very simple to use, here is an example:


import android

droid = android.Android()
name = droid.getInput("Hello!", "What is your name?")
print name  # name is a namedtuple
droid.makeToast("Hello, %s" % name.result)


All this scripts are located in the following path: /sdcard/sl4a/scripts
Here you can create new ones, and organize them in folders if you want, check the SL4A User Guide

To learn about the commands and all the functionality you can access from the API, the best places are the docs, and also all the examples they have.

Now, we need the Shotgun API to perform what we want, this is really easy, only put the shotgun_api3 folder in the following path: /sdcard/com.googlecode.pythonforandroid/extras/python/

By doing this, you can perform "from shotgun_api3 import Shotgun"

At this point you can create scripts that works like command line or auto scripts but, what if we want an app with a graphical user interface?

Well, Android API has it's own way to do this by XML layouts, and is really easy to use. 
Follow this link to check how to use it.


In order to make this more simple, I use Android Studio, where I design the layout in a graphic mode like Qt-Designer, dragging widgets and adjusting properties, then I copied the formatted xml and saved as an xml file to load it into my code as a string:


layout = open("layout.xml","r").read()


And that's it, you can start to build python apps for Android that can interact with Shotgun, let me show you an app to Send Support Tickets:

This example is made of three files: The main python script, the xml layout and a banner image





The banner image:



Here the XML Layout:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#b0c6c3">

    <LinearLayout
            android:orientation="vertical"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:layout_alignParentTop="true"
            android:layout_alignParentLeft="true"
            android:id="@+id/linearLayout">

        <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:id="@+id/image_banner"
                android:layout_marginTop="13dp"
                android:adjustViewBounds="true"
                android:src=""
                android:layout_alignParentTop="true"
                android:layout_centerHorizontal="true"/>

        <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Write your request:"
                android:id="@+id/label_solicitud"
                android:layout_marginTop="10dp"
                android:textColor="#010101"
                android:textSize="14dp"/>

        <EditText
                android:layout_width="fill_parent"
                android:layout_height="200dp"
                android:id="@+id/ticket_body"
                android:layout_gravity="center"
                android:layout_marginTop="5dp"
                android:linksClickable="false"
                android:visibility="visible"
                android:background="#d9f0ed"
                android:gravity="top"
                android:autoText="false"
                android:editable="false"/>

        <CheckBox
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Check as urgent!"
                android:id="@+id/urgent_check"
                android:layout_gravity="left|center_vertical"
                android:layout_marginTop="10dp"
                android:checked="false"
                android:enabled="true"
                android:focusable="false"
                android:textColor="#010101"
                android:textStyle="bold"/>

        <LinearLayout
                android:layout_width="fill_parent"
                android:layout_height="fill_parent">

            <Button
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="Send Ticket"
                    android:id="@+id/sent_ticket"
                    android:layout_marginTop="10dp"/>

            <Button
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="Close App"
                    android:id="@+id/close_app"
                    android:layout_marginTop="10dp"/>
        </LinearLayout>
    </LinearLayout>
    <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Status Bar: "
            android:id="@+id/status_bar"
            android:layout_marginTop="5dp"
            android:layout_alignParentBottom="true"
            android:textSize="10dp"
            android:textColor="#430817"
            android:textStyle="italic"/>
</RelativeLayout>

Here the main python script:


from shotgun_api3 import Shotgun
import android
import os
from time import sleep

droid=android.Android()
sg = Shotgun("Your Shotgun Site","Your API name","Your API code")

def eventloop():
  global banner_file
  droid.fullSetProperty("status_bar",
     "text",
     "Status Bar: Waiting ticket description...")
  droid.fullSetProperty("image_banner","src",banner_file)
  droid.addOptionsMenuItem("About","about",None,"ic_dialog_info")
  while True:
    event=droid.eventWait().result
    print event
    if event["name"]=="click":
      id=event["data"]["id"]
      if id=="close_app":
        return
      elif id=="sent_ticket":
        droid.fullSetProperty("status_bar",
           "text",
           "Status Bar: Sending Ticket... Please Wait...")
        ticket_body = droid.fullQueryDetail("ticket_body").result
        ticket_body = ticket_body["text"]

        urgent = droid.fullQueryDetail("urgent_check").result
        urgent = urgent["checked"]

        if urgent == "false":
          ticket_status = "2"
        else:
          ticket_status = "1 (High)"

        data = {
          "title": "Support ticket",
          "project": {'id': 123, 'name': 'Support', 'type': 'Project'},
          "description": ticket_body,
          "sg_priority": ticket_status
        }

        # Let the user know we're doing something
        droid.dialogCreateSpinnerProgress('Please Wait...', 
            'Creating Ticket in Shotgun', 
            100, 
            False)
        droid.dialogShow()

        Ticket = sg.create("Ticket", data)

        droid.dialogDismiss()

        droid.dialogCreateAlert("Ticket succesfully created!")
        droid.dialogSetPositiveButtonText("Ok")
        droid.dialogShow()

        droid.fullSetProperty("status_bar",
           "text",
           "Status Bar: The ticket was created...")
        droid.fullSetProperty("ticket_body","text","")
        sleep(2)
        droid.fullSetProperty("status_bar",
           "text",
           "Status Bar: Waiting ticket description...")

    elif event["name"]=="screen":
      if event["data"]=="destroy":
        return

    elif event["name"]=="about":
      droid.dialogCreateAlert("Autor: Hasiel Alvarez", 
          "Mail: hasielhassan@gmail.com")
      droid.dialogSetPositiveButtonText("Ok")
      droid.dialogShow()

dirname, filename = os.path.split(os.path.abspath(__file__))
xml_file = dirname + os.sep + "layouts" + os.sep + "activity_main.xml"
layout = open(xml_file,"r").read()

banner_file = str(dirname + os.sep + "images" + os.sep + "help_banner.png")
banner_file = "file:" + os.sep + os.sep + banner_file[4:] 

droid.fullShow(layout)
eventloop()
droid.fullQuery()
droid.fullDismiss()

From here, there are a lot more things to review:
By using this approach to build mobile apps for Shotgun you are delivering the source code to the users including the api access to Shotgun and this won't be acceptable for most studios.
Also it's difficult to setup the first time, and not all the users can do it.
And maybe the most important one, this approach works for Android and most (if not all) of the producers use iPhones, and for that, currently I haven't found an easy way to script apps in Python on iOS.

But it's good to know and an interesting and really quickly way to prototype apps you need and make changes really easy, so the users can give you feedback and get all the info needed to build the final app, maybe with Phonegap or Kivy.