Part IV

Data Stores, Network Services, and APIs

Chapter 30

Accessing Files

While Android offers structured storage, via preferences and databases, sometimes a simple file will suffice. Android offers two models for accessing files: one for files prepackaged with your application and one for files created on-device by your application.

You and the Horse You Rode in On

Let's suppose you have some static data you want to ship with the application, such as a list of words for a spell checker. The easiest way to deploy that is to put the file in the res/raw directory, so that it will be put in the Android application APK file as part of the packaging process as a raw resource.

To access this file, you need to get yourself a Resources object. From an activity, that is as simple as calling getResources(). A Resources object offers openRawResource() to get an InputStream on the file you specify. Rather than a path, openRawResource() expects an integer identifier for the file as packaged. This works just like accessing widgets via findViewById(); for example, if you put a file named words.xml in res/raw, the identifier is accessible in Java as R.raw.words.

Since you can get only an InputStream, you have no means of modifying this file. Hence, it is useful really only for static reference data. Moreover, since it doesn't change until the user installs an updated version of your application package, either the reference data must be valid for the foreseeable future or you must provide some means of updating the data. The simplest way to handle that is to use the reference data to bootstrap some other modifiable form of storage (e.g., a database), but that results in two copies of the data in storage. An alternative is to keep the reference data as is and keep modifications in a file or database, and then merge them together when you need a complete picture of the information. For example, if your application ships a file of URLs, you could have a second file that tracks URLs added by the user or references URLs that were deleted by the user.

In the Files/Static sample project, you will find a reworking of the list box example from earlier, this time using a static XML file instead of a hardwired array in Java. The layout is the same:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent" >
  <TextView
    android:id="@+id/selection"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
  />
  <ListView
    android:id="@android:id/list"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:drawSelectorOnTop="false"
  />
</LinearLayout>

In addition to that XML file, you also need an XML file with the words to show in the list:

<words>
  <word value="lorem" />
  <word value="ipsum" />
  <word value="dolor" />
  <word value="sit" />
  <word value="amet" />
  <word value="consectetuer" />
  <word value="adipiscing" />
  <word value="elit" />
  <word value="morbi" />
  <word value="vel" />
  <word value="ligula" />
  <word value="vitae" />
  <word value="arcu" />
  <word value="aliquet" />
  <word value="mollis" />
  <word value="etiam" />
  <word value="vel" />
  <word value="erat" />
  <word value="placerat" />
  <word value="ante" />
  <word value="porttitor" />
  <word value="sodales" />
  <word value="pellentesque" />
  <word value="augue" />
  <word value="purus" />
</words>

While this XML structure is not exactly a model of space efficiency, it will suffice for a demo.

The Java code now must read in that XML file, parse out the words, and put them someplace for the list to pick up:

public class StaticFileDemo extends ListActivity {
  TextView selection;
ArrayList<String> items=new ArrayList<String>();

  @Override
  public void onCreate(Bundle icicle) {
    super.onCreate(icicle);
    setContentView(R.layout.main);
    selection=(TextView)findViewById(R.id.selection);

    try {
      InputStream in=getResources().openRawResource(R.raw.words);
      DocumentBuilder builder=DocumentBuilderFactory
                               .newInstance()
                               .newDocumentBuilder();
      Document doc=builder.parse(in, null);
      NodeList words=doc.getElementsByTagName("word");

      for (inti=0;i<words.getLength();i++) {
        items.add(((Element)words.item(i)).getAttribute("value"));
      }

      in.close();
    }
    catch (Throwable t) {
      Toast
        .makeText(this, "Exception: "+t.toString(), Toast.LENGTH_LONG)
        .show();
    }

    setListAdapter(new ArrayAdapter<String>(this,
                                 android.R.layout.simple_list_item_1,
                                 items));
  }

  public void onListItemClick(ListView parent, View v, int position,
                  long id) {
    selection.setText(items.get(position).toString());
  }
}

