Hands-on: Multiple Activities and Data Persistence in Android

Building on our Android-based TicketResponder applicatoin, we add a new screen and data persistence.

Motivation

This article builds upon the work of our previous articles in building a real-world data collection application for Android. In this article we add an additional user interface screen to capture necessary configuration data which will be used by the application in a subsequent step. The code samples in this article demonstrate:

  • The creation of a new user interface screen (Activity) with multiple controls
  • Navigation between two different Activity classes
  • Creating and using a basic Intent
  • The retrieval/storage of configuration data via the program

These are some necessary steps along the path to building the full TicketResponder application.

Persistence Options

Before jumping directly into the code, I thought it might be helpful to take a quick tour of data persistence options in Android. Storing data in Android can be accomplished through a number of mechanisms. Which technique is right for your application depends on a number of factors including:

  • How much data is to be stored
  • How frequently the data is accessed
  • The kind of manipulations you need to perform on/with the data
  • Which applications need access to the data

If your application needs to store some small data elements such as strings or integer values, you might first consider SharedPreferences. SharedPreferences work in a manner similar to an “ini” file with key/value pairs often found on other desktop and server platforms. We will use this technique later in this article as we add a Settings screen to our application to store username, password, and a server internet address, or URL.

If you have larger amounts of data to manage, it is also possible to store data in text or binary files, much like you would use in a server or desktop application. The familiar java.io package is available for manipulating files including a variety of Stream-based classes familiar to Java programmers. One item to note with respect to working with files is that you must use a non-standard means of opening/creating the file to properly navigate the process/security context within the Linux-based Android environment. Your files should be referenced by way of your application’s context, as shown below:

FileOutputStream fos = this.context.openFileOutput("somefilename.xml", Context.MODE_PRIVATE);
fos.write("\n".getBytes());
fos.write("somedatagoeshere".getBytes());
fos.close();

The example above opens a file for output within its own directory. Once the file is opened, we can access it via a FileOutputStream or other type of file I/O class. Once the file is opened, we write two lines of text to the file and then close it the file. This technique of working with a file-based persistence method would be appropriate for storing a local-cache of application specific files for applications such as RSS readers, virus definition files, or perhaps a global high-scorers file downloaded from a gaming server on the Internet.

For more sophisticated data storage requirements, your application can leverage the powerful and flexible SQLite database. SQLite provides a significant subset of the popular Structured Query Langauge (SQL) found in more powerful (and costly) database technologies. Those familiar with relational databases such as MySQL, Microsoft SQL Server, Oracle and others will find that much of the terminology to be similar to what you are accustomed to — you work with databases, tables, fields/columns, etc. There are some differences with respect to how certain data types are treated but for a mobile platform the opportunity to have a real SQL database is just awesome. I’ve spent more hours than I care to think about navigating the RecordStores of the Research in Motion BlackBerry platform or the limited database structures of Palm OS. I’ve even written ISAM libraries for a mobile device but I’d rather not think about that right now. SQLite has become the defacto mobile database as it is now used in Android, iPhone, Palm webOS, and is the underlying data storage technology for HTML5 databases powered by WebKit.

Using an SQLite database is a good idea when you have larger data sources that you need to search or sort. Examples that come to mind include a list of client-specific data such as inventory data or a product list. An application can manipulate an SQLite database for its own purposes, however the application must know the schema of the data and in general, using an SQL database is a task aimed at developers building a closed application, meaning no other application knows or cares about your data per se. An SQLite database is “private” to the application which utilizes it and other applications cannot readily attach to it nor manipulate its data.

If your application intends to share data with a variety of other applications, you will want to consider a creating a ContentProvider. A ContentProvider is the preferred technique for sharing data amongst multiple applications. For example, the Contacts data (i.e. Address Book records) is accessed via a ContentProvider with standard queries.

Lastly, we would be remiss to omit the “network” as a place to store data. In fact, a mobile device with powerful Internet connectivity such as Android can leverage the storage facilities of a datacenter-hosted server on an as-needed basis. Over time we will see more of this kind of application — mobile users leveraging cloud services, including storage. In fact, this paradigm is so prevalent on the web today that it is often over-looked.

