Android Development – Fun With Files And UI

By | May 15, 2010

Bare bones file manipulation on an Android device is stupid easy, as long as you know how to interact with files in Java and as long as you stay within your application’s designated space. The former, well, this post isn’t going to fill in the blanks. The latter is cake; just start with the guidelines below. Note that to get something slightly above trivial, I had to sprinkle some additional UI goodness throughout the sample app that takes us a bit far afield, so bear with it as we go.

The Goal

Let’s say that in our Android app we want to represent users by separate files. The files could contains user preferences for that specific user in some predefined format; for this post, we’ll just be writing out the time of user “creation” so we can focus on the file manipulation aspect of things.  Our convention will be “<username>.prefs” where <username> is the name of the selected user.

When our application spins up, it will look for a list of preference files and present the corresponding list of defined users in a spinner control so that the current user can be chosen. It will also allow the user to enter a new name, which will create a corresponding preferences file in the application’s file space. This will allow us to support multiple users in our app in a very bare bones way.

The App

Let’s start by creating a new Android app. Our first task will be to look in the application’s directory for any preference files, and if they are found, to present them in a spinner control for the user to select from. We’ll be calling our application FileTests, and our source file will be called the same. Let’s modify the main layout file, main.xml by adding a Spinner control inside of the LinearLayout that Eclipse gave us in our default layout. Find your main.xml layout file in the res –> layout node in the Package Explorer and double click it to bring up the file in the editor.

defaultLayout

Now double-click the LinearLayout node in the bottom right of the IDE, and then click the green plus sign that appears in the toolbar above the element list.

addingElement

This brings up a simple dialog where you can specify what UI element you want to add to your screen. Select Spinner and click OK.

pickSpinner

Your layout will now look as below.

spinnerAdded

Let’s go ahead and rename the spinner to something more indicative of what it does, such as UserList. Let’s also change the width to something a little less offensive for cases when our user list is empty. Our layout now looks as below.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/hello" />
    <Spinner
        android:id="@+id/UserList"
        android:layout_width="300sp"
        android:layout_height="wrap_content"></Spinner>
</LinearLayout>

Now we need to write the code that looks for files of type <username>.prefs in our application space to load into our spinner control. We will be using the fileList method, which returns a list of all the files in our application directory. The code is as below.

private void loadUsers() {
    String [] files = fileList();
    _users = new ArrayList<String>();
    for (String f : files) {
        _users.add(f);
    }
}

Pretty straightforward. Again, the only file access we have to worry about here is figuring out what files we have in our application directory. That method call is on line 2 of the listing above, the rest is just storing it in an ArrayList called _users that we are defining in our class. After we lace this into our onCreate method, our FileTests.java file looks like below.

public class FileTests extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        loadUsers();
    }

    private ArrayList<String> _users;

    private void loadUsers() {
        String [] files = fileList();
        _users = new ArrayList<String>();
        for (String f : files) {
            _users.add(f);
        }
    }
}

Since we haven’t defined any users yet, running our app does nothing.

Adding New Users

Bear with me here – it’s going to take a bit to get to the part where we actually need to write out a file to the file system. To add a new user in our bare bones app, we need to create a file of the right name in our application directory space. But first we need to let the user enter her name so that the right file can be created. To do this, we’ll add another LinearLayout above our current one which will contain and EditText element and a Button element. The user will then be able to enter her name in the EditText element and press the button to add herself to the list of users the app knows about.

To effect our desired layout (get it), we need to add two new LinearLayouts to our root LinearLayout. We’ll present the add new user functionality at the top of our screen, so we’ll shift our current spinner element into the second of our new LinearLayouts, and add an EditText and Button to the first of our new LinearLayouts.

Again, open up the main.xml layout file, double-click the root LinearLayout node in our Outline tab, and add two LinearLayouts.

newLinears

With the second LinearLayout selected, press the blue up arrow once to make it a sibling of the first LinearLayout that you added. Go ahead and select the TextView and delete it, as we don’t need it right now. Then select the Spinner control and click the down arrow three times to make it a child of the second LinearLayout in our screen. Finally, select the first child LinearLayout and add an EditText and a Button control to it.

newUserElementsAdded

Finally, we’ll set a width on our EditText element and our Button element so they don’t shift all willy-nilly based on the contained text, and we’ll rename our screen elements to better reflect their function. Our new layout finally looks like below.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <LinearLayout
        android:id="@+id/UserNameEntryLayout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
        <EditText
            android:text=""
            android:id="@+id/UserNameEntry"
            android:layout_width="200sp"
            android:layout_height="wrap_content"></EditText>
        <Button
            android:text="Add"
            android:id="@+id/AddNewUser"
            android:layout_width="100sp"
            android:layout_height="wrap_content"></Button>
    </LinearLayout>
    <LinearLayout
        android:id="@+id/CurrentUserDisplay"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
        <Spinner
            android:id="@+id/UserList"
            android:layout_width="300sp"
            android:layout_height="wrap_content"></Spinner>
    </LinearLayout>
</LinearLayout>

Now for some code. Let’s start by adding a handler to our AddNewUser button. This handler will take the text from our EditText control and add it to our internal list of user names.

At this point you’ll notice that we actually didn’t bother binding our list of user names to our spinner control up above. Since we’re actually going to start dealing with users now, we’ll go ahead and write that code as well. First, the button handler to get our new user name.

private void setupButtonHandlers() {
    Button b = (Button) findViewById(R.id.addNewUser);
    b.setOnClickListener(new OnClickListener() {
        public void onClick(View v) {
            TextView t = (TextView) findViewById(R.id.userNameEntry);
            String newUserName = t.getText().toString();
            if (newUserName.length() <= 0)
                return;
            // non-zero user name
            _users.add(newUserName);

            bindUsers();
        }
    });
}