NOTE: Our call to openRawResource() references R.raw.words as previously described. From Ice Cream Sandwich onward—and more specifically, SDK and ADT releases 14 and 15—Google has moved to disallow some references to resource fields in this fashion, to allow for library projects to be compiled only once and then reused across applications. Normally, this wouldn't warrant a mention. However, in Eclipse, the ADT plug-in released with SDK 14 mistakenly flags as an error our usage, as an attempt to use R.raw.words in a switch statement. Until this glitch is ironed out, you'll need to either build from the command line or tweak your ADT plug-in level.

The differences mostly lie within onCreate(). We get an InputStream for the XML file (getResources().openRawResource(R.raw.words)), then use the built-in XML parsing logic to parse the file into a DOM Document, pick out the word elements, and then pour the value attributes into an ArrayList for use by the ArrayAdapter.

The resulting activity looks the same as before, as shown in Figure 30–1, since the list of words is the same, just relocated.

images

Figure 30–1. The StaticFileDemo sample application

Of course, there are even easier ways to have XML files available to you as prepackaged files, such as by using an XML resource. That is covered in Chapter 31. However, while this example used XML, the file could just as easily have been a simple one-word-per-line list, or in some other format not handled natively by the Android resource system.

Readin' 'n Writin'

Reading and writing your own, application-specific data files is nearly identical to what you might do in a desktop Java application. The key is to use openFileInput() and openFileOutput() on your Activity or other Context to get an InputStream and OutputStream, respectively. From that point forward, it is not much different from regular Java I/O logic:

  • Wrap those streams as needed, such as by using an InputStreamReader or OutputStreamWriter for text-based I/O.
  • Read or write the data.
  • Use close() to release the stream when done.

If two applications both try to read a notes.txt file via openFileInput(), each will access its own edition of the file. If you need to have one file accessible from many places, you probably want to create a content provider, as will be described in an upcoming chapter.

Note that openFileInput() and openFileOutput() do not accept file paths (e.g., path/to/file.txt), just simple file names.

Following is the layout for the world's most trivial text editor, pulled from the Files/ReadWrite sample application:

<?xml version="1.0" encoding="utf-8"?>
<EditTextxmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/editor"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:singleLine="false"
  android:gravity="top"
  />

All we have here is a large text-editing widget…which is pretty boring.

The Java is only slightly more complicated:

package com.commonsware.android.readwrite;

importandroid.app.Activity;
importandroid.os.Bundle;
importandroid.view.View;
importandroid.widget.Button;
importandroid.widget.EditText;
importandroid.widget.Toast;
importjava.io.BufferedReader;
importjava.io.File;
importjava.io.InputStream;
importjava.io.InputStreamReader;
importjava.io.OutputStream;
importjava.io.OutputStreamWriter;

public class ReadWriteFileDemo extends Activity {
  private final static String NOTES="notes.txt";
  privateEditText editor;

  @Override
  public void onCreate(Bundle icicle) {
    super.onCreate(icicle);
    setContentView(R.layout.main);
    editor=(EditText)findViewById(R.id.editor);
  }

  public void onResume() {
    super.onResume();

    try {
      IputStream in=openFileInput(NOTES);

      if (in!=null) {
        InputStreamReader tmp=new InputStreamReader(in);
        BufferedReader reader=new BufferedReader(tmp);
        String str;
        StringBuilderbuf=new StringBuilder();

        while ((str = reader.readLine()) != null) {
          buf.append(str+" ");
        }

        in.close();
        editor.setText(buf.toString());
      }
    }
    catch (java.io.FileNotFoundException e) {
      // that's OK, we probably haven't created it yet
    }
    catch (Throwable t) {
      Toast
        .makeText(this, "Exception: "+t.toString(), Toast.LENGTH_LONG)
        .show();
    }
  }

  public void onPause() {
    super.onPause();

    try {
      OutputStreamWriter out=
          new OutputStreamWriter(openFileOutput(NOTES, 0));

      out.write(editor.getText().toString());
      out.close();
    }
    catch (Throwable t) {
      Toast
        .makeText(this, "Exception: "+t.toString(), Toast.LENGTH_LONG)
        .show();
    }
  }
}

First, we hook into onResume(), so we get control when our editor is coming back to life, from a fresh launch or after having been frozen. We use openFileInput() to read in notes.txt and pour the contents into the text editor. If the file is not found, we assume this is the first time the activity was run (or the file was deleted by other means), and we just leave the editor empty.

Next, we hook into onPause(), so we get control as our activity gets hidden by another activity or is closed, such as via the device's Back button. Here, we use openFileOutput() to open notes.txt, into which we pour the contents of the text editor.

The net result is that we have a persistent notepad, as shown in Figures 30–2 and 30–3. Whatever is typed in will remain until deleted, surviving our activity being closed (e.g., via the Back button), the phone being turned off, or similar situations.

images

Figure 30–2. The ReadWriteFileDemo sample application, as initially launched

images

Figure 30–3. The same application, after entering some text

Another approach for working with application-local files is to use getFilesDir(). This returns a File object pointing to a place in the onboard flash where an application can store files. This directory is where openFileInput() and openFileOutput() work. However, while openFileInput() and openFileOutput() do not support subdirectories, the File from getFilesDir() can be used to create and navigate subdirectories if desired.

The files stored here are accessible only to your application, by default. Other applications on the device have no rights to read, let alone write, to this space. However, bear in mind that some users “root” their Android phones, gaining superuser access. These users will be able to read and write whatever files they wish. As a result, please do not consider application-local files to be secure against interested users.

External Storage: Giant Economy-Size Space

In addition to application-local storage, you also have access to external storage. This may be in the form of a removable media card, like an SD card or microSD card, or in the form of additional onboard flash set aside to serve in the “external storage” role.

On the plus side, external storage tends to have more space available than onboard storage. Onboard storage can be rather limited; for example, the original T-Mobile G1 (HTC Dream) had a total of 70MB for all applications combined. Although newer phones offer more onboard space, external storage is usually at least 2GB and can be as big as 32GB.

On the minus side, all applications can, if they wish, read and write external storage, so these files are not very secure. Furthermore, external storage can be mounted on a host computer as a USB mass storage device—when it is in use in this mode, Android applications cannot access it. As a result, files on external storage may or may not be available to you at any given moment.

Where to Write

If you have files tied to your application that are simply too big to risk putting in the application-local file area, you can use getExternalFilesDir(), available on any activity or other Context. This gives you a File object pointing to an automatically created directory on external storage, unique for your application. While not secure against other applications, it does have one big advantage: when your application is uninstalled, these files are automatically deleted, just like the ones in the application-local file area.

If you have files that belong more to the user than to your app (for example, pictures taken by the camera, downloaded MP3 files, etc.), a better solution is to use getExternalStoragePublicDirectory(), available on the Environment class. This gives you a File object pointing to a directory set aside for a certain type of file, based on the type you pass into getExternalStoragePublicDirectory(). For example, you can ask for DIRECTORY_MOVIES, DIRECTORY_MUSIC, or DIRECTORY_PICTURES for storing MP4, MP3, or JPEG files, respectively. These files will be left behind when your application is uninstalled.

You will also find a getExternalStorageDirectory() method on Environment, pointing to the root of the external storage. This is no longer the preferred approach—the methods previously described help keep the user's files better organized. However, if you are supporting older Android devices, you may need to use getExternalStorageDirectory(), simply because the newer options may not be available to you.

When to Write

Starting with Android 1.6, you also need to hold permissions to work with external storage (e.g., WRITE_EXTERNAL_STORAGE). The concept of permissions will be covered in a later chapter.

Also, external storage may be tied up if the user has mounted it as a USB storage device. You can use getExternalStorageState() (a static method on Environment) to determine whether or not the external storage is presently available.

StrictMode: Avoiding Janky Code

Users are more likely to like your application if, to them, it feels responsive. By “responsive,” we mean that it reacts swiftly and accurately to user operations, like taps and swipes.