For example, Amazon has created a service called Amazon Simple Storage Service where they provide a simple API for storing and retrieving data. Amazon (and other providers) take care of the dirty work of running a massively web-scaled datacenter, allowing the developer to focus on application functionality and not get bogged down with database server maintenance. If a given application is wildly successful and needs more storage, they just crank up the volume with the storage provider — no need to go out and buy new hardware and software to support the load. As a developer first and a reluctant system admin second, I can appreciate the allure of such arrangements. The challenge of course is to make sure that your business model justifies the purchase of more storage. Remember, if you’re losing money, you cannot make it up in volume!

We will examine both SQLite, ContentProviders, and even Cloud connectivity in future Android articles. For now, we’re going to focus on expanding our TicketResponder application by adding a second Activity. This Activity presents a user interface for the user to manipulate configuration data along with the code for saving and restoring the configuration data. These data elements are necessary for the application to access a remote server. Let’s look at the bigger picture to better understand what our application must accomplish.

The Requirements

If you recall from a prior article, our application collects a couple of data elements from the user. The application takes as input a ticket number (never mind for now where that came from — we’ll get there eventually!) and a desired status such as “In Progress”, “Waiting on Client”, “Closed”, etc. as shown in the image below.

enterticket.png

Once the user selects the “Update Ticket” button it is time to package the data into a server-friendly format and then transmit it to the server. We will cover the actual communications code in a future article — for now we need to manage to navigate between our two Activity classes and learn how to store and retrieve the data elements, which in our case consists of three string values: server address, username, and password.

The Intent and the Activity

Recall that an Activity is essentially a user interface screen. The user interface elements are defined in a layout file which is an xml file. This file is created either “by hand” or through the use of WSYWIG tools in the Android Developer Tools plugin for Eclipse. I typically copy and paste from one file to the next and then tweak the new one as desired. For more information on creating layout resources, please see prior article. Once we’ve got the user interface defined for our second Activity (see settings.xml in the source code for layout details), we need to know how to launch this second Activity. The image below depicts our new settings screen user interface.

settingsscreen1.png

The most straight-forward means to launch one Activity from another is to use an explicit Intent — that is, an Intent which explicitly identifies another Activity class. Here is a snippet of code taken from TicketResponder.java which implements this new functionality.

final ImageView settingsImage = (ImageView) findViewById(R.id.ImageSettings);
settingsImage.setOnClickListener(new ImageView.OnClickListener() {
		public void onClick(View v) {
			try {
				Intent in = new Intent(v.getContext(),TRSettings.class);
				startActivity(in);
			}
			catch (Exception e){
				Log.i(tag,"Failed to launch Settings Activity " + e.getMessage());
			}
		}
}
);

We must also define in our application what the user interaction must be to perform this operation. Note in the prior screen shot the “gear-like” icon in the lower left portion of the screen. When this icon is clicked, we run the above code to launch the TRSettings Activity. This code is an anonymous onClick handler for an ImageView widget. It simply creates a new Intent which identifies the TRSettings class (defined in TRSettings.java) and then starts that Activity by passing the Intent to the startActivity() method. Let’s have a closer look at the functionality of TRSettings Activity.

Shared Preferences Example

The purpose of the TRSettings class is to:

  1. Retrieve three data elements from persistent storage
  2. Display those three data elements: user name, password, server address in EditText widgets
  3. Handle a Button press by capturing data from the EditText widgets
  4. Validate that the user entered appropriate data. For our purposes, we just make sure the fields are not empty
  5. Store the new values to persistent storage
  6. Close this Activity and return to the prior screen

We implement our data persistence with the previously mentioned SharedPreferences class. We simply create an instance of our SharedPreferences with a call to the getSharedPreferences() method passing in a name for the preferences we want to work with and an access mode. In this case we use our application name of “TicketResponder” and the MODE_PRIVATE flag. These preferences are private to our own application. Remember, SharedPreferences are appropriate for simple storage such as our example of username/server address kinds of data elements. Other uses might include saving state from one invocation of our application to another — this is helpful for pausing a game during an incoming phone call. You certainly wouldn’t want your user to have to go back to level 1 just because the phone rang! This might also look like remembering the unique identifier of a contact record or inventory part so the application can re-load that data the next time the user starts the application. Providing this kind of feature goes a long way in making your application more usable and building loyal fans.

We next create an instance of the Editor class with a call to the edit() method of the SharedPreferences class instance. Data is retrieved with a call to the getString() method. Data is stored with calls to the putString() method. When all manipulation of the data is complete and we’re ready to go, we call the commit() method of the Editor instance. This performs the actual persistence of our data to the SharedPreferences instance.

