Building Android Client Applications

I've had the privilege of spending this summer down in San Francisco working as an intern at Cloudkick (side note: if you like the cloud and you're interested in working with an incredible team on some sweet projects, check out our jobs page) working on all sorts of cool projects.

In any case, one of the projects I've been working on this summer is an Android client for Cloudkick. Earlier today I pushed version 0.2.1 (the first public release of the 0.2.x series, after I got cold feet on 0.2.0) of the application to the Android Market, and to celebrate all the lessons I've learned, I wanted to share a few of them. In this case, I'm going to give an overview of how to create an Android client for an existing web service. This won't be a full tutorial (indeed, it will assume that you already know how to create a functional Android application of some sort), but if you're interested the source to Cloudkick for Android is available on GitHub.

Disclaimer

  1. I'm not an experienced Java developer, and I'm certainly not an Android expert, so if it looks like I'm doing something wrong, I probably am. I'd love to know about it though, so if you're feeling nice, drop me a comment.
  2. A lot of this code is taken out of context, so if you want it to actually work, just get the original, functional source code.

Step 1: API Client Library --------------------------

The first thing you'll need is an API client library of some sort. I started out with something like this.

public class CloudkickAPI {  
    private static final String TAG = "CloudkickAPI";
    private static String API_HOST = "api.cloudkick.com";
    private static String API_VERSION = "1.0";
    private final String key;
    private final String secret;
    private final HttpClient client;
    private SharedPreferences prefs = null;

    public CloudkickAPI(Context context) throws EmptyCredentialsException {
        prefs = PreferenceManager.getDefaultSharedPreferences(context);
        key = prefs.getString("editKey", "");
        secret = prefs.getString("editSecret", "");
        if (key == "" || secret == "") {
                throw new EmptyCredentialsException();
        }

        HttpParams params = new BasicHttpParams();
        HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
        HttpProtocolParams.setContentCharset(params, HTTP.DEFAULT_CONTENT_CHARSET);
        HttpProtocolParams.setUseExpectContinue(params, true);

        SchemeRegistry registry = new SchemeRegistry();
        registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
        registry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));

        ClientConnectionManager connman = new ThreadSafeClientConnManager(params, registry);
        client = new DefaultHttpClient(connman, params);
    }
}

