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.
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.
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.
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:
InputStreamReader
or OutputStreamWriter
for text-based I/O.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.
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.
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.
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.
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.
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:
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.
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
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.
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=169
ms: android.os.StrictMode$StrictModeDiskReadViolation: policy=23 violation=2
12-28 17:19:40.009: DEBUG/StrictMode(480): at
android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:745)
12-28 17:19:40.009: DEBUG/StrictMode(480): at
dalvik.system.BlockGuard$WrappedFileSystem.open(BlockGuard.java:228)
12-28 17:19:40.009: DEBUG/StrictMode(480): at
android.app.ContextImpl.openFileOutput(ContextImpl.java:410)
12-28 17:19:40.009: DEBUG/StrictMode(480): at
android.content.ContextWrapper.openFileOutput(ContextWrapper.java:158)
12-28 17:19:40.009: DEBUG/StrictMode(480): at
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.
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
StrictMode
setup code when you prepare your production buildsStrictMode
setup code when neededStrictMode
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
).
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
.