The final thing we do is call the finish() method which causes our Activity to close. Hitting the “back” button (or escape while running in the Android Emulator) causes the Activity to close without saving the configuration data. This is in essence a “cancel” operation. Here is the code for the Activity as found in TRSettings.java

package com.msi.linuxmagazine.ticketing;
import android.app.Activity;
import android.content.Context;
import android.view.View;
import android.os.Bundle;
import android.widget.Button;
import android.widget.EditText;
import android.util.Log;
import android.app.AlertDialog;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
public class TRSettings extends Activity {
	final String tag = "TRSettings";
	private SharedPreferences prefs = null;
	private Editor editor = null;
	public static final String SERVERADDRESS = "serveraddress";
	public static final String USERNAME = "username";
	public static final String PASSWORD = "password";
	public static final String PREFERENCESNAME = "TicketResponder";
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.settings);
        final EditText serverAddress = (EditText) findViewById(R.id.serverAddress);
        final EditText userName = (EditText) findViewById(R.id.userName);
        final EditText password = (EditText) findViewById(R.id.password);
        final Button saveButton = (Button) findViewById(R.id.saveButton);
        prefs = this.getSharedPreferences("TicketResponder", Context.MODE_PRIVATE);
        editor = prefs.edit();
        serverAddress.setText(prefs.getString(TRSettings.SERVERADDRESS, "http://servernamegoeshere/"));
        userName.setText(prefs.getString(TRSettings.USERNAME, "user"));
        password.setText(prefs.getString(TRSettings.PASSWORD,"password"));
        saveButton.setOnClickListener(new Button.OnClickListener() {
            public void onClick(View v) {
                try {
                	String server = serverAddress.getText().toString();
                	String user = userName.getText().toString();
                	String pword = password.getText().toString();
                	Log.i(tag,"server address is [" + server + "]");
                	Log.i(tag,"username is [" + user + "]");
                	Log.i(tag,"password address is [" + pword + "]");
                	// let's do some basic input filtering
                	if (user.trim().length() == 0 || server.trim().length() == 0 || pword.trim().length() == 0)
                	{
                		AlertDialog.Builder adb = new AlertDialog.Builder(v.getContext());
                        AlertDialog ad = adb.create();
                        ad.setMessage("All fields are required.");
                        ad.show();
                        return;
                	}
                	// let's store in a shared preference
                	editor.putString(TRSettings.SERVERADDRESS,server);
                	editor.putString(TRSettings.USERNAME,user);
                	editor.putString(TRSettings.PASSWORD,pword);
                	editor.commit();
                	finish();
                } catch (Exception e) {
                    Log.i(tag, "Error occurred [" + e.getMessage() + "]");
                }
            }
        });
    }
}

Updating AndroidManifest.xml

Running the code is easy — just launch the Android Emulator (or real device) through the Run menu in Eclipse as described in the previous article, which covered application debugging. If you do this, you’ll get an error in your application complaining that the application could not continue because the Activity doesn’t exist. Huh? After all that we cannot run our application to see the new Activity? What could it be? If you look in the Log you’ll see a message that looks like this:

08-31 23:05:11.130: INFO/TicketResponder(770): Failed to launch Settings Activity Unable to find explicit activity class {com.msi.linuxmagazine.ticketing/com.msi.linuxmagazine.ticketing.TRSettings}; have you declared this activity in your AndroidManifest.xml?

The problem is that we didn’t tell Android that we had an additional, runnable Activity class. In order to do this we need to modify our AndroidManifest.xml file to let Android know about our new Activity. Fortunately, this is a very simple thing to do — all we have to do is add a single line to our AndroidManifset.xml file: . Now our complete AndroidManifset.xml file has two Activity classes defined:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      android:versionCode="1"
      android:versionName="1.0" package="com.msi.linuxmagazine.ticketing">
    <application android:label="@string/app_name" android:icon="@drawable/network_services">
        <activity android:name=".TicketResponder"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    <activity android:name="TRSettings"></activity>
</application>
    <uses-sdk android:minSdkVersion="3" />
</manifest>

Our TicketResponder application now has two Activity classes that we can navigate between and the ability to store small amounts of data. The full source code is available on the Linux Magazine mobile source website in both a browsable & SVN checkout format and as a zip file. Coming up we’re going to look at adding some Location/GPS data to our application.

Fatal error: Call to undefined function aa_author_bios() in /opt/apache/dms/b2b/linux-mag.com/site/www/htdocs/wp-content/themes/linuxmag/single.php on line 62