There are a few things going on here:

  1. The TAG string is useful for logging (I saw this suggested on StackOverflow, I'm not sure if its standard practice or what)
  2. The Cloudkick API requires OAuth authentication, so I retrieve an OAuth key and secret from the preferences storage, which we haven't set up yet. So unless you know how to do that yourself, this isn't going to actually work just yet.
  3. If the credentials are empty, throw a custom exception. This is handy for redirecting the user to the login screen if they haven't attempted a login.
  4. The HTTP Client setup is somewhat more complicated than I would like, but I needed it to support multi-threading. I'm still not positive I got all of the necessary settings, but it seems to work for me.

Right, so this obviously doesn't actually do anything so far, so lets go ahead and make that happen:

private String doRequest(String path) throws BadCredentialsException, OAuthException, IOException {  
    HttpResponse response = null;
    StringBuilder body = new StringBuilder();
    try {
        HttpGet request = new HttpGet("https://" + API_HOST + "/" + API_VERSION + path);
        OAuthConsumer consumer = new CommonsHttpOAuthConsumer(key, secret);
        consumer.sign(request);
        response = client.execute(request);
        if (response.getStatusLine().getStatusCode() == 401) {
            response.getEntity().consumeContent();
            throw new BadCredentialsException();
        }
        InputStream is = response.getEntity().getContent();
        BufferedReader rd = new BufferedReader(new InputStreamReader(is));
        String line;
        while ((line = rd.readLine()) != null) {
            body.append(line);
        }
    }
    finally {
        if (response != null && response.getEntity().isStreaming()) {
            response.getEntity().consumeContent();
        }
    }
    return body.toString();
}

public ArrayList<Node> getNodes() throws BadCredentialsException, OAuthException, IOException, JSONException {  
    String body = doRequest("/query/nodes");
    ArrayList<Node> nodes = new ArrayList<Node>();
    JSONArray rawNodes = new JSONArray(body);
    int rawCount = rawNodes.length();
    for (int i = 0; i < rawCount; i++) {
        nodes.add(new Node(rawNodes.getJSONObject(i)));
    }
    Log.i(TAG, "Retrieved " + nodes.size() + " Nodes");
    return nodes;
}

So, notable things here:

  1. It is important to consume all content off of the HTTP response, even if an error occurs, otherwise the connection won't be freed up and you will eventually run out of free connections in your pool.
  2. We have another custom exception which gets thrown if a 401 status code is returned, indicating that the user's credentials were invalid (as opposed to empty).
  3. We throw everything. You don't really need to list these out, but it comes in handy later when you want to know what exceptions to look for.
  4. I'm constructing custom Node objects from the returned JSON list. The details aren't really important, but a Node is basically a constructor that fills in a bunch of 'final' fields.

Obviously you'll want more API methods than this, but you get the idea. Each method just calls doRequest() then constructs an object or list of objects to be returned.

Step 2: An Activity --------------

To actually display the data, you'll need an activity. A lot of web API clients spend a lot of time displaying and drilling down lists, so I'm going to use one of those as an example.

public class DashboardActivity extends Activity implements OnItemClickListener {  
    private static final String TAG = "DashboardActivity";
    private static final int SETTINGS_ACTIVITY_ID = 0;
    private static final int LOGIN_ACTIVITY_ID = 1;
    private static final int refreshRate = 30;
    private CloudkickAPI api;
    private ProgressDialog progress;
    private ListView dashboard;
    private NodesAdapter adapter;
    private boolean haveNodes = false;
    private boolean isRunning = false;
    private final ArrayList<Node> nodes = new ArrayList<Node>();
    private final Handler reloadHandler = new Handler();

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
        dashboard = new ListView(this);
        adapter = new NodesAdapter(this, R.layout.node_item, nodes);
        dashboard.setAdapter(adapter);
        dashboard.setOnItemClickListener(this);
        dashboard.setBackgroundColor(Color.WHITE);
        setContentView(dashboard);
        reloadAPI();
    }

    private void reloadAPI() {
        try {
            api = new CloudkickAPI(this);
            haveNodes = false;
        }
        catch (EmptyCredentialsException e) {
            Log.i(TAG, "Empty Credentials, forcing login");
            Intent loginActivity = new Intent(getBaseContext(), LoginActivity.class);
            startActivityForResult(loginActivity, LOGIN_ACTIVITY_ID);
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == SETTINGS_ACTIVITY_ID) {
            reloadAPI();
        }
        if (requestCode == LOGIN_ACTIVITY_ID) {
            try {
                if (data.getBooleanExtra("login", false)) {
                    reloadAPI();
                }
                else {
                    finish();
                }
            }
            catch (NullPointerException e) {
                finish();
            }
        }
    }
}

This is pretty straightforward, when the Activity is created it sets up the list view which uses a subclassed ArrayAdapter called NodesAdapter (more on that momentarily). Then, it calls reloadAPI() which attempts to instantiate a CloudkickAPI. However if an EmptyCredentialsException is thrown it starts up a LoginActivity. To be honest, the way the LoginActivity in Cloudkick for Android works is probably very dissimilar from anything most people will need (the API it uses was designed to be easily parsed from a C client, the login process requires multiple requests with possible user input between them, etc), but the concept with this is fairly simple: get and validate some sort of credentials from the user, and store the necessary tokens for use by the API client. Then return from the LoginActivity, setting a boolean to indicate that the user actually logged in (as opposed to using the back button to exit the application).

This last bit might seem a little odd, but the idea is that when the user opens the application for the first time and sees the login screen, you (obviously) want them to be able to exit if they don't feel like logging in just then. If you were to simply call reloadAPI() every time the LoginActivity returned, if the user just pressed the back button they would be continuously bounced back to the LoginActivity until they filled in some sort of credentials. So we detect that case by the absence of the "login" boolean in the returned Intent and simply exit if they didn't attempt to login. This is another one of those things that I'm sure there is a better way to handle, but I haven't put much thought into figuring out what it is.

Now ListViews aren't much fun unless you populate them, so lets look at how to do that. Inside of the DashboardActivity we add some methods:

