237 237 Chapter 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. Both of these models are covered in this chapter. 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 place the file in the res/raw directory, so 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 really useful just for static reference data. Moreover, since it is unchanging until the user installs an updated version of your application package, either the reference data must be valid for the foreseeable future or you will need to 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 you end up with two copies of the data in storage. An alternative is to keep the reference data as is but keep modifications in a file or database, and 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 reference URLs that were deleted by the user. 23 CHAPTER 23: Accessing Files 238 In the Files/Static sample project, you will find a reworking of the list box example from Chapter 7, 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"?> <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: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: CHAPTER 23: Accessing Files 239 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 (int i=0;i<words.getLength();i++) { items.add(((Element)words.item(i)).getAttribute("value")); } in.close(); } catch (Throwable t) { Toast .makeText(this, "Exception: "+t.toString(), 2000) .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()); } } 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 23–1, since the list of words is the same, just relocated. CHAPTER 23: Accessing Files 240 Figure 23–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, as discussed in Chapter 20. 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 or 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() or openFileOutput() on your Activity or other Context to get an InputStream or OutputStream, respectively. From that point forward, it is not much different from regular Java I/O logic: Wrap those streams as needed, such as 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 reading 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 described in Chapter 27. Note that openFileInput() and openFileOutput() do not accept file paths (e.g., path/to/file.txt), just simple filenames. CHAPTER 23: Accessing Files 241 Here 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"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical"> <Button android:id="@+id/close" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Close" /> <EditText android:id="@+id/editor" android:layout_width="fill_parent" android:layout_height="fill_parent" android:singleLine="false" android:gravity="top" /> </LinearLayout> All we have here is a large text-editing widget, with a Close button above it. The Java is only slightly more complicated: package com.commonsware.android.readwrite; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.Toast; import java.io.BufferedReader; import java.io.File; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; public class ReadWriteFileDemo extends Activity { private final static String NOTES="notes.txt"; private EditText editor; @Override public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main); editor=(EditText)findViewById(R.id.editor); Button btn=(Button)findViewById(R.id.close); btn.setOnClickListener(new Button.OnClickListener() { public void onClick(View v) { finish(); } }); CHAPTER 23: Accessing Files 242 } public void onResume() { super.onResume(); try { InputStream in=openFileInput(NOTES); if (in!=null) { InputStreamReader tmp=new InputStreamReader(in); BufferedReader reader=new BufferedReader(tmp); String str; StringBuffer buf=new StringBuffer(); while ((str = reader.readLine()) != null) { buf.append(str+"\n"); } 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(), 2000) .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(), 2000) .show(); } } } CHAPTER 23: Accessing Files 243 First, we wire up the button to close our activity when clicked by using setOnClickListener() to invoke finish() on the activity. Next, 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. Finally, we hook into onPause(), so we get control as our activity is hidden by another activity or closed, such as via our Close 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 23–2 and 23–3. Whatever is typed in will remain until deleted, surviving our activity being closed, the phone being turned off, or similar situations. Figure 23–2. The ReadWriteFileDemo sample application, as initially launched CHAPTER 23: Accessing Files 244 Figure 23–3. The same application, after entering some text You are also welcome to read and write files on external storage (a.k.a., the SD card). Use Environment.getExternalStorageDirectory() to obtain a File object at the root of the SD card. Starting with Android 1.6, you will also need to hold permissions to work with external storage (e.g., WRITE_EXTERNAL_STORAGE). Permissions are covered in Chapter 28. Bear in mind that external storage is accessible by all applications, whereas openFileInput() and openFileOutput() are in an application-private area. . com.commonsware.android.readwrite; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.Toast;. in=getResources().openRawResource(R.raw.words); DocumentBuilder builder=DocumentBuilderFactory .newInstance() .newDocumentBuilder(); Document doc= builder.parse(in, null); NodeList words =doc. getElementsByTagName("word");. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical">