Professional Android 4 Application Development (2012)
Chapter 7. Files, Saving State,and Preferences
What's in this Chapter?
Persisting simple application data using Shared Preferences
Saving Activity instance data between sessions
Managing application preferences and building Preference Screens
Saving and loading files and managing the local filesystem
Including static files as external resources
This chapter introduces some of the simplest and most versatile data-persistence techniques in Android: Shared Preferences, instance-state Bundles, and local files.
Saving and loading data is essential for most applications. At a minimum, an Activity should save its user interface (UI) state before it becomes inactive to ensure the same UI is presented when it restarts. It's also likely that you'll need to save user preferences and UI selections.
Android's nondeterministic Activity and application lifetimes make persisting UI state and application data between sessions particularly important, as your application process may have been killed and restarted before it returns to the foreground. Android offers several alternatives for saving application data, each optimized to fulfill a particular need.
Shared Preferences are a simple, lightweight name/value pair (NVP) mechanism for saving primitive application data, most commonly a user's application preferences. Android also offers a mechanism for recording application state within the Activity lifecycle handlers, as well as for providing access to the local filesystem, through both specialized methods and the java.io classes.
Android also offers a rich framework for user preferences, allowing you to create settings screens consistent with the system settings.
Saving Simple Application Data
The data-persistence techniques in Android provide options for balancing speed, efficiency, and robustness.
· Shared Preferences—When storing UI state, user preferences, or application settings, you want a lightweight mechanism to store a known set of values. Shared Preferences let you save groups of name/value pairs of primitive data as named preferences.
· Saved application UI state—Activities and Fragments include specialized event handlers to record the current UI state when your application is moved to the background.
· Files—It's not pretty, but sometimes writing to and reading from files is the only way to go. Android lets you create and load files on the device's internal or external media, providing support for temporary caches and storing files in publicly accessible folders.
There are two lightweight techniques for saving simple application data for Android applications: Shared Preferences and a set of event handlers used for saving Activity instance state. Both mechanisms use an NVP mechanism to store simple primitive values. Both techniques support primitive types Boolean, string, float, long, and integer, making them ideal means of quickly storing default values, class instance variables, the current UI state, and user preferences.
Creating and Saving Shared Preferences
Using the SharedPreferences class, you can create named maps of name/value pairs that can be persisted across sessions and shared among application components running within the same application sandbox.
To create or modify a Shared Preference, call getSharedPreferences on the current Context, passing in the name of the Shared Preference to change.
SharedPreferences mySharedPreferences = getSharedPreferences(MY_PREFS,
Activity.MODE_PRIVATE);
Shared Preferences are stored within the application's sandbox, so they can be shared between an application's components but aren't available to other applications.
To modify a Shared Preference, use the SharedPreferences.Editor class. Get the Editor object by calling edit on the Shared Preferences object you want to change.
SharedPreferences.Editor editor = mySharedPreferences.edit();
Use the put<type> methods to insert or update the values associated with the specified name:
// Store new primitive types in the shared preferences object.
editor.putBoolean("isTrue", true);
editor.putFloat("lastFloat", 1f);
editor.putInt("wholeNumber", 2);
editor.putLong("aNumber", 3l);
editor.putString("textEntryValue", "Not Empty");
To save edits, call apply or commit on the Editor object to save the changes asynchronously or synchronously, respectively.
// Commit the changes.
editor.apply();
The apply method was introduced in Android API level 9 (Android 2.3). Calling it causes a safe asynchronous write of the Shared Preference Editor object to be performed. Because it is asynchronous, it is the preferred technique for saving Shared Preferences.
If you require confi rmation of success or want to support earlier Android releases, you can call the commit method, which blocks the calling thread and returns true once a successful write has completed, or false otherwise.
Retrieving Shared Preferences
Accessing Shared Preferences, like editing and saving them, is done using the getSharedPreferences method.
Use the type-safe get<type> methods to extract saved values. Each getter takes a key and a default value (used when no value has yet been saved for that key.)
// Retrieve the saved values.
boolean isTrue = mySharedPreferences.getBoolean("isTrue", false);
float lastFloat = mySharedPreferences.getFloat("lastFloat", 0f);
int wholeNumber = mySharedPreferences.getInt("wholeNumber", 1);
long aNumber = mySharedPreferences.getLong("aNumber", 0);
String stringPreference =
mySharedPreferences.getString("textEntryValue", "");
You can return a map of all the available Shared Preferences keys values by calling getAll, and check for the existence of a particular key by calling the contains method.
Map<String, ?> allPreferences = mySharedPreferences.getAll();
boolean containsLastFloat = mySharedPreferences.contains("lastFloat");
Creating a Settings Activity for the Earthquake Viewer
In the following example you build an Activity to set application preferences for the earthquake viewer last seen in the previous chapter. The Activity lets users configure settings for a more personalized experience. You'll provide the option to toggle automatic updates, control the frequency of updates, and filter the minimum earthquake magnitude displayed.
Creating your own Activity to control user preferences is considered bad practice. Later in this chapter you'll replace this Activity with a standard settings screen using the Preferences Screen classes.
1. Open the Earthquake project you created in Chapter 6, “Using Internet Resources.” Add new string resources to the res/values/strings.xml file for the labels to be displayed in the Preference Screen. Also, add a string for the new Menu Item that will let users open the Preference Screen:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Earthquake</string>
<string name="quake_feed">
http://earthquake.usgs.gov/eqcenter/catalogs/1day-M2.5.xml
</string>
<string name="menu_update">Refresh Earthquakes</string>
<string name="auto_update_prompt">Auto Update?</string>
<string name="update_freq_prompt">Update Frequency</string>
<string name="min_quake_mag_prompt">Minimum Quake Magnitude</string>
<string name="menu_preferences">Preferences</string>
</resources>
2. Create a new preferences.xml layout resource in the res/layout folder for the Preferences Activity. Include a check box for indicating the “automatic update” toggle, and spinners to select the update rate and magnitude filter:
<?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/auto_update_prompt"
/>
<CheckBox android:id="@+id/checkbox_auto_update"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
/>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/update_freq_prompt"
/>
<Spinner android:id="@+id/spinner_update_freq"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:drawSelectorOnTop="true"
/>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/min_quake_mag_prompt"
/>
<Spinner android:id="@+id/spinner_quake_mag"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:drawSelectorOnTop="true"
/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<Button android:id="@+id/okButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/ok"
/>
<Button android:id="@+id/cancelButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/cancel"
/>
</LinearLayout>
</LinearLayout>
3. Create four array resources in a new res/values/arrays.xml file. They will provide the values to use for the update frequency and minimum magnitude spinners:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="update_freq_options">
<item>Every Minute</item>
<item>5 minutes</item>
<item>10 minutes</item>
<item>15 minutes</item>
<item>Every Hour</item>
</string-array>
<string-array name="magnitude">
<item>3</item>
<item>5</item>
<item>6</item>
<item>7</item>
<item>8</item>
</string-array>
<string-array name="magnitude_options">
<item>3</item>
<item>5</item>
<item>6</item>
<item>7</item>
<item>8</item>
</string-array>
<string-array name="update_freq_values">
<item>1</item>
<item>5</item>
<item>10</item>
<item>15</item>
<item>60</item>
</string-array>
</resources>
4. Create a PreferencesActivity Activity. Override onCreate to inflate the layout you created in step 2, and get references to the check box and both the spinner controls. Then make a call to the populateSpinners stub:
package com.paad.earthquake;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.Spinner;
public class PreferencesActivity extends Activity {
CheckBox autoUpdate;
Spinner updateFreqSpinner;
Spinner magnitudeSpinner;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.preferences);
updateFreqSpinner = (Spinner)findViewById(R.id.spinner_update_freq);
magnitudeSpinner = (Spinner)findViewById(R.id.spinner_quake_mag);
autoUpdate = (CheckBox)findViewById(R.id.checkbox_auto_update);
populateSpinners();
}
private void populateSpinners() {
}
}
5. Fill in the populateSpinners method, using Array Adapters to bind each spinner to its corresponding array:
private void populateSpinners() {
// Populate the update frequency spinner
ArrayAdapter<CharSequence> fAdapter;
fAdapter = ArrayAdapter.createFromResource(this, R.array.update_freq_options,
android.R.layout.simple_spinner_item);
int spinner_dd_item = android.R.layout.simple_spinner_dropdown_item;
fAdapter.setDropDownViewResource(spinner_dd_item);
updateFreqSpinner.setAdapter(fAdapter);
// Populate the minimum magnitude spinner
ArrayAdapter<CharSequence> mAdapter;
mAdapter = ArrayAdapter.createFromResource(this,
R.array.magnitude_options,
android.R.layout.simple_spinner_item);
mAdapter.setDropDownViewResource(spinner_dd_item);
magnitudeSpinner.setAdapter(mAdapter);
}
6. Add public static string values that you'll use to identify the Shared Preference keys you'll use to store each preference value. Update the onCreate method to retrieve the named preference and call updateUIFromPreferences. The updateUIFromPreferences method uses the get<type>methods on the Shared Preference object to retrieve each preference value and apply it to the current UI.
Use the default application Shared Preference object to save your settings values:
public static final String USER_PREFERENCE = "USER_PREFERENCE";
public static final String PREF_AUTO_UPDATE = "PREF_AUTO_UPDATE";
public static final String PREF_MIN_MAG_INDEX = "PREF_MIN_MAG_INDEX";
public static final String PREF_UPDATE_FREQ_INDEX = "PREF_UPDATE_FREQ_INDEX";
SharedPreferences prefs;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.preferences);
updateFreqSpinner = (Spinner)findViewById(R.id.spinner_update_freq);
magnitudeSpinner = (Spinner)findViewById(R.id.spinner_quake_mag);
autoUpdate = (CheckBox)findViewById(R.id.checkbox_auto_update);
populateSpinners();
Context context = getApplicationContext();
prefs = PreferenceManager.getDefaultSharedPreferences(context);
updateUIFromPreferences();
}
private void updateUIFromPreferences() {
boolean autoUpChecked = prefs.getBoolean(PREF_AUTO_UPDATE, false);
int updateFreqIndex = prefs.getInt(PREF_UPDATE_FREQ_INDEX, 2);
int minMagIndex = prefs.getInt(PREF_MIN_MAG_INDEX, 0);
updateFreqSpinner.setSelection(updateFreqIndex);
magnitudeSpinner.setSelection(minMagIndex);
autoUpdate.setChecked(autoUpChecked);
}
7. Still in the onCreate method, add event handlers for the OK and Cancel buttons. The Cancel button should close the Activity, whereas the OK button should call savePreferences first:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.preferences);
updateFreqSpinner = (Spinner)findViewById(R.id.spinner_update_freq);
magnitudeSpinner = (Spinner)findViewById(R.id.spinner_quake_mag);
autoUpdate = (CheckBox)findViewById(R.id.checkbox_auto_update);
populateSpinners();
Context context = getApplicationContext();
prefs = PreferenceManager.getDefaultSharedPreferences(context);
updateUIFromPreferences();
Button okButton = (Button) findViewById(R.id.okButton);
okButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
savePreferences();
PreferencesActivity.this.setResult(RESULT_OK);
finish();
}
});
Button cancelButton = (Button) findViewById(R.id.cancelButton);
cancelButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
PreferencesActivity.this.setResult(RESULT_CANCELED);
finish();
}
});
}
private void savePreferences() {
}
8. Fill in the savePreferences method to record the current preferences, based on the UI selections, to the Shared Preference object:
private void savePreferences() {
int updateIndex = updateFreqSpinner.getSelectedItemPosition();
int minMagIndex = magnitudeSpinner.getSelectedItemPosition();
boolean autoUpdateChecked = autoUpdate.isChecked();
Editor editor = prefs.edit();
editor.putBoolean(PREF_AUTO_UPDATE, autoUpdateChecked);
editor.putInt(PREF_UPDATE_FREQ_INDEX, updateIndex);
editor.putInt(PREF_MIN_MAG_INDEX, minMagIndex);
editor.commit();
}
9. That completes the Preferences Activity. Make it accessible in the application by adding it to the manifest:
<activity android:name=".PreferencesActivity"
android:label="Earthquake Preferences">
</activity>
10. Return to the Earthquake Activity, and add support for the new Shared Preferences file and a Menu Item to display the Preferences Activity. Start by adding the new Menu Item. Override the onCreateOptionsMenu method to include a new item that opens the Preferences Activity and another to refresh the earthquake list:
static final private int MENU_PREFERENCES = Menu.FIRST+1;
static final private int MENU_UPDATE = Menu.FIRST+2;
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
menu.add(0, MENU_PREFERENCES, Menu.NONE, R.string.menu_preferences);
return true;
}
11. Override the onOptionsItemSelected method to display the PreferencesActivity Activity when the new Menu Item is selected. To launch the Preferences Activity, create an explicit Intent, and pass it in to the startActivityForResult method. This will launch the Activity and alert the Earthquake class when the preferences are saved through the onActivityResult handler:
private static final int SHOW_PREFERENCES = 1;
public boolean onOptionsItemSelected(MenuItem item){
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case (MENU_PREFERENCES): {
Intent i = new Intent(this,
PreferencesActivity.class);
startActivityForResult(i, SHOW_PREFERENCES);
return true;
}
}
return false;
}
12. Launch your application and select Preferences from the Activity menu. The Preferences Activity should be displayed, as shown in Figure 7.1.
Figure 7.1
13. All that's left is to apply the preferences to the earthquake functionality. Implementing the automatic updates will be left until Chapter 9, “Working in the Background,” where you'll learn to use Services and background threads. For now you can put the framework in place and apply the magnitude filter. Start by creating a new updateFromPreferences method in the Earthquake Activity that reads the Shared Preference values and creates instance variables for each of them:
public int minimumMagnitude = 0;
public boolean autoUpdateChecked = false;
public int updateFreq = 0;
private void updateFromPreferences() {
Context context = getApplicationContext();
SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(context);
int minMagIndex = prefs.getInt(PreferencesActivity.PREF_MIN_MAG_INDEX, 0);
if (minMagIndex < 0)
minMagIndex = 0;
int freqIndex = prefs.getInt(PreferencesActivity.PREF_UPDATE_FREQ_INDEX, 0);
if (freqIndex < 0)
freqIndex = 0;
autoUpdateChecked = prefs.getBoolean(PreferencesActivity.PREF_AUTO_UPDATE, false);
Resources r = getResources();
// Get the option values from the arrays.
String[] minMagValues = r.getStringArray(R.array.magnitude);
String[] freqValues = r.getStringArray(R.array.update_freq_values);
// Convert the values to ints.
minimumMagnitude = Integer.valueOf(minMagValues[minMagIndex]);
updateFreq = Integer.valueOf(freqValues[freqIndex]);
}
14. Apply the magnitude filter by updating the addNewQuake method from the EarthquakeListFragment to check a new earthquake's magnitude before adding it to the list:
private void addNewQuake(Quake _quake) {
Earthquake earthquakeActivity = (Earthquake)getActivity();
if (_quake.getMagnitude() > earthquakeActivity.minimumMagnitude) {
// Add the new quake to our list of earthquakes.
earthquakes.add(_quake);
}
// Notify the array adapter of a change.
aa.notifyDataSetChanged();
}
15. Return to the Earthquake Activity and override the onActivityResult handler to call updateFromPreferences and refresh the earthquakes whenever the Preferences Activity saves changes. Note that once again you are creating a new Thread on which to execute the earthquake refresh code.
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == SHOW_PREFERENCES)
if (resultCode == Activity.RESULT_OK) {
updateFromPreferences();
FragmentManager fm = getFragmentManager();
final EarthquakeListFragment earthquakeList =
(EarthquakeListFragment)fm.findFragmentById(R.id.EarthquakeListFragment);
Thread t = new Thread(new Runnable() {
public void run() {
earthquakeList.refreshEarthquakes();
}
});
t.start();
}
}
16. Finally, call updateFromPreferences in onCreate of the Earthquake Activity to ensure the preferences are applied when the Activity starts:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
updateFromPreferences();
}
All code snippets in this example are part of the Chapter 7 Earthquake Part 1 project, available for download at www.wrox.com.
Introducing the Preference Framework and the Preference Activity
Android offers an XML-driven framework to create system-style Preference Screens for your applications. By using this framework you can create Preference Activities that are consistent with those used in both native and other third-party applications.
This has two distinct advantages:
· Users will be familiar with the layout and use of your settings screens.
· You can integrate settings screens from other applications (including system settings such as location settings) into your application's preferences.
The preference framework consists of four parts:
· Preference Screen layout—An XML file that defines the hierarchy of items displayed in your Preference screens. It specifies the text and associated controls to display, the allowed values, and the Shared Preference keys to use for each control.
· Preference Activity and Preference Fragment—Extensions of PreferenceActivity and PreferenceFragment respectively, that are used to host the Preference Screens. Prior to Android 3.0, Preference Activities hosted the Preference Screen directly; since then, Preference Screens are hosted by Preference Fragments, which, in turn, are hosted by Preference Activities.
· Preference Header definition—An XML file that defines the Preference Fragments for your application and the hierarchy that should be used to display them.
· Shared Preference Change Listener—An implementation of the OnSharedPreferenceChangeListener class used to listen for changes to Shared Preferences.
Android API level 11 (Android 3.0) introduced significant changes to the preference framework by introducing the concept of Preference Fragments and Preference Headers. This is now the preferred technique for creating Activity Preference screens.
As of the time of writing, Preference Fragments are not included in the support library, restricting their use to devices Android 3.0 and above.
The following sections describe the best practice techniques for creating Activity screens for Android 3.0+ devices, making note of how to achieve similar functionality for older devices.
Defining a Preference Screen Layout in XML
Unlike in the standard UI layout, preference definitions are stored in the res/xml resources folder.
Although conceptually they are similar to the UI layout resources described in Chapter 4, “Building User Interfaces,” Preference Screen layouts use a specialized set of controls designed specifically for preferences. These native preference controls are described in the next section.
Each preference layout is defined as a hierarchy, beginning with a single PreferenceScreen element:
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
</PreferenceScreen>
You can include additional Preference Screen elements, each of which will be represented as a selectable element that will display a new screen when clicked.
Within each Preference Screen you can include any combination of PreferenceCategory and Preference<control> elements. Preference Category elements, as shown in the following snippet, are used to break each Preference Screen into subcategories using a title bar separator:
<PreferenceCategory
android:title="My Preference Category"/>
Figure 7.2 shows the SIM card lock, device administration, and credential storage Preference Categories used on the Security Preference Screen.
Figure 7.2
All that remains is to add the preference controls that will be used to set the preferences. Although the specific attributes available for each preference control vary, each of them includes at least the following four:
· android:key—The Shared Preference key against which the selected value will be recorded.
· android:title—The text displayed to represent the preference.
· android:summary—The longer text description displayed in a smaller font below the title text.
· android:defaultValue—The default value that will be displayed (and selected) if no preference value has been assigned to the associated preference key.
Listing 7.1 shows a sample Preference Screen that includes a Preference Category and CheckBox Preference.
Listing 7.1: A simple Shared Preferences screen
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:title="My Preference Category">
<CheckBoxPreference
android:key="PREF_CHECK_BOX"
android:title="Check Box Preference"
android:summary="Check Box Preference Description"
android:defaultValue="true"
/>
</PreferenceCategory>
</PreferenceScreen>
code snippet PA4AD_Ch07_Preferences/res/xml/userpreferences.xml
When displayed, this Preference Screen will appear as shown in Figure 7.3. You'll learn how to display a Preference Screen later in this chapter.
Figure 7.3
Native Preference Controls
Android includes several preference controls to build your Preference Screens:
· CheckBoxPreference—A standard preference check box control used to set preferences to true or false.
· EditTextPreference—Allows users to enter a string value as a preference. Selecting the preference text at run time will display a text-entry dialog.
· ListPreference—The preference equivalent of a spinner. Selecting this preference will display a dialog box containing a list of values from which to select. You can specify different arrays to contain the display text and selection values.
· MultiSelectListPreference—Introduced in Android 3.0 (API level 11), this is the preference equivalent of a check box list.
· RingtonePreference—A specialized List Preference that presents the list of available ringtones for user selection. This is particularly useful when you're constructing a screen to configure notification settings.
You can use each preference control to construct your Preference Screen hierarchy. Alternatively, you can create your own specialized preference controls by extending the Preference class (or any of the subclasses listed above).
You can find further details about preference controls at http://developer.android.com/reference/android/preference/Preference.html.
Using Intents to Import System Preferences into Preference Screens
In addition to including your own Preference Screens, preference hierarchies can include Preference Screens from other applications—including system preferences.
You can invoke any Activity within your Preference Screen using an Intent. If you add an Intent node within a Preference Screen element, the system will interpret this as a request to call startActivity using the specified action. The following XML snippet adds a link to the system display settings:
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
android:title="Intent preference"
android:summary="System preference imported using an intent">
<intent android:action="android.settings.DISPLAY_SETTINGS "/>
</PreferenceScreen>
The android.provider.Settings class includes a number of android.settings.* constants that can be used to invoke the system settings screens. To make your own Preference Screens available for invocation using this technique, simply add an Intent Filter to the manifest entry for the host Preference Activity (described in detail in the following section):
<activity android:name=".UserPreferences" android:label="My User Preferences">
<intent-filter>
<action android:name="com.paad.myapp.ACTION_USER_PREFERENCE" />
</intent-filter>
</activity>
Introducing the Preference Fragment
Since Android 3.0, the PreferenceFragment class has been used to host the preference screens defined by Preferences Screen resources. To create a new Preference Fragment, extend the PreferenceFragment class, as follows:
public class MyPreferenceFragment extends PreferenceFragment
To inflate the preferences, override the onCreate handler and call addPreferencesFromResource, as shown here:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.userpreferences);
}
Your application can include several different Preference Fragments, which will be grouped according to the Preference Header hierarchy and displayed within a Preference Activity, as described in the following sections.
Defining the Preference Fragment Hierarchy Using Preference Headers
Preference headers are XML resources that describe how your Preference Fragments should be grouped and displayed within a Preference Activity. Each header identifies and allows you to select a particular Preference Fragment.
The layout used to display the headers and their associated Fragments can vary depending on the screen size and OS version. Figure 7.4 shows examples of how the same Preference Header definition is displayed on a phone and tablet.
Figure 7.4
Preference Headers are XML resources stored in the res/xml folder of your project hierarchy. The resource ID for each header is the filename (without extension).
Each header must be associated with a particular Preference Fragment that will be displayed when its header is selected. You must also specify a title and, optionally, a summary and icon resource to represent each Fragment and the Preference Screen it contains, as shown in Listing 7.2.
Listing 7.2: Defining a Preference Headers resource
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
<header android:fragment="com.paad.preferences.MyPreferenceFragment"
android:icon="@drawable/preference_icon"
android:title="My Preferences"
android:summary="Description of these preferences" />
</preference-headers>
code snippet PA4AD_Ch07_Preferences/res/xml/preferenceheaders.xml
Like Preference Screens, you can invoke any Activity within your Preference Headers using an Intent. If you add an Intent node within a header element, as shown in the following snippet, the system will interpret this as a request to call startActivity using the specified action:
<header android:icon="@drawable/ic_settings_display"
android:title="Intent"
android:summary="Launches an Intent.">
<intent android:action="android.settings.DISPLAY_SETTINGS "/>
</header>
Introducing the Preference Activity
The PreferenceActivity class is used to host the Preference Fragment hierarchy defined by a preference headers resource. Prior to Android 3.0, the Preference Activity was used to host Preference Screens directly. For applications that target devices prior to Android 3.0, you may still need to use the Preference Activity in this way.
To create a new Preference Activity, extend the PreferenceActivity class as follows:
public class MyFragmentPreferenceActivity extends PreferenceActivity
When using Preference Fragments and headers, override the onBuildHeaders handler, calling loadHeadersFromResource and specifying your preference headers resource file:
public void onBuildHeaders(List<Header> target) {
loadHeadersFromResource(R.xml.userpreferenceheaders, target);
}
For legacy applications, you can inflate the Preference Screen directly in the same way as you would from a Preference Fragment—by overriding the onCreate handler and calling addPreferencesFromResource, specifying the Preference Screen layout XML resource to display within that Activity:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.userpreferences);
}
Like all Activities, the Preference Activity must be included in the application manifest:
<activity android:name=".MyPreferenceActivity"
android:label="My Preferences">
</activity>
To display the application settings hosted in this Activity, open it by calling startActivity or startActivityForResult:
Intent i = new Intent(this, MyPreferenceActivity.class);
startActivityForResult(i, SHOW_PREFERENCES);
Backward Compatibility and Preference Screens
As noted earlier, the Preference Fragment and associated Preference Headers are not supported on Android platforms prior to Android 3.0 (API level 11). As a result, if you want to create applications that support devices running on both pre- and post-Honeycomb devices, you need to implement separate Preference Activities to support both, and launch the appropriate Activity at run time, as shown in Listing 7.3.
Listing 7.3: Runtime selection of pre- or post-Honeycomb Preference Activities
Class c = Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB ?
MyPreferenceActivity.class : MyFragmentPreferenceActivity.class;
Intent i = new Intent(this, c);
startActivityForResult(i, SHOW_PREFERENCES);
code snippet PA4AD Ch07_Preferences/src/MyActivity.java
Finding and Using the Shared Preferences Set by Preference Screens
The Shared Preference values recorded for the options presented in a Preference Activity are stored within the application's sandbox. This lets any application component, including Activities, Services, and Broadcast Receivers, access the values, as shown in the following snippet:
Context context = getApplicationContext();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
// TODO Retrieve values using get<type> methods.
Introducing On Shared Preference Change Listeners
The onSharedPreferenceChangeListener can be implemented to invoke a callback whenever a particular Shared Preference value is added, removed, or modified.
This is particularly useful for Activities and Services that use the Shared Preference framework to set application preferences. Using this handler, your application components can listen for changes to user preferences and update their UIs or behavior, as required.
Register your On Shared Preference Change Listeners using the Shared Preference you want to monitor:
public class MyActivity extends Activity implements
OnSharedPreferenceChangeListener {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Register this OnSharedPreferenceChangeListener
SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(this);
prefs.registerOnSharedPreferenceChangeListener(this);
}
public void onSharedPreferenceChanged(SharedPreferences prefs,
String key) {
// TODO Check the shared preference and key parameters
// and change UI or behavior as appropriate.
}
}
Creating a Standard Preference Activity for the Earthquake Viewer
Previously in this chapter you created a custom Activity to let users modify the application settings for the earthquake viewer. In this example you replace this custom Activity with the standard application settings framework described in the previous section.
This example describes two ways of creating a Preference Activity—first, using the legacy PreferencesActivity, and then a backward-compatible alternative using the newer PreferenceFragment techniques.
1. Start by creating a new XML resource folder at res/xml. Within it create a new userpreferences.xml file. This file will define the settings UI for your earthquake application settings. Use the same controls and data sources as in the previous Activity, but this time create them using the standard application settings framework. Note that in this example difference key names are selected. This is because where you were previously recording integers, you're now recording strings. To avoid type mismatches when the application attempts to read the saved preferences, use a different key name.
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<CheckBoxPreference
android:key="PREF_AUTO_UPDATE"
android:title="Auto refresh"
android:summary="Select to turn on automatic updating"
android:defaultValue="true"
/>
<ListPreference
android:key="PREF_UPDATE_FREQ"
android:title="Refresh frequency"
android:summary="Frequency at which to refresh earthquake list"
android:entries="@array/update_freq_options"
android:entryValues="@array/update_freq_values"
android:dialogTitle="Refresh frequency"
android:defaultValue="60"
/>
<ListPreference
android:key="PREF_MIN_MAG"
android:title="Minimum magnitude"
android:summary="Select the minimum magnitude earthquake to report"
android:entries="@array/magnitude_options"
android:entryValues="@array/magnitude"
android:dialogTitle="Magnitude"
android:defaultValue="3"
/>
</PreferenceScreen>
2. Open the PreferencesActivity Activity and modify its inheritance to extend PreferenceActivity:
public class PreferencesActivity extends PreferenceActivity {
3. The Preference Activity will handle the controls used in the UI, so you can remove the variables used to store the check box and spinner objects. You can also remove the populateSpinners, updateUIFromPreferences, and savePreferences methods. Update the preference name strings to match those used in the user preferences definition in step 1.
public static final String PREF_MIN_MAG = "PREF_MIN_MAG";
public static final String PREF_UPDATE_FREQ = "PREF_UPDATE_FREQ";
4. Update onCreate by removing all the references to the UI controls and the OK and Cancel buttons. Instead of using these, inflate the userpreferences.xml file you created in step 1:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.userpreferences);
}
5. Open the Earthquake Activity and update the updateFromPreferencesMethod. Using this technique, the selected value itself is stored in the preferences, so there's no need to perform the array lookup steps.
private void updateFromPreferences() {
Context context = getApplicationContext();
SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(context);
minimumMagnitude =
Integer.parseInt(prefs.getString(PreferencesActivity.PREF_MIN_MAG, "3"));
updateFreq =
Integer.parseInt(prefs.getString(PreferencesActivity.PREF_UPDATE_FREQ, "60"));
autoUpdateChecked = prefs.getBoolean(PreferencesActivity.PREF_AUTO_UPDATE, false);
}
6. Update the onActivityResult handler to remove the check for the return value. Using this mechanism, all changes to user preferences are applied as soon as they are made.
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode,
data);
if (requestCode == SHOW_PREFERENCES)
updateFromPreferences();
FragmentManager fm = getFragmentManager();
EarthquakeListFragment earthquakeList =
(EarthquakeListFragment)
fm.findFragmentById(R.id.EarthquakeListFragment);
Thread t = new Thread(new Runnable() {
public void run() {
earthquakeList.refreshEarthquakes();
}
});
t.start();
}
7. If you run your application and select the Preferences Menu Item, your new “native” settings screen should be visible, as shown in Figure 7.5.
Figure 7.5
Now create an backward-compatible alternative implementation using the newer Preference Fragments and Preference Headers.
1. Start by creating a new UserPreferenceFragment class that extends the Preference Fragment:
public class UserPreferenceFragment extends PreferenceFragment
2. Override its onCreate handler to populate the Fragment with the Preference screen, as you did in step 4 above to populate the legacy Preference Activity:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.userpreferences);
}
3. Add your Preference Fragment to a new preference_headers.xml file in the res/xml folder.
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
<header android:fragment="com.paad.earthquake.UserPreferenceFragment"
android:title="Settings"
android:summary="Earthquake Refresh Settings" />
</preference-headers>
4. Make a copy of the PreferencesActivity class, naming the copy FragmentPreferences:
public class FragmentPreferences extends PreferenceActivity
5. Add the new User Fragment Preferences Activity to the application manifest.
<activity android:name=".FragmentPreferences"/>
6. Open the User Fragment Preferences Activity and remove the onCreate handler completely. Instead, override the onBuildHeaders method, inflating the headers you defined in step 3:
@Override
public void onBuildHeaders(List<Header> target) {
loadHeadersFromResource(R.xml.preference_headers, target);
}
7. Finally, open the Earthquake Activity and modify the onOptionsItemSelected method to select the appropriate Preference Activity. Create an explicit Intent based on the host platform version and pass it in to the startActivityForResult method:
private static final int SHOW_PREFERENCES = 1;
public boolean onOptionsItemSelected(MenuItem item){
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case (MENU_PREFERENCES): {
Class c = Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB ?
PreferencesActivity.class : FragmentPreferences.class;
Intent i = new Intent(this, c);
startActivityForResult(i, SHOW_PREFERENCES);
return true;
}
}
return false;
}
All the code snippets in this example are part of the Chapter 7 Earthquake Part 2 project, available for download at www.wrox.com.
Persisting the Application Instance State
To save Activity instance variables, Android offers two specialized variations of Shared Preferences. The first uses a Shared Preference named specifically for your Activity, whereas the other relies on a series of lifecycle event handlers.
Saving Activity State Using Shared Preferences
If you want to save Activity information that doesn't need to be shared with other components (e.g., class instance variables), you can call Activity.getPreferences()without specifying a Shared Preferences name. This returns a Shared Preference using the calling Activity's class name as the Shared Preference name.
// Create or retrieve the activity preference object.
SharedPreferences activityPreferences =
getPreferences(Activity.MODE_PRIVATE);
// Retrieve an editor to modify the shared preferences.
SharedPreferences.Editor editor = activityPreferences.edit();
// Retrieve the View
TextView myTextView = (TextView)findViewById(R.id.myTextView);
// Store new primitive types in the shared preferences object.
editor.putString("currentTextValue",
myTextView.getText().toString());
// Commit changes.
editor.apply();
Saving and Restoring Activity Instance State Using the Lifecycle Handlers
Activities offer the onSaveInstanceState handler to persist data associated with UI state across sessions. It's designed specifically to persist UI state should an Activity be terminated by the run time, either in an effort to free resources for foreground applications or to accommodate restarts caused by hardware configuration changes.
If an Activity is closed by the user (by pressing the Back button), or programmatically with a call to finish, the instance state bundle will not be passed in to onCreate or onRestoreInstanceState when the Activity is next created. Data that should be persisted across user sessions should be stored using Shared Preferences, as described in the previous sections.
By overriding an Activity's onSaveInstanceState event handler, you can use its Bundle parameter to save UI instance values. Store values using the same put methods as shown for Shared Preferences, before passing the modified Bundle into the superclass's handler:
private static final String TEXTVIEW_STATE_KEY = "TEXTVIEW_STATE_KEY";
@Override
public void onSaveInstanceState(Bundle saveInstanceState) {
// Retrieve the View
TextView myTextView = (TextView)findViewById(R.id.myTextView);
// Save its state
saveInstanceState.putString(TEXTVIEW_STATE_KEY,
myTextView.getText().toString());
super.onSaveInstanceState(saveInstanceState);
}
This handler will be triggered whenever an Activity completes its active lifecycle, but only when it's not being explicitly finished (with a call to finish). As a result, it's used to ensure a consistent Activity state between active lifecycles of a single user session.
The saved Bundle is passed in to the onRestoreInstanceState and onCreate methods if the application is forced to restart during a session.
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
TextView myTextView = (TextView)findViewById(R.id.myTextView);
String text = "";
if (savedInstanceState != null &&
savedInstanceState.containsKey(TEXTVIEW_STATE_KEY))
text = savedInstanceState.getString(TEXTVIEW_STATE_KEY);
myTextView.setText(text);
}
Saving and Restoring Fragment Instance State Using the Lifecycle Handlers
The UI for most applications will be encapsulated within Fragments. Accordingly, Fragments also include an onSaveInstanceState handler that works in much the same way as its Activity counterpart.
The instance state persisted in the bundle is passed as a parameter to the Fragment's onCreate, onCreateView, and onActivityCreated handlers.
If an Activity is destroyed and restarted to handle a hardware configuration change, such as the screen orientation changing, you can request that your Fragment instance be retained. By calling setRetainInstance within a Fragment's onCreate handler, you specify that Fragment's instance should not be killed and restarted when its associated Activity is re-created.
As a result, the onDestroy and onCreate handlers for a retained Fragment will not be called when the device configuration changes and the attached Activity is destroyed and re-created. This can provide a significant efficiency improvement if you move the majority of your object creation intoonCreate, while using onCreateView to update the UI with the values stored within those persisted instance values.
Note that the rest of the Fragment's lifecycle handlers, including onAttach, onCreateView, onActivityCreated, onStart, onResume, and their corresponding tear-down handlers, will still be called.
Listing 7.4 shows how to use the lifecycle handlers to record the current UI state while taking advantage of the efficiency gains associated with retaining the Fragment instance.
Listing 7.4: Persisting UI state by using lifecycle handlers and retaining Fragment instances
public class MyFragment extends Fragment {
private static String USER_SELECTION = "USER_SELECTION";
private int userSelection = 0;
private TextView tv;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
if (savedInstanceState != null)
userSelection = savedInstanceState.getInt(USER_SELECTION);
}
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.mainfragment, container, false);
tv = (TextView)v.findViewById(R.id.text);
setSelection(userSelection);
Button b1 = (Button)v.findViewById(R.id.button1);
Button b2 = (Button)v.findViewById(R.id.button2);
Button b3 = (Button)v.findViewById(R.id.button3);
b1.setOnClickListener(new OnClickListener() {
public void onClick(View arg0) {
setSelection(1);
}
});
b2.setOnClickListener(new OnClickListener() {
public void onClick(View arg0) {
setSelection(2);
}
});
b3.setOnClickListener(new OnClickListener() {
public void onClick(View arg0) {
setSelection(3);
}
});
return v;
}
private void setSelection(int selection) {
userSelection = selection;
tv.setText("Selected: " + selection);
}
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putInt(USER_SELECTION, userSelection);
super.onSaveInstanceState(outState);
}
}
code snippet PA4AD_Ch07_Preferences/src/MyFragment.java
Including Static Files as Resources
If your application requires external file resources, you can include them in your distribution package by placing them in the res/raw folder of your project hierarchy.
To access these read-only file resources, call the openRawResource method from your application's Resource object to receive an InputStream based on the specified file. Pass in the filename (without the extension) as the variable name from the R.raw class, as shown in the following skeleton code:
Resources myResources = getResources();
InputStream myFile = myResources.openRawResource(R.raw.myfilename);
Adding raw files to your resources hierarchy is an excellent alternative for large, preexisting data sources (such as dictionaries) for which it's not desirable (or even possible) to convert them into Android databases.
Android's resource mechanism lets you specify alternative resource files for different languages, locations, and hardware configurations. For example, you could create an application that loads a different dictionary resource based on the user's language settings.
Working with the File System
It's good practice to use Shared Preferences or a database to store your application data, but there may still be times when you'll want to use files directly rather than rely on Android's managed mechanisms—particularly when working with multimedia files.
File-Management Tools
Android supplies some basic file-management tools to help you deal with the file system. Many of these utilities are located within the java.io.File package.
Complete coverage of Java file-management utilities is beyond the scope of this book, but Android does supply some specialized utilities for file management that are available from the application Context.
· deleteFile—Enables you to remove files created by the current application
· fileList—Returns a string array that includes all the files created by the current application
These methods are particularly useful for cleaning up temporary files left behind if your application crashes or is killed unexpectedly.
Using Application-Specific Folders to Store Files
Many applications will create or download files that are specific to the application. There are two options for storing these application-specific files: internally or externally.
When referring to the external storage, we refer to the shared/media storage that is accessible by all applications and can typically be mounted to a computer file system when the device is connected via USB. Although it is typically located on the SD Card, some devices implement this as a separate partition on the internal storage.
The most important thing to remember when storing files on external storage is that no security is enforced on files stored here. Any application can access, overwrite, or delete files stored on the external storage.
It's also important to remember that files stored on external storage may not always be available. If the SD Card is ejected, or the device is mounted for access via a computer, your application will be unable to read (or create) files on the external storage.
Android offers two corresponding methods via the application Context, getDir and getExternalFilesDir, both of which return a File object that contains the path to the internal and external application file storage directory, respectively.
All files stored in these directories or the subfolders will be erased when your application is uninstalled.
The getExternalFilesDir method was introduced in Android API level 8 (Android 2.2). To support earlier platform releases, you can call Environment.getExternalStorageDirectory to return a path to the root of the external storage.
It's good practice to store your application-specific data in its own subdirectory using the same style as getExternalFilesDir—that is, /Android/data/[Your Package Name]/files.
Note that this work-around will not automatically delete your application fi les when it is uninstalled.
Both of these methods accept a string parameter that can be used to specify the subdirectory into which you want to place your files. In Android 2.2 (API level 8) the Environment class introduced a number of DIRECTORY_[Category] string constants that represent standard directory names, including downloads, images, movies, music, and camera files.
Files stored in the application folders should be specific to the parent application and are typically not detected by the media-scanner, and therefore won't be added to the Media Library automatically. If your application downloads or creates files that should be added to the Media Library or otherwise made available to other applications, consider putting them in the public external storage directory, as described later in this chapter.
Creating Private Application Files
Android offers the openFileInput and openFileOutput methods to simplify reading and writing streams from and to files stored in the application's sandbox.
String FILE_NAME = "tempfile.tmp";
// Create a new output file stream that's private to this application.
FileOutputStream fos = openFileOutput(FILE_NAME, Context.MODE_PRIVATE);
// Create a new file input stream.
FileInputStream fis = openFileInput(FILE_NAME);
These methods support only those files in the current application folder; specifying path separators will cause an exception to be thrown.
If the filename you specify when creating a FileOutputStream does not exist, Android will create it for you. The default behavior for existing files is to overwrite them; to append an existing file, specify the mode as Context.MODE_APPEND.
By default, files created using the openFileOutput method are private to the calling application—a different application will be denied access. The standard way to share a file between applications is to use a Content Provider. Alternatively, you can specify either Context.MODE_WORLD_READABLE orContext.MODE_WORLD_WRITEABLE when creating the output file, to make it available in other applications, as shown in the following snippet:
String OUTPUT_FILE = "publicCopy.txt";
FileOutputStream fos = openFileOutput(OUTPUT_FILE, Context.MODE_WORLD_WRITEABLE);
You can find the location of files stored in your sandbox by calling getFilesDir. This method will return the absolute path to the files created using openFileOutput:
File file = getFilesDir();
Log.d("OUTPUT_PATH_", file.getAbsolutePath());
Using the Application File Cache
Should your application need to cache temporary files, Android offers both a managed internal cache, and (since Android API level 8) an unmanaged external cache. You can access them by calling the getCacheDir and getExternalCacheDir methods, respectively, from the current Context.
Files stored in either cache location will be erased when the application is uninstalled. Files stored in the internal cache will potentially be erased by the system when it is running low on available storage; files stored on the external cache will not be erased, as the system does not track available storage on external media.
In either case it's good form to monitor and manage the size and age of your cache, deleting files when a reasonable maximum cache size is exceeded.
Storing Publicly Readable Files
Android 2.2 (API level 8) also includes a convenience method, Environment.getExternalStoragePublicDirectory, that can be used to find a path in which to store your application files. The returned location is where users will typically place and manage their own files of each type.
This is particularly useful for applications that provide functionality that replaces or augments system applications, such as the camera, that store files in standard locations.
The getExternalStoragePublicDirectory method accepts a String parameter that determines which subdirectory you want to access using a series of Environment static constants:
· DIRECTORY_ALARMS—Audio files that should be available as user-selectable alarm sounds
· DIRECTORY_DCIM—Pictures and videos taken by the device
· DIRECTORY_DOWNLOADS—Files downloaded by the user
· DIRECTORY_MOVIES—Movies
· DIRECTORY_MUSIC—Audio files that represent music
· DIRECTORY_NOTIFICATIONS—Audio files that should be available as user-selectable notification sounds
· DIRECTORY_PICTURES—Pictures
· DIRECTORY_PODCASTS—Audio files that represent podcasts
· DIRECTORY_RINGTONES—Audio files that should be available as user-selectable ringtones
Note that if the returned directory doesn't exit, you must create it before writing files to the directory, as shown in the following snippet:
String FILE_NAME = "MyMusic.mp3";
File path = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_MUSIC);
File file = new File(path, FILE_NAME);
try {
path.mkdirs();
[... Write Files ...]
} catch (IOException e) {
Log.d(TAG, "Error writing " + FILE_NAME, e);
}