@Override
protected void onResume() {  
    super.onResume();
    isRunning = true;
    reloadService.run();
    Log.i(TAG, "Reloading service started");
}

@Override
protected void onPause() {  
    super.onPause();
    isRunning = false;
    if (progress != null) {
        progress.dismiss();
        progress = null;
    }
    reloadHandler.removeCallbacks(reloadService);
    Log.i(TAG, "Reloading callbacks canceled");
}

private void refreshNodes() {  
    if (api != null) {
        if (!haveNodes) {
            progress = ProgressDialog.show(this, "", "Loading Nodes...", true);
        }
        new NodeUpdater().execute();
    }
}

private class NodeUpdater extends AsyncTask<Void, Void, ArrayList<Node>> {  
    private Exception e = null;

    @Override
    protected ArrayList<Node> doInBackground(Void...voids) {
        try {
            return api.getNodes();
        }
        catch (Exception e) {
            this.e = e;
            return null;
        }
    }

    @Override
    protected void onPostExecute(ArrayList<Node> retrieved_nodes) {
        // Get rid of the progress dialog either way
        if (progress != null) {
            progress.dismiss();
            progress = null;
        }
        // Handle errors
        if (e != null) {
            if (e instanceof InvalidCredentialsException) {
                Toast.makeText(DashboardActivity.this.getApplicationContext(), "Invalid Credentials", Toast.LENGTH_SHORT).show();
                Intent settingsActivity = new Intent(getBaseContext(), Preferences.class);
                startActivityForResult(settingsActivity, SETTINGS_ACTIVITY_ID);
            }
            else if (e instanceof IOException) {
                Toast.makeText(DashboardActivity.this.getApplicationContext(), "A Network Error Occurred", Toast.LENGTH_SHORT).show();
            }
            else {
                Toast.makeText(DashboardActivity.this.getApplicationContext(), "Unknown Refresh Error", Toast.LENGTH_SHORT).show();
                Log.e(TAG, "Unknown Refresh Error", e);
            }
        }
        // Handle success
        else if (isRunning) {
            nodes.clear();
            nodes.addAll(retrieved_nodes);
            haveNodes = true;
            adapter.notifyDataSetChanged();
            // Schedule the next run
            reloadHandler.postDelayed(reloadService, refreshRate * 1000);
            Log.i(TAG, "Next reload in " + refreshRate + " seconds");
        }
    }
}

private final Runnable reloadService = new Runnable() {  
    public void run() {
        // This happens asynchronously and schedules the next run
        refreshNodes();
    }
};

So this is where things get a bit weird. Obviously you don't want to lock up the UI when you retrieve data from the API, but since we're still (as far as I can tell) mostly stuck doing things synchronously on Android, we need to push API requests into a background thread. So I'm just going to run through what happens here, in the order that it happens:

  1. Whenever the Activity is resumed we set a variable to indicate that its running, then kick off this Runnable called reloadService.
  2. reloadService just calls refreshNodes()
  3. refreshNodes() makes sure the API is non-null (I don't remember why I did that or if I still need that check...). Then if we don't have any nodes yet it pops up a fatty modal dialog box (some day I'm going to replace this with a spinning "Loading" animation in the ListView itself like I just did for the NodeViewActivity). Finally it instantiates and executes a NodeUpdater object.
  4. The NodeUpdater extends AsyncTask, which is a neat little API that Android has for doing tasks in the background. Basically the method doInBackground() is (believe it or not) done in a background thread.
  5. This is all great, but it really messes with error handling. Remember that giant list of Exceptions that the API's getNodes() method can throw? Well we can't modify the UI from this background thread, so normal exception handling patterns sort of go out the window. Instead, if any sort of Exception is thrown, we store it in a field called 'e' for later access.
  6. Once the API call has returned the onPostExecute() method is called, this time in the UI thread. If one of those obnoxious modal loading dialogs exists we kill it off right away. Then, we check whether an exception is stored in that 'e' field and if it is we put it through a big if/elseif where we check its type against various Exception subclasses that we might want the user to know about, and show the user some sort of Toast based on the type of exception.
  7. If no exception occurred and 'isRunning' is still true (ie, the user didn't get bored and leave in the meantime), we replace the contents of the existing (currently empty) node array with the returned list of nodes. Then we set 'haveNodes' so the user won't see that silly modal dialog again, and use a handler to schedule another run of the reloadService in 30 seconds.
  8. When the activity is paused any scheduled API calls are canceled and 'isRunning' is set to false so that any ongoing API calls don't try to update the UI when they return.

Step 3: The List Adapter ------------------

In order to make the ListView not suck, we also need to use a custom adapter.

public class NodesAdapter extends ArrayAdapter<Node> {  
    private final int resource;

    public NodesAdapter(Context context, int resource, ArrayList<Node> nodes)
    {
        super(context, resource, nodes);
        this.resource = resource;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent)
    {
        RelativeLayout nodeView;
        Node node = getItem(position);

        String inflater = Context.LAYOUT_INFLATER_SERVICE;
        LayoutInflater li = (LayoutInflater)getContext().getSystemService(inflater);

        if(convertView==null) {
            nodeView = new RelativeLayout(getContext());
            li.inflate(resource, nodeView, true);
        }

        else {
            nodeView = (RelativeLayout) convertView;
        }

        // Set the a color representing the state
        TextView statusView = (TextView)nodeView.findViewById(R.id.node_item_status);
        statusView.setBackgroundDrawable(new ColorDrawable(node.getStateColor()));

        // Set the background
        ColorDrawable transparent = new ColorDrawable(Color.TRANSPARENT);
        ColorDrawable opaque = new ColorDrawable(node.color);
        StateListDrawable bg = new StateListDrawable();
        bg.addState(new int[] {android.R.attr.state_selected}, transparent);
        bg.addState(new int[] {android.R.attr.state_pressed}, transparent);
        bg.addState(new int[] {}, opaque);
        nodeView.setBackgroundDrawable(bg);

        // Set the name and tags
        TextView nameText = (TextView)nodeView.findViewById(R.id.name);
        TextView tagsText = (TextView)nodeView.findViewById(R.id.tags);
        nameText.setText(node.name);
        tagsText.setText(node.getTagString());

        return nodeView;
    }
}

Basically whenever the ListView wants to display a node in the list, it will call getView on the adapter. In this case most of that code is dedicated to setting a crazy background on the list items. If there are a relatively small number of background colors you will need for list items, just do this in XML and save yourself the trouble. But one cool feature of Cloudkick is that you can assign one of a bunch of colors to each of your nodes (just for organizational purposes). In order to make the selector (the orange - with the default theme - thing that highlights list items when you scroll) show through, I had to use a StateListDrawable to make the background transparent when the item was selected or pressed and opaque otherwise. But like I say, most people won't need this so your adapter will probably be much simpler than this.

Step 4: Drill Down --------------

If all your application does is display a ListView, it had better be a really really nice ListView. Even then, you probably want to be able to drill down to get details on those items. This is easy, we just need to add one function to the DashboardActivity:

public void onItemClick(AdapterView<?> parent, View view, int position, long id) {  
    Bundle data = new Bundle();
    data.putSerializable("node", nodes.get(position));
    Intent intent = new Intent(DashboardActivity.this, NodeViewActivity.class);
    intent.putExtras(data);
    startActivity(intent);
}

And don't forget that line (already shown above) in onCreate():

dashboard.setOnItemClickListener(this);  

When the user clicks a node, the node is serialized into a Bundle which is passed to a NodeViewActivity.

The NodeViewActivity is very similar to the DashboardActivity, with a few modifications:

  1. It extracts the serialized node from the passed Bundle and uses that to instantly populate (part of) the view.
  2. It polls for changes on both the node itself, and its check data. This works pretty much the same way as it does here, there are simply two
  3. It uses a nicer loading animation in the ListView itself, sort of like Gmail, Seesmic (which I really like) and half of the other apps on the market.

And thats basically it. There is a lot of code duplication between Activities, so one thing on my TODO list is attempting to abstract most of that out into a single base class, but I'm not certain when I'll be able to make that happen.

Again, check out the full source over on GitHub, and drop me a comment if you found this helpful, or if it looks like I'm doing something wrong.