Next, the method that binds our user name list to our spinner control.

private void bindUsers() {
    ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
            android.R.layout.simple_spinner_item, _users);
    adapter
            .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    Spinner s = (Spinner) findViewById(R.id.userList);
    s.setAdapter(adapter);
}

So now when we run our app, we can add users to our list by entering their name and clicking the Add button. However, when we restart our app, it’s as if we didn’t add any new users during the last run. This is where we need to persist our new user preference files out to disk. Before that, let’s make our user experience a smidgen better by clearing out the EditText element when we click the Add button, preventing the user from adding a duplicate name, and selecting the newly added user from the list after the Add button is clicked.

The first tweak is easy; we just add the below line to the addNewUser click handler.

t.setText("");

The second tweak is also straightforward,  we just look for the entered user name in our list of user names, returning if we find it, adding it if we don’t. Our Add button handler looks like below now.

private void setupButtonHandlers() {
    Button b = (Button) findViewById(R.id.addNewUser);
    b.setOnClickListener(new OnClickListener() {
        public void onClick(View v) {
            TextView t = (TextView) findViewById(R.id.userNameEntry);
            String newUserName = t.getText().toString();
            if (newUserName.length() <= 0)
                return;
            int pos = _users.indexOf(newUserName);
            if (pos == -1) {
                _users.add(newUserName);
                pos = _users.indexOf(newUserName);
            }
            t.setText("");
            bindUsers();
        }
    });
}

The third tweak builds on the second tweak. We’ll use the integer position of the found or newly added user name to simply select it in our Spinner control. This requires adding the below two lines to our Add button handler below the call to bindUsers.

Spinner s = (Spinner) findViewById(R.id.userList);
s.setSelection(pos);

Persisting Information

Finally we get to the next aspect of file handling in an Android app, creating files. So we’re still stuck with the issue where we can add users find while within the app, but restarting the app causes us to forget all the users we’ve added. We can fix this by adding a method that will create a file based on our convention above, namely <username>.prefs.

private void persistUser(String userName) {
    String filename = userName.concat(".prefs");
    FileOutputStream fos = null;
    try {
        fos = openFileOutput(filename, MODE_PRIVATE);
        PrintStream p = new PrintStream(fos);
        p.println(userName);
        p.println(System.currentTimeMillis());
        p.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } finally {
        if (fos != null)
            try {
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
    }
}

The call to openFileOutput is what creates our file, if it doesn’t already exist. If it does, it simply opens it up for output. You can see also that we are writing two pieces of information to our file, namely the user name and the current system time in milliseconds. We have standard exception handling and cleanup in the rest of the method. Now when we add users in our application, a corresponding file is persisted, allowing our application to remember what users we have added the next time we start up.

One more thing we have to do is modify our routine that loads our users from the file list. We need to splice out the file extension, which we do by splitting on the “.” in the file name.

private void loadUsers() {
    String[] files = fileList();
    _users = new ArrayList<String>();
    for (String f : files) {
        String[] parts = f.split("\\.");
        _users.add(parts[0]);
    }
}

Reading File Contents

The next thing we want to do is read the contents of the user file when a user is selected or added. To show the contents, we’ll add another LinearLayout element after our current two LinearLayouts, and add a TextView to display the actual contents.

contentsDisplay

Since we want the contents to be read when we select a user, we wire up an event handler to our Spinner’s item selected event.

private void setupSpinnerHandler() {
    Spinner s = (Spinner) findViewById(R.id.userList);
    s.setOnItemSelectedListener(new OnItemSelectedListener() {
        public void onItemSelected(AdapterView<?> arg0, View arg1,
                int arg2, long arg3) {
            TextView item = (TextView) arg1;
            loadUserFileContents(item.getText().toString());
        }
        public void onNothingSelected(AdapterView<?> arg0) {
        }
    });
}

And the meat of the file read is in loadUserFileContents, shown below.

private void loadUserFileContents(String userName) {
    String filename = userName.concat(".prefs");
    FileInputStream f = null;
    try {
        f = openFileInput(filename);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
        return;
    }

    StringBuilder contents = new StringBuilder();
    int buf = -1;
    do {
        try {
            buf = f.read();
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
        contents.append((char) buf);
    } while (buf != -1);

    TextView fileContents = (TextView) findViewById(R.id.userFileContents);
    fileContents.setText(contents.toString());
}

The magic happens on line 5 with the call to openFileInput. The rest is just standard file reading cruft.

Deleting Files

We’ll wrap up our fantastic app by demonstrating file deletion. We’ll add another LinearLayout and a button to delete the currently selected user along with his file.

deleteUserLayout

And the code is just as straightforward, we simply have to add the below to our setupButtonHandlers method.

b = (Button) findViewById(R.id.deleteUser);
b.setOnClickListener(new OnClickListener() {
    public void onClick(View v) {
        Spinner s = (Spinner) findViewById(R.id.userList);
        String userName = s.getSelectedItem().toString();
        if (userName == null) return;

        String fileToDelete = userName.concat(".prefs");
        deleteFile(fileToDelete);

        loadUsers();
        bindUsers();
        s.setSelection(0);
    }
});

Here the magic takes place on the call to deleteFile.

There is a lot more that could be done to make our app more intelligent, but we strayed off the beaten path enough as it was. We covered creating files, writing to files, reading from files, and deleting files. All of it was straightforward, and adds another weapon to your arsenal when it comes to handling persisting of user information.