Conversely, users are less likely to be happy with your application if they perceive that your UI is “janky”—sluggish to respond to their requests. For example, perhaps your lists do not scroll as smoothly as users would like, or tapping a button does not yield the immediate results they seek.

While threads and AsyncTask and the like can help, it may not always be obvious where you should apply them. A full-scale performance analysis, using Traceview or similar Android tools, is certainly possible. However, there are a few standard sorts of things that developers do, sometimes quite by accident, on the main application thread that tend to cause sluggishness:

  • Flash I/O, both for the onboard storage and for external storage (e.g., the SD card)
  • Network I/O

However, even here, it may not be obvious that you are performing these operations on the main application thread. This is particularly true when the operations are really being done by Android's code that you are simply calling.

That is where StrictMode comes in. Its mission is to help you determine when you are doing things on the main application thread that might cause a janky user experience.

Setting Up StrictMode

StrictMode works on a set of policies. There are presently two categories of policies: VM policies and thread policies. VM policies represent bad coding practices that pertain to your entire application, notably leaking SQLite Cursor objects and kin. Thread policies represent things that are bad when performed on the main application thread, notably flash I/O and network I/O.

Each policy dictates what StrictMode should watch for (e.g., flash reads are OK but flash writes are not) and how StrictMode should react when you violate the rules, such as

  • Log a message to LogCat
  • Display a dialog box
  • Crash your application (seriously!)

The simplest thing to do is call the static enableDefaults() method on StrictMode from onCreate() of your first activity. This will set up normal operation, reporting all violations by simply logging to LogCat. However, you can set your own custom policies via Builder objects if you so choose.

Seeing StrictMode in Action

The Threads/ReadWriteStrict sample application is a reworking of the Files/ReadWrite sample application shown earlier in this chapter. All it adds is a custom StrictMode thread policy:

StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                           .detectAll()
                           .penaltyLog()
                           .build());

If you run the application, the user will see no difference. However, you will have a debug-level log message in LogCat with the following stack trace:

12-28 17:19:40.009: DEBUG/StrictMode(480): StrictMode policy violation; ~duration=169images
 ms: android.os.StrictMode$StrictModeDiskReadViolation: policy=23 violation=2
12-28 17:19:40.009: DEBUG/StrictMode(480): atimages
 android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:745)
12-28 17:19:40.009: DEBUG/StrictMode(480): atimages
 dalvik.system.BlockGuard$WrappedFileSystem.open(BlockGuard.java:228)
12-28 17:19:40.009: DEBUG/StrictMode(480): atimages
 android.app.ContextImpl.openFileOutput(ContextImpl.java:410)
12-28 17:19:40.009: DEBUG/StrictMode(480): atimages
 android.content.ContextWrapper.openFileOutput(ContextWrapper.java:158)
12-28 17:19:40.009: DEBUG/StrictMode(480): atimages
 com.commonsware.android.readwrite.ReadWriteFileDemo.onPause(ReadWriteFileDemo.java:82)

Here, StrictMode is warning us that we attempted a flash write on the main application thread (the thread on which we set the StrictMode policy). Ideally, we would rewrite this project to use an AsyncTask or something for writing out the data.

Development Only, Please!

Do not use StrictMode in production code. It is designed for use when you are building, testing, and debugging your application. It is not designed to be used in the field.

To deal with this, you could

  • Simply comment out or remove the StrictMode setup code when you prepare your production builds
  • Use some sort of production flag to skip the StrictMode setup code when needed

Conditionally Being Strict

StrictMode is only for Android 2.3 and higher. Hence, if we have it in our code, even in development mode, it might interfere when we try testing on older emulators or devices. As we saw in an earlier chapter, there are techniques for dealing with this, but using reflection for configuring StrictMode would be rather painful.

The right approach, therefore, is simply to organize your code such that you have regular classes using newer APIs, but you do not load those classes on older devices. The APIVersions/ReadWriteStrict project demonstrates this, allowing an application to use Android 2.3's StrictMode where available and skip it where it is not available.

When we examined StrictMode earlier in this section, we configured StrictMode right in the onCreate() method of our sample activity. This works, but only on Android 2.3 and newer.

To allow this to work on older versions of Android, we use StrictWrapper:

packagecom.commonsware.android.readwrite;

importandroid.os.Build;

abstract class StrictWrapper {
  static private StrictWrapper INSTANCE=null;

  static public void init() {
    if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.GINGERBREAD) {
      INSTANCE=new StrictForRealz();
    }
    else {
      INSTANCE=new NotAllThatStrict();
    }
  }

  static class NotAllThatStrict extends StrictWrapper {
    // no methods needed
  }
}

This odd-looking class encapsulates our “do-we-or-don't-we” logic for dealing with StrictMode. It contains an init() method that, when called, checks to see which version of Android the application is running on, and creates a singleton instance of a StrictWrapper subclass based upon it—StrictForRealz for Android 2.3 and higher, NotAllThatStrict for older versions of Android. The latter class, a static inner class of StrictWrapper, does nothing, reflecting that there is no StrictMode in newer versions of Android.

StrictForRealz contains the StrictMode initialization logic:

packagecom.commonsware.android.readwrite;

importandroid.os.StrictMode;

classStrictForRealz extends StrictWrapper {
  StrictForRealz() {
    StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                               .detectAll()
                               .penaltyLog()
                               .build());
  }
}

And, our onCreate() method of our activity calls init() on StrictWrapper, to trigger creating the proper object:

@Override
public void onCreate(Bundle icicle) {
  super.onCreate(icicle);
  setContentView(R.layout.main);

  StrictWrapper.init();

  editor=(EditText)findViewById(R.id.editor);
}

When the activity first starts up, neither StrictWrapper nor StrictForRealz is loaded in the process. As soon as we reach the init() statement in onCreate(), Android loads StrictWrapper into the process, but this is safe, as it does not refer to any potentially nonexistent classes. The init() method on StrictWrapper then executes a statement involving StrictForRealz only if we are safely on a supported version of Android. Hence, StrictForRealz will be loaded into the process only if we are on a newer Android release, so our use of StrictMode in StrictForRealz will not trigger a VerifyError.

Here, all we needed was a bit of initialization. The singleton pattern is used to demonstrate that you could expose a version-dependent API implementation if you desired. Simply define the API as abstract methods on the abstract class (StrictWrapper) and have version-dependent concrete implementations of those abstract methods on the concrete subclasses (StrictForRealz, NotAllThatStrict).

Linux File Systems: You Sync, You Win

Android is built atop a Linux kernel and uses Linux file systems for holding its files. Classically, Android used YAFFS (Yet Another Flash File System), optimized for use on low-power devices for storing data to flash memory. Many devices still use YAFFS today.

YAFFS has one big problem: only one process can write to the file system at a time. Rather than offering file-level locking, YAFFS has partition-level locking. This can become a bit of a bottleneck, particularly as Android devices grow in power and start wanting to do more things at the same time, like their desktop and notebook brethren.

Android is starting to move toward ext4, another Linux file system aimed more at desktops/notebooks. Your applications will not directly perceive the difference. However, ext4 does a fair bit of buffering, and it can cause problems for applications that do not take this buffering into account. Linux application developers ran headlong into this in 2008 and 2009, when ext4 started to become popular. As an Android developer, you will need to think about it now…for your own file storage.

If you are using SQLite or SharedPreferences, you do not need to worry about this problem. Android (and SQLite, if you are using it) handles all the buffering issues for you. If, however, you write your own files, you may wish to contemplate an extra step as you flush your data to disk. Specifically, you need to trigger a Linux system call known as fsync(), which tells the file system to ensure all buffers are written to disk.

If you are using java.io.RandomAccessFile in a synchronous mode, this step is handled for you as well, so you will not need to worry about it. However, Java developers tend to use FileOutputStream, which does not trigger an fsync(), even when you call close() on the stream. Instead, you call getFD().sync() on the FileOutputStream to trigger the fsync(). Note that this may be time consuming, and so disk writes should be done off the main application thread wherever practical, such as via an AsyncTask.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset