52862.fb2
Android has many different ways for you to store data for long-term use by your activity. The simplest to use is the preferences system.
Android allows activities and applications to keep preferences, in the form of key/value pairs (akin to a Map
), that will hang around between invocations of an activity. As the name suggests, the primary purpose is for you to store user-specified configuration details, such as the last feed the user looked at in your feed reader, or what sort order to use by default on a list, or whatever. Of course, you can store in the preferences whatever you like, so long as it is keyed by a String
and has a primitive value (boolean, String
, etc.).
Preferences can either be for a single activity or shared among all activities in an application. Eventually preferences might be shareable across applications, but that is not supported as of the time of this writing.
To get access to the preferences, you have three APIs to choose from:
• getPreferences()
from within your Activity
, to access activity-specific preferences
• getSharedPreferences()
from within your Activity
(or other application Context
), to access application-level preferences
• getDefaultSharedPreferences()
, on PreferencesManager
, to get the shared preferences that work in concert with Android’s overall preference framework
The first two take a security-mode parameter — for now, pass in 0. The getSharedPreferences()
method also takes a name of a set of preferences — getPreferences()
effectively calls getSharedPreferences()
with the activity’s class name as the preference set name. The getDefaultSharedPreferences()
method takes the Context
for the preferences (e.g., your Activity
).
All of those methods return an instance of SharedPreferences
, which offers a series of getters to access named preferences, returning a suitably typed result (e.g., getBoolean()
to return a Boolean
preference). The getters also take a default value, which is returned if there is no preference set under the specified key.
Given the appropriate SharedPreferences
object, you can use edit() to get an “editor” for the preferences. This object has a group of setters that mirror the getters on the parent SharedPreferences
object. It also has the following:
• remove()
to get rid of a single named preference
• clear()
to get rid of all preferences
• commit()
to persist your changes made via the editor
The last one is important — if you modify preferences via the editor and fail to commit()
the changes, those changes will evaporate once the editor goes out of scope.
Conversely, since the preferences object supports live changes, if one part of your application (say, an activity) modifies shared preferences, another part of your application (say, a service) will have access to the changed value immediately.
Beginning with the 0.9 SDK, Android has a framework for managing preferences. This framework does not change anything mentioned previously. Instead, the framework is more for presenting consistent preference-setting options for users so different applications do not have to reinvent the wheel.
The linchpin to the preferences framework is yet another XML data structure. You can describe your application’s preferences in an XML file stored in your project’s res/xml/
directory. Given that, Android can present a pleasant UI for manipulating those preferences, which are then stored in the SharedPreferences
you get back from getDefaultSharedPreferences()
.
The following is the preference XML for the Prefs/Simple
preferences sample project available in the Source Code section at http://apress.com:
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<CheckBoxPreference
android:key="@string/checkbox"
android:title="Checkbox Preference"
android:summary="Check it on, check it off" />
<RingtonePreference
android:key="@string/ringtone"
android:title="Ringtone Preference"
android:showDefault="true"
android:showSilent="true"
android:summary="Pick a tone, any tone" />
</PreferenceScreen>
The root of the preference XML is a PreferenceScreen
element. (I will explain why it is named that later in this chapter; for now, take it on faith that it is a sensible name.) One of the things you can have inside a PreferenceScreen
element, not surprisingly, is preference definitions — subclasses of Preference
, such as CheckBoxPreference
or RingtonePreference
, as shown in the preceding code. As one might expect, these allow you to check a checkbox and choose a ringtone, respectively. In the case of RingtonePreference
, you have the option of allowing users to choose the system-default ringtone or to choose “silence” as a ringtone.
Given that you have set up the preference XML, you can use a nearly built-in activity for allowing your users to set their preferences. The activity is “nearly built-in” because you merely need to subclass it and point it to your preference XML, plus hook the activity into the rest of your application.
So, for example, here is the EditPreferences
activity of the Prefs/Simple
project available on the Apress Web site:
package com.commonsware.android.prefs;
import android.app.Activity;
import android.os.Bundle;
import android.preference.PreferenceActivity;
public class EditPreferences extends PreferenceActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
}
}
As you can see, there is not much to see. All you need to do is call addPreferencesFromResource()
and specify the XML resource containing your preferences. You will also need to add this as an activity to your AndroidManifest.xml
file:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.commonsware.android.prefs">
<application android:label="@string/app_name">
<activity
android:name=".SimplePrefsDemo"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".EditPreferences"
android:label="@string/app_name">
</activity>
</application>
</manifest>
And you will need to arrange to invoke the activity, such as from a menu option, here pulled from SimplePrefsDemo
at http://apress.com:
@Override
public boolean onCreateOptionsMenu (Menu menu) {
menu.add(Menu.NONE, EDIT_ID, Menu.NONE, "Edit Prefs")
.setIcon(R.drawable.misc)
.setAlphabeticShortcut('e');
menu.add(Menu.NONE, CLOSE_ID, Menu.NONE, "Close")
.setIcon(R.drawable.eject)
.setAlphabeticShortcut('c');
return(super.onCreateOptionsMenu(menu));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case EDIT_ID:
startActivity(new Intent(this, EditPreferences.class));
return(true);
case CLOSE_ID:
finish();
return(true);
}
return(super.onOptionsItemSelected(item));
}
However, that is all that is needed, and it really is not that much code outside of the preferences XML. What you get for your effort is an Android-supplied preference UI, as shown in Figure 17-1.
Figure 17-1. The Simple project’s preferences UI
The checkbox can be directly checked or unchecked. To change the ringtone preference, just click on the entry in the preference list to bring up a selection dialog like the one in Figure 17-2.
Figure 17-2. Choosing a ringtone preference
Note that there is no explicit Save or Commit button or menu — changes are persisted as soon as they are made.
The SimplePrefsDemo
activity, beyond having the aforementioned menu, also displays the current preferences via a TableLayout
:
<?xml version="1.0" encoding="utf-8"?>
<TableLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TableRow>
<TextView
android:text="Checkbox:"
android:paddingRight="5px"
/>
<TextView android:id="@+id/checkbox"
/>
</TableRow>
<TableRow>
<TextView
android:text="Ringtone:"
android:paddingRight="5px"
/>
<TextView android:id="@+id/ringtone"
/>
</TableRow>
</TableLayout>
The fields for the table are found in onCreate()
:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
checkbox = (TextView)findViewById(R.id.checkbox);
ringtone = (TextView)findViewById(R.id.ringtone);
}
The fields are updated on each onResume()
:
@Override
public void onResume() {
super.onResume();
SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(this);
checkbox.setText(new Boolean(prefs
.getBoolean("checkbox", false)).toString());
ringtone.setText(prefs.getString("ringtone", "<unset>"));
}
This means the fields will be updated when the activity is opened and after the preferences activity is left (e.g., via the back button); see Figure 17-3.
Figure 17-3. The Simple project’s list of saved preferences
If you have a lot of preferences for users to set, having them all in one big list may become troublesome. Android’s preference framework gives you a few ways to impose a bit of structure on your bag of preferences, including categories and screens.
Categories are added via a PreferenceCategory
element in your preference XML and are used to group together related preferences. Rather than have your preferences all as children of the root PreferenceScreen
, you can put a few PreferenceCategory
elements in the PreferenceScreen
, and then put your preferences in their appropriate categories. Visually, this adds a divider with the category title between groups of preferences.
If you have lots and lots of preferences — more than is convenient for users to scroll through — you can also put them on separate “screens” by introducing the PreferenceScreen
element.
Yes, that PreferenceScreen
element.
Any children of PreferenceScreen
go on their own screen. If you nest PreferenceScreens
, the parent screen displays the screen as a placeholder entry — tapping that entry brings up the child screen. For example, from the Prefs/Structured
sample project on the Apress Web site, here is a preference XML file that contains both PreferenceCategory
and nested PreferenceScreen
elements:
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="Simple Preferences">
<CheckBoxPreference
android:key="@string/checkbox"
android:title="Checkbox Preference"
android:summary="Check it on, check it off"
/>
<RingtonePreference
android:key="@string/ringtone"
android:title="Ringtone Preference"
android:showDefault="true"
android:showSilent="true"
android:summary="Pick a tone, any tone"
/>
</PreferenceCategory>
<PreferenceCategory android:title="Detail Screens">
<PreferenceScreen
android:key="detail"
android:title="Detail Screen"
android:summary="Additional preferences held in another page">
<CheckBoxPreference
android:key="@string/checkbox2"
android:title="Another Checkbox"
android:summary="On. Off. It really doesn't matter."
/>
</PreferenceScreen>
</PreferenceCategory>
</PreferenceScreen>
The result, when you use this preference XML with your PreferenceActivity
implementation, is a categorized list of elements like those in Figure 17-4.
Figure 17-4. The Structured project’s preference UI, showing categories and a screen placeholder
And if you tap on the Detail Screen entry, you are taken to the child preference screen (Figure 17-5).
Figure 17-5. The child preference screen of the Structured project’s preference UI
Of course, not all preferences are checkboxes and ringtones.
For others, like entry fields and lists, Android uses pop-up dialogs. Users do not enter their preference directly in the preference UI activity, but rather tap on a preference, fill in a value, and click OK to commit the change.
Structurally, in the preference XML, fields and lists are not significantly different from other preference types, as seen in this preference XML from the Prefs/Dialogs
sample project available at http://apress.com:
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="Simple Preferences">
<CheckBoxPreference
android:key="@string/checkbox"
android:title="Checkbox Preference"
android:summary="Check it on, check it off"
/>
<RingtonePreference
android:key="@string/ringtone"
android:title="Ringtone Preference"
android:showDefault="true"
android:showSilent="true"
android:summary="Pick a tone, any tone"
/>
</PreferenceCategory>
<PreferenceCategory android:title="Detail Screens">
<PreferenceScreen
android:key="detail"
android:title="Detail Screen"
android:summary="Additional preferences held in another page">
<CheckBoxPreference
android:key="@string/checkbox2"
android:title="Another Checkbox"
android:summary="On. Off. It really doesn't matter."
/>
</PreferenceScreen>
</PreferenceCategory>
<PreferenceCategory android:title="Simple Preferences">
<EditTextPreference
android:key="@string/text"
android:title="Text Entry Dialog"
android:summary="Click to pop up a field for entry"
android:dialogTitle="Enter something useful"
/>
<ListPreference
android:key="@string/list"
android:title="Selection Dialog"
android:summary="Click to pop up a list to choose from"
android:entries="@array/cities"
android:entryValues="@array/airport_codes"
android:dialogTitle="Choose a Pennsylvania city" />
</PreferenceCategory>
</PreferenceScreen>
With the field (EditTextPreference
), in addition to the title and summary you put on the preference itself, you can also supply the title to use for the dialog.
With the list (ListPreference
), you supply both a dialog title and two string-array resources: one for the display names, one for the values. These need to be in the same order — the index of the chosen display name determines which value is stored as the preference in the SharedPreferences
. For example, here are the arrays for use by the ListPreference
shown previously:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="cities">
<item>Philadelphia</item>
<item>Pittsburgh</item>
<item>Allentown/Bethlehem</item>
<item>Erie</item>
<item>Reading</item>
<item>Scranton</item>
<item>Lancaster</item>
<item>Altoona</item>
<item>Harrisburg</item>
</string-array>
<string-array name="airport_codes">
<item>PHL</item>
<item>PIT</item>
<item>ABE</item>
<item>ERI</item>
<item>RDG</item>
<item>AVP</item>
<item>LNS</item>
<item>AOO</item>
<item>MDT</item>
</string-array>
</resources>
When you bring up the preference UI, you start with another category with another pair of preference entries (see Figure 17-6).
Figure 17-6. The preference screen of the Dialogs project’s preference UI
Tapping the Text Entry Dialog preference brings up… a text-entry dialog — in this case, with the prior preference entry pre–filled in (Figure 17-7).
Figure 17-7. Editing a text preference
Tapping the Selection Dialog preference brings up… a selection dialog, showing the display names (Figure 17-8).
Figure 17-8. Editing a list preference
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 pre-packaged 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 it gets put in the Android application APK file as part of the packaging process as a raw resource.
To access this file, you need 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()
— 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 only for static reference data. Moreover, since it is unchanging until the user installs an updated version of your application package, either the reference data has to be valid for the foreseeable future, or you 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 this makes for 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.
In the Files/Static
sample project available in the Source Code section of http://apress.com, you will find a reworking of the listbox example from Chapter 8, this time using a static XML file instead of a hard-wired 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 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 (int i=0; iwords.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 between the Chapter 8 example and this one 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, then pour the value attributes into an ArrayList
for use by the ArrayAdapter
.
The resulting activity looks the same as before (Figure 18-1), since the list of words is the same, just relocated.
Figure 18-1. The StaticFileDemo sample application
Of course, there are even easier ways to have XML files available to you as pre-packaged files, such as by using an XML resource. That is covered in the next chapter. However, while this example uses 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, the process is not much different from using 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()
, they will each access their 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 Chapter 28.
Note that openFileInput()
and openFileOutput()
do not accept file paths (e.g., path/to/file.txt
), just simple filenames.
The following code shows the layout for the world’s most trivial text editor, pulled from the Files/ReadWrite
sample application available on the Apress Web site:
<?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"
/>
</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.files;
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();
}
});
}
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();
}
}
}
First we wire up the button to close out our activity when it’s clicked, by using setOnClickListener()
to invoke finish()
on the activity.
Next we hook into onResume()
so we get control when our editor is coming 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 that 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 gets hidden by another activity or is 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: whatever is typed in will remain until deleted, surviving our activity being closed, the phone being turned off, and similar situations.
Resources are static bits of information held outside the Java source code. You have seen one type of resource — the layout — frequently in the examples in this book. There are many other types of resource, such as images and strings, that you can take advantage of in your Android applications.
Resources are stored as files under the res/
directory in your Android project layout. With the exception of raw resources (res/raw/
), all the other types of resources are parsed for you, either by the Android packaging system or by the Android system on the device or emulator. For example, when you lay out an activity’s UI via a layout resource (res/layout/
), you do not have to parse the layout XML yourself — Android handles that for you.
In addition to layout resources (first seen in Chapter 5) and raw resources (introduced in Chapter 18), there are several other types of resources available to you, including:
• Animations (res/anim/
), designed for short clips as part of a user interface, such as an animation suggesting the turning of a page when a button is clicked
• Images (res/drawable
), for putting static icons or other pictures in an user interface
• Strings, colors, arrays, and dimensions (res/values/
), to both give these sorts of constants symbolic names and to keep them separate from the rest of the code (e.g., for internationalization and localization)
• XML (res/xml/
), for static XML files containing your own data and structure
Keeping your labels and other bits of text outside the main source code of your application is generally considered to be a very good idea. In particular, it helps with internationalization (I18N) and localization (L10N), covered in the section “Different Strokes for Different Folks” later on in this chapter. Even if you are not going to translate your strings to other languages, it is easier to make corrections if all the strings are in one spot instead of scattered throughout your source code.
Android supports regular externalized strings, along with “string formats”, where the string has placeholders for dynamically-inserted information. On top of that, Android supports simple text formatting, called “styled text”, so you can make your words be bold or italic intermingled with normal text.
Generally speaking, all you need to have for plain strings is an XML file in the res/values
directory (typically named res/values/strings.xml
), with a resources root element, and one child string element for each string you wish to encode as a resource. The string element takes a name attribute, which is the unique name for this string, and a single text element containing the text of the string:
<resources>
<string name="quick">The quick brown fox...</string>
<string name="laughs">He who laughs last...</string>
</resources>
The only tricky part is if the string value contains a quote (") or an apostrophe ('). In those cases, you will want to escape those values, by preceding them with a backslash (e.g., These are the times that try men\'s souls
). Or, if it is just an apostrophe, you could enclose the value in quotes (e.g., "These are the times that try men's souls."
).
You can then reference this string from a layout file (as @string/...
, where the ellipsis is the unique name — e.g., @string/laughs
). Or you can get the string from your Java code by calling getString()
with the resource ID of the string resource, that being the unique name prefixed with R.string.
(e.g., getString(R.string.quick)
).
As with other implementations of the Java language, Android’s Dalvik VM supports string formats. Here, the string contains placeholders representing data to be replaced at runtime by variable information (e.g., My name is %1$s
). Plain strings stored as resources can be used as string formats:
String strFormat = getString(R.string.my_name);
String strResult = String.format(strFormat, "Tim");
((TextView)findViewById(R.layout.some_label)).setText(strResult);
If you want really rich text, you should have raw resources containing HTML, then pour those into a WebKit widget. However, for light HTML formatting, using <b>, <i>, and <u>, you can just use a string resource:
<resources>
<string name="b">This has <b>bold</b> in it.</string>
<string name="i">Whereas this has <i>italics</i>!</string>
</resources>
You can access these the same as with plain strings, with the exception that the result of the getString()
call is really an object supporting the android.text.Spanned
interface:
((TextView)findViewById(R.layout.another_label))
.setText(getString(R.string.laughs));
Where styled text gets tricky is with styled string formats, as String.format()
works on String
objects, not Spanned
objects with formatting instructions. If you really want to have styled string formats, here is the workaround:
1. Entity-escape the angle brackets in the string resource (e.g., this is <b>%1$s</b>
).
2. Retrieve the string resource as normal, though it will not be styled at this point (e.g., getString(R.string.funky_format)
).
3. Generate the format results, being sure to escape any string values you substitute in, in case they contain angle brackets or ampersands.
String.format(getString(R.string.funky_format),
TextUtils.htmlEncode(strName));
4. Convert the entity-escaped HTML into a Spanned
object via Html.fromHtml()
.
someTextView.setText(Html
.fromHtml(resultFromStringFormat));
To see this in action, let’s look at the Resources/Strings
demo, which can be found in the Source Code area of http://apress.com. Here is the layout file:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
>
<Button android:id="@+id/format"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_name"
/>
<EditText android:id="@+id/name"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
/>
</LinearLayout>
<TextView android:id="@+id/result"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
/>
</LinearLayout>
As you can see, it is just a button, a field, and a label. The intent is for somebody to enter their name in the field, then click the button to cause the label to be updated with a formatted message containing their name.
The Button in the layout file references a string resource (@string/btn_name
), so we need a string resource file (res/values/strings.xml
):
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">StringsDemo</string>
<string name="btn_name">Name:</string>
<string name="funky_format">My name is <b>%1$s</b></string>
</resources>
The app_name
resource is automatically created by the activityCreator
script. The btn_name
string is the caption of the Button
, while our styled string format is in funky_format
.
Finally, to hook all this together, we need a pinch of Java:
package com.commonsware.android.resources;
import android.app.Activity;
import android.os.Bundle;
import android.text.TextUtils;
import android.text.Html;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
public class StringsDemo extends Activity {
EditText name;
TextView result;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);
name = (EditText)findViewById(R.id.name);
result = (TextView)findViewById(R.id.result);
Button btn = (Button)findViewById(R.id.format);
btn.setOnClickListener(new Button.OnClickListener() {
public void onClick(View v) {
applyFormat();
}
});
}
private void applyFormat() {
String format = getString(R.string.funky_format);
String simpleResult = String.format(format,
TextUtils.htmlEncode(name.getText().toString()));
result.setText(Html.fromHtml(simpleResult));
}
}
The string resource manipulation can be found in applyFormat()
, which is called when the button is clicked. First, we get our format via getString()
— something we could have done at onCreate()
time for efficiency. Next, we format the value in the field using this format, getting a String
back, since the string resource is in entity-encoded HTML. Note the use of TextUtils.htmlEncode()
to entity-encode the entered name, in case somebody decides to use an ampersand or something. Finally, we convert the simple HTML into a styled text object via Html.fromHtml()
and update our label.
When the activity is first launched, we have an empty label (see Figure 19-1).
Figure 19-1. The StringsDemo sample application, as initially launched
However, if we fill in a name and click the button, we get the result seen in Figure 19-2.
Figure 19-2. The same application, after filling in some heroic figure’s name
Android supports images in the PNG, JPEG, and GIF formats. GIF is officially discouraged, however; PNG is the overall preferred format. Images can be used anywhere that requires a Drawable, such as the image and background of an ImageView
.
Using images is simply a matter of putting your image files in res/drawable/
and then referencing them as a resource. Within layout files, images are referenced as @drawable/...
where the ellipsis is the base name of the file (e.g., for res/drawable/foo.png
, the resource name is @drawable/foo
). In Java, where you need an image resource ID, use R.drawable.
plus the base name (e.g., R.drawable.foo
).
If you need a Uri to an image resource, you can use one of two different string formats for the path:
• android.resource://com.example.app/...
, where com.example.app
is the name of the Java package used by your application in AndroidManifest.xml
and ...
is the numeric resource ID for the resource in question (e.g., the value of R.drawable.foo
)
• android.resource://com.example.app/raw/...
, where com.example.app
is the name of the Java package used by your application in AndroidManifest.xml
and ...
is the textual name of the raw resource (e.g., foo
for res/drawable/foo.png
)
Note that Android ships with some image resources built in. Those are addressed in Java with an android.R.drawable
prefix to distinguish them from application-specific resources (e.g., android.R.drawable.picture_frame
).
Let’s update the previous example to use an icon for the button instead of the string resource. This can be found as Resources/Images
. First, we slightly adjust the layout file, using an ImageButton
and referencing a drawable named @drawable/icon
:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
>
<ImageButton android:id="@+id/format"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/icon"
/>
<EditText android:id="@+id/name"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
/>
</LinearLayout>
<TextView android:id="@+id/result"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
/>
</LinearLayout>
Next, we need to put an image file in res/drawable
with a base name of icon. In this case, we use a 32×32 PNG file from the Nuvola[16] icon set. Finally, we twiddle the Java source, replacing our Button
with an ImageButton
:
package com.commonsware.android.resources;
import android.app.Activity;
import android.os.Bundle;
import android.text.TextUtils;
import android.text.Html;
import android.view.View;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.EditText;
import android.widget.TextView;
public class ImagesDemo extends Activity {
EditText name;
TextView result;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);
name = (EditText)findViewById(R.id.name);
result = (TextView)findViewById(R.id.result);
ImageButton btn = (ImageButton)findViewById(R.id.format);
btn.setOnClickListener(new Button.OnClickListener() {
public void onClick(View v) {
applyFormat();
}
});
}
private void applyFormat() {
String format = getString(R.string.funky_format);
String simpleResult = String.format(format,
TextUtils.htmlEncode(name.getText().toString()));
result.setText(Html.fromHtml(simpleResult));
}
}
Now, our button has the desired icon (see Figure 19-3).
Figure 19-3. The ImagesDemo sample application
In Chapter 18, we showed how you can package XML files as raw resources and get access to them for parsing and usage. There is another way of packaging static XML with your application: the XML resource.
Simply put the XML file in res/xml/
, and you can access it by getXml()
on a Resources object, supplying it a resource ID of R.xml.
plus the base name of your XML file. So, in an activity, with an XML file of words.xml
, you could call getResources().getXml(R.xml.words)
.
This returns an instance of the currently-undocumented XmlPullParser
, found in the org.xmlpull.v1
Java namespace. Documentation for this library can be found at the parser’s site[17] as of this writing.
An XML pull parser is event-driven: you keep calling next()
on the parser to get the next event, which could be START_TAG
, END_TAG
, END_DOCUMENT
, etc. On a START_TAG
event, you can access the tag’s name and attributes; a single TEXT
event represents the concatenation of all text nodes that are direct children of this element. By looping, testing, and invoking per-element logic, you parse the file.
To see this in action, let’s rewrite the Java code for the Files/Static
sample project to use an XML resource. This new project, Resources/XML
, requires that you place the words.xml
file from Static
not in res/raw/
, but in res/xml/
. The layout stays the same, so all that needs replacing is the Java source:
package com.commonsware.android.resources;
import android.app.Activity;
import android.os.Bundle;
import android.app.ListActivity;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import java.io.InputStream;
import java.util.ArrayList;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
public class XMLResourceDemo 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 {
XmlPullParser xpp = getResources().getXml(R.xml.words);
while (xpp.getEventType()!=XmlPullParser.END_DOCUMENT) {
if (xpp.getEventType()==XmlPullParser.START_TAG) {
if (xpp.getName().equals("word")) {
items.add(xpp.getAttributeValue(0));
}
}
xpp.next();
}
} catch (Throwable t) {
Toast
.makeText(this, "Request failed: " + t.toString(), 4000).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());
}
}
Now, inside our try...catch
block, we get our XmlPullParser
and loop until the end of the document. If the current event is START_TAG
and the name of the element is word xpp.getName().equals("word"))
, then we get the one-and-only attribute and pop that into our list of items for the selection widget. Since we’re in complete control over the XML file, it is safe enough to assume there is exactly one attribute. But, if you were not as comfortable that the XML is properly defined, you might consider checking the attribute count (getAttributeCount())
and the name of the attribute (getAttributeName()
) before blindly assuming the 0-index attribute is what you think it is.
As you can see in Figure 19-4, the result looks the same as before, albeit with a different name in the title bar.
Figure 19-4. The XMLResourceDemo sample application
In the res/values/
directory, you can place one (or more) XML files describing simple resources: dimensions, colors, and arrays. We have already seen uses of dimensions and colors in previous examples, where they were passed as simple strings (e.g., "10px"
) as parameters to calls. You can, of course, set these up as Java static final objects and use their symbolic names… but this only works inside Java source, not in layout XML files. By putting these values in resource XML files, you can reference them from both Java and layouts, plus have them centrally located for easy editing.
Resource XML files have a root element of resources
; everything else is a child of that root.
Dimensions are used in several places in Android to describe distances, such as a widget’s padding. While this book usually uses pixels (e.g., 10px
for ten pixels), there are several different units of measurement available to you:
• in
and mm
for inches and millimeters, respectively, based on the actual size of the screen
• pt
for points, which in publishing terms is 1/72nd of an inch (again, based on the actual physical size of the screen)
• dp
and sp
for device-independent pixels and scale-independent pixels — one pixel equals one dp for a 160dpi resolution screen, with the ratio scaling based on the actual screen pixel density (scale-independent pixels also take into account the user’s preferred font size)
To encode a dimension as a resource, add a dimen
element, with a name attribute for your unique name for this resource, and a single child text element representing the value:
<resources>
<dimen name="thin">10px</dimen>
<dimen name="fat">1in</dimen>
</resources>
In a layout, you can reference dimensions as @dimen/...
, where the ellipsis is a placeholder for your unique name for the resource (e.g., thin and fat from the previous sample). In Java, you reference dimension resources by the unique name prefixed with R.dimen.
(e.g., Resources.getDimen(R.dimen.thin)
).
Colors in Android are hexadecimal RGB values, also optionally specifying an alpha channel.
You have your choice of single-character hex values or double-character hex values, leaving you with four styles:
• #RGB
• #ARGB
• #RRGGBB
• #AARRGGBB
These work similarly to their counterparts in Cascading Style Sheets (CSS).
You can, of course, put these RGB values as string literals in Java source or layout resources. If you wish to turn them into resources, though, all you need to do is add color elements to the resources file, with a name attribute for your unique name for this color, and a single text element containing the RGB value itself:
<resources>
<color name="yellow_orange">#FFD555</color>
<color name="forest_green">#005500</color>
<color name="burnt_umber">#8A3324</color>
</resources>
In a layout, you can reference colors as @color/...
, replacing the ellipsis with your unique name for the color (e.g., burnt_umber
). In Java, you reference color resources by the unique name prefixed with R.color.
(e.g., Resources.getColor(R.color.forest_green)
).
Array resources are designed to hold lists of simple strings, such as a list of honorifics (Mr., Mrs., Ms., Dr., etc.).
In the resource file, you need one string-array element per array, with a name attribute for the unique name you are giving the array. Then, add one or more child item elements, each of which have a single text element with the value for that entry in the array:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="cities">
<item>Philadelphia</item>
<item>Pittsburgh</item>
<item>Allentown/Bethlehem</item>
<item>Erie</item>
<item>Reading</item>
<item>Scranton</item>
<item>Lancaster</item>
<item>Altoona</item>
<item>Harrisburg</item>
</string-array>
<string-array name="airport_codes">
<item>PHL</item>
<item>PIT</item>
<item>ABE</item>
<item>ERI</item>
<item>RDG</item>
<item>AVP</item>
<item>LNS</item>
<item>AOO</item>
<item>MDT</item>
</string-array>
</resources>
From your Java code, you can then use Resources.getStringArray()
to get a String[]
of the items in the list. The parameter to getStringArray()
is your unique name for the array, prefixed with R.array.
(e.g., Resources.getStringArray(R.array.honorifics)
).
One set of resources may not fit all situations where your application may be used. One obvious area comes with string resources and dealing with internationalization (I18N) and localization (L10N). Putting strings all in one language works fine — probably at least for the developer — but only covers one language.
That is not the only scenario where resources might need to differ, though. Here are others:
• Screen orientation: is the screen in a portrait orientation? Landscape? Is the screen square and, therefore, does not really have an orientation?
• Screen size: how many pixels does the screen have, so you can size your resources accordingly (e.g., large versus small icons)?
• Touchscreen: does the device have a touchscreen? If so, is the touchscreen set up to be used with a stylus or a finger?
• Keyboard: what keyboard does the user have (QWERTY, numeric, neither), either now or as an option?
• Other input: does the device have some other form of input, like a directional pad or click-wheel?
The way Android currently handles this is by having multiple resource directories, with the criteria for each embedded in their names.
Suppose, for example, you want to support strings in both English and Spanish. Normally, for a single-language setup, you would put your strings in a file named res/values/strings.xml
. To support both English and Spanish, you would create two folders, res/values-en
and res/values-es
, where the value after the hyphen is the ISO 639-1[18] two-letter code for the language you want. Your English-language strings would go in res/values-en/strings.xml
and the Spanish ones in res/values-es/strings.xml
. Android will choose the proper file based on the user’s device settings.
Seems easy, right?
Where things start to get complicated is when you need to use multiple disparate criteria for your resources. For example, let us suppose you want to develop both for the T-Mobile G1 and two currently-fictitious devices. One device (the Fictional One) has a VGA screen normally in a landscape orientation (640×480), an always-open QWERTY keyboard, a directional pad, but no touch-screen. The other device (the Fictional Two) has a G1-sized screen (320×480), a numeric keyboard but no QWERTY, a directional pad, and no touch-screen.
You may want to have somewhat different layouts for these devices, to take advantage of different screen real estate and different input options:
• You want different layouts for each combination of resolution and orientation
• You want different layouts for touch-screen devices versus ones without touch-screens
• You want different layouts for QWERTY versus non-QWERTY devices
Once you get into these sorts of situations, though, all sorts of rules come into play, such as:
• The configuration options (e.g., -en
) have a particular order of precedence, and they must appear in the directory name in that order. The Android documentation[19] outlines the specific order in which these options can appear. For the purposes of this example, screen orientation must precede touchscreen type, which must precede screen size.
• There can only be one value of each configuration option category per directory.
• Options are case sensitive.
So, for the scenario described previously, in theory, we would need the following directories:
• res/layout-port-notouch-qwerty-640x480
• res/layout-port-notouch-qwerty-480x320
• res/layout-port-notouch-12key-640x480
• res/layout-port-notouch-12key-480x320
• res/layout-port-notouch-nokeys-640x480
• res/layout-port-notouch-nokeys-480x320
• res/layout-port-stylus-qwerty-640x480
• res/layout-port-stylus-qwerty-480x320
• res/layout-port-stylus-12key-640x480
• res/layout-port-stylus-12key-480x320
• res/layout-port-stylus-nokeys-640x480
• res/layout-port-stylus-nokeys-480x320
• res/layout-port-finger-qwerty-640x480
• res/layout-port-finger-qwerty-480x320
• res/layout-port-finger-12key-640x480
• res/layout-port-finger-12key-480x320
• res/layout-port-finger-nokeys-640x480
• res/layout-port-finger-nokeys-480x320
• res/layout-land-notouch-qwerty-640x480
• res/layout-land-notouch-qwerty-480x320
• res/layout-land-notouch-12key-640x480
• res/layout-land-notouch-12key-480x320
• res/layout-land-notouch-nokeys-640x480
• res/layout-land-notouch-nokeys-480x320
• res/layout-land-stylus-qwerty-640x480
• res/layout-land-stylus-qwerty-480x320
• res/layout-land-stylus-12key-640x480
• res/layout-land-stylus-12key-480x320
• res/layout-land-stylus-nokeys-640x480
• res/layout-land-stylus-nokeys-480x320
• res/layout-land-finger-qwerty-640x480
• res/layout-land-finger-qwerty-480x320
• res/layout-land-finger-12key-640x480
• res/layout-land-finger-12key-480x320
• res/layout-land-finger-nokeys-640x480
• res/layout-land-finger-nokeys-480x320
Don’t panic! We will shorten this list in just a moment!
Note that for many of these, the actual layout files will be identical. For example, we only care about touch-screen layouts being different than the other two layouts, but since we cannot combine those two, we would theoretically have to have separate directories with identical contents for finger
and stylus
.
Also note that there is nothing preventing you from also having a directory with the unadorned base name (res/layout
). In fact, this is probably a good idea, in case future editions of the Android runtime introduce other configuration options you did not consider — having a default layout might make the difference between your application working or failing on that new device.
Now, we can “cheat” a bit, by decoding the rules Android uses for determining which, among a set of candidates, is the “right” resource directory to use:
1. First up, Android tosses out ones that are specifically invalid. So, for example, if the screen size of the device is 320×240, the 640x480 directories would be dropped as candidates, since they specifically call for some other size.
2. Next, Android counts the number of matches for each folder, and only pays attention to those with the most matches.
3. Finally, Android goes in the order of precedence of the options — in other words, it goes from left to right in the directory name.
So we could skate by with only the following configurations:
• res/layout-port-notouch-qwerty-640x480
• res/layout-port-notouch-qwerty
• res/layout-port-notouch-640x480
• res/layout-port-notouch
• res/layout-port-qwerty-640x480
• res/layout-port-qwerty
• res/layout-port-640x480
• res/layout-port
• res/layout-land-notouch-qwerty-640x480
• res/layout-land-notouch-qwerty
• res/layout-land-notouch-640x480
• res/layout-land-notouch
• res/layout-land-qwerty-640x480
• res/layout-land-qwerty
• res/layout-land-640x480
• res/layout-land
Here, we take advantage of the fact that specific matches take precedence over “unspecified” values. So, a device with a QWERTY keyboard will choose a resource with qwerty in the directory over a resource that does not specify its keyboard type. Combine that with the “most matches wins” rule, we see that res/layout-port
will only match devices with 480×320 screens, no QWERTY keyboard, and a touch-screen in portrait orientation.
We could refine this even further, to only cover the specific devices we are targeting (the T-Mobile G1, the Fictional One, and the Fictional Two), plus take advantage of res/layout
being the overall default:
• res/layout-port-notouch-640x480
• res/layout-port-notouch
• res/layout-land-notouch-640x480
• res/layout-land-notouch
• res/layout-land
• res/layout
Here, 640x480
differentiates the Fictional One from the other two devices, while notouch
differentiates the Fictional Two from the T-Mobile G1.
SQLite[20] is a very popular embedded database, as it combines a clean SQL interface with a very small memory footprint and decent speed. Moreover, it is public domain, so everyone can use it. Lots of firms (Adobe, Apple, Google, Sun, Symbian) and open-source projects (Mozilla, PHP, Python) all ship products with SQLite.
For Android, SQLite is “baked into” the Android runtime, so every Android application can create SQLite databases. Since SQLite uses a SQL interface, it is fairly straightforward to use for people with experience in other SQL-based databases. However, its native API is not JDBC, and JDBC might be too much overhead for a memory-limited device like a phone, anyway. Hence, Android programmers have a different API to learn — the good news is that it is not very difficult.
This chapter will cover the basics of SQLite use in the context of working on Android. It by no means is a thorough coverage of SQLite as a whole. If you want to learn more about SQLite and how to use it in environments other than Android, a fine book is The Definitive Guide to SQLite[21] by Mike Owens (Apress, 2006).
Activities will typically access a database via a content provider or service. Therefore, this chapter does not have a full example. You will find a full example of a content provider that accesses a database in Chapter 28.
SQLite, as the name suggests, uses a dialect of SQL for queries (SELECT
), data manipulation (INSERT, et al), and data definition (CREATE TABLE
, et al). SQLite has a few places where it deviates from the SQL-92 standard, no different than most SQL databases. The good news is that SQLite is so space-efficient that the Android runtime can include all of SQLite, not some arbitrary subset to trim it down to size.
The biggest difference from other SQL databases you will encounter is probably the data typing. While you can specify the data types for columns in a CREATE TABLE
statement, and while SQLite will use those as a hint, that is as far as it goes. You can put whatever data you want in whatever column you want. Put a string in an INTEGER column? Sure! No problem! Vice versa? Works too! SQLite refers to this as “manifest typing,” as described in the documentation:[22]
In manifest typing, the datatype is a property of the value itself, not of the column in which the value is stored. SQLite thus allows the user to store any value of any datatype into any column regardless of the declared type of that column.
In addition, there is a handful of standard SQL features not supported in SQLite, notably FOREIGN KEY
constraints, nested transactions, RIGHT OUTER JOIN
and FULL OUTER JOIN
, and some flavors of ALTER TABLE
.
Beyond that, though, you get a full SQL system, complete with triggers, transactions, and the like. Stock SQL statements, like SELECT
, work pretty much as you might expect.
If you are used to working with a major database, like Oracle, you may look upon SQLite as being a “toy” database. Please bear in mind that Oracle and SQLite are meant to solve different problems, and that you will not likely be seeing a full copy of Oracle on a phone any time soon.
No databases are automatically supplied to you by Android. If you want to use SQLite, you have to create your own database, then populate it with your own tables, indexes, and data.
To create and open a database, your best option is to craft a subclass of SQLiteOpenHelper
. This class wraps up the logic to create and upgrade a database, per your specifications, as needed by your application. Your subclass of SQLiteOpenHelper
will need three methods:
• The constructor, chaining upward to the SQLiteOpenHelper
constructor. This takes the Context
(e.g., an Activity
), the name of the database, an optional cursor factory (typically, just pass null
), and an integer representing the version of the database schema you are using.
• onCreate()
, which passes you a SQLiteDatabase
object that you need to populate with tables and initial data, as appropriate.
• onUpgrade()
, which passes you a SQLiteDatabase
object and the old and new version numbers, so you can figure out how best to convert the database from the old schema to the new one. The simplest, albeit least friendly, approach is to simply drop the old tables and create new ones. This is covered in greater detail in Chapter 28.
The rest of this chapter will discuss how you actually create tables, insert data, drop tables, etc., and will show sample code from a SQLiteOpenHelper
subclass.
To use your SQLiteOpenHelper
subclass, create an instance and ask it to getReadableDatabase()
or getWriteableDatabase()
, depending upon whether or not you will be changing its contents:
db = (new DatabaseHelper(getContext())).getWritableDatabase();
return (db == null) ? false : true;
This will return a SQLiteDatabase
instance, which you can then use to query the database or modify its data.
When you are done with the database (e.g., your activity is being closed), simply call close()
on the SQLiteDatabase
to release your connection.
For creating your tables and indexes, you will need to call execSQL()
on your SQLiteDatabase
, providing the DDL statement you wish to apply against the database. Barring a database error, this method returns nothing.
So, for example, you can use the following code:
db.execSQL("CREATE TABLE constants (_id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, value REAL);");
This will create a table, named constants, with a primary key column named _id
that is an auto-incremented integer (i.e., SQLite will assign the value for you when you insert rows), plus two data columns: title
(text) and value
(a float, or “real” in SQLite terms). SQLite will automatically create an index for you on your primary-key column — you could add other indices here via some CREATE INDEX
statements, if you so chose.
Most likely, you will create tables and indexes when you first create the database, or possibly when the database needs upgrading to accommodate a new release of your application. If you do not change your table schemas, you might never drop your tables or indexes, but if you do, just use execSQL()
to invoke DROP INDEX
and DROP TABLE
statements as needed.
Given that you have a database and one or more tables, you probably want to put some data in them and such. You have two major approaches for doing this.
You can always use execSQL()
, just like you did for creating the tables. The execSQL()
method works for any SQL that does not return results, so it can handle INSERT
, UPDATE
, DELETE
, etc. just fine. So, for example you could use this code:
db.execSQL("INSERT INTO widgets (name, inventory)" +
VALUES ('Sprocket', 5));
Your alternative is to use the insert()
, update()
, and delete()
methods on the SQLiteDatabase
object. These are “builder” sorts of methods, in that they break down the SQL statements into discrete chunks, then take those chunks as parameters.
These methods make use of ContentValues
objects, which implement a Map-esque interface, albeit one that has additional methods for working with SQLite types. For example, in addition to get()
to retrieve a value by its key, you have getAsInteger()
, getAsString()
, and so forth.
The insert()
method takes the name of the table, the name of one column as the null column hack, and a ContentValues
with the initial values you want put into this row. The null column hack is for the case where the ContentValues
instance is empty — the column named as the null column hack will be explicitly assigned the value NULL
in the SQL INSERT
statement generated by insert()
.
ContentValues cv = new ContentValues();
cv.put(Constants.TITLE, "Gravity, Death Star I");
cv.put(Constants.VALUE, SensorManager.GRAVITY_DEATH_STAR_I);
db.insert("constants", getNullColumnHack(), cv);
The update()
method takes the name of the table, a ContentValues
representing the columns and replacement values to use, an optional WHERE
clause, and an optional list of parameters to fill into the WHERE
clause, to replace any embedded question marks (?
). Since update()
replaces only columns with fixed values, versus ones computed based on other information, you may need to use execSQL()
to accomplish some ends.
The WHERE
clause and parameter list work akin to the positional SQL parameters you may be used to from other SQL APIs. Consider this example:
// replacements is a ContentValues instance
String[] parms = new String[] {"snicklefritz"};
db.update("widgets", replacements, "name=?", parms);
The delete()
method works akin to update()
, taking the name of the table, the optional WHERE
clause, and the corresponding parameters to fill into the WHERE
clause.
As with INSERT
, UPDATE
, and DELETE
, you have two main options for retrieving data from a SQLite database using SELECT
:
• You can use rawQuery()
to invoke a SELECT
statement directly.
• You can use query()
to build up a query from its component parts.
Confounding matters is the SQLiteQueryBuilder
class and the issue of cursors and cursor factories. Let’s take all of this one piece at a time.
The simplest solution, at least in terms of the API, is rawQuery()
. Simply call it with your SQL SELECT
statement. The SELECT
statement can include positional parameters; the array of these forms your second parameter to rawQuery()
. So, we wind up with this:
Cursor c = db.rawQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='constants'", null);
In this example, we actually query a SQLite system table (sqlite_master
) to see if our constants table already exists. The return value is a Cursor
, which contains methods for iterating over results (see the “Using Cursors” section).
If your queries are pretty much baked into your application, this is a very straightforward way to use them. However, it gets complicated if parts of the query are dynamic, beyond what positional parameters can really handle. For example, if the set of columns you need to retrieve is not known at compile time, puttering around concatenating column names into a comma-delimited list can be annoying — which is where query()
comes in.
The query()
method takes the discrete pieces of a SELECT
statement and builds the query from them. The pieces, in the order they appear as parameters to query()
, are as follows:
1. The name of the table to query against
2. The list of columns to retrieve
3. The WHERE
clause, optionally including positional parameters
4. The list of values to substitute in for those positional parameters
5. The GROUP BY
clause, if any
6. The ORDER BY
clause, if any
7. The HAVING
clause, if any
These can be null
when they are not needed (except the table name, of course):
String[] columns = {"ID", "inventory"};
String[] parms = {"snicklefritz"};
Cursor result = db.query("widgets", columns, "name=?",
parms, null, null, null);
Yet another option is to use SQLiteQueryBuilder
, which offers much richer query-building options, particularly for nasty queries involving things like the union of multiple sub-query results. More importantly, the SQLiteQueryBuilder
interface dovetails nicely with the ContentProvider
interface for executing queries. Hence, a common pattern for your content provider’s query()
implementation is to create a SQLiteQueryBuilder
, fill in some defaults, then allow it to build up (and optionally execute) the full query combining the defaults with what is provided to the content provider on the query request.
For example, here is a snippet of code from a content provider using SQLiteQueryBuilder
:
@Override
public Cursor query(Uri url, String[] projection, String selection,
String[] selectionArgs, String sort) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(getTableName());
if (isCollectionUri(url)) {
qb.setProjectionMap(getDefaultProjection());
} else {
qb.appendWhere(getIdColumnName() + "=" + url.getPathSegments().get(1));
}
String orderBy;
if (TextUtils.isEmpty(sort)) {
orderBy = getDefaultSortOrder();
} else {
orderBy = sort;
}
Cursor c = qb.query(db, projection, selection, selectionArgs,
null, null, orderBy);
c.setNotificationUri(getContext().getContentResolver(), url);
return c;
}
Content providers are explained in greater detail in Part 5 of this book, so some of this you will have to take on faith until then. Here, we see the following:
1. A SQLiteQueryBuilder
is constructed.
2. It is told the table to use for the query (setTables(getTableName())
).
3. It is either told the default set of columns to return (setProjectionMap()
), or is given a piece of a WHERE
clause to identify a particular row in the table by an identifier extracted from the Uri supplied to the query()
call (appendWhere()
).
4. Finally, it is told to execute the query, blending the preset values with those supplied on the call to query()
(qb.query(db, projection, selection, selectionArgs, null, null, orderBy)
).
Instead of having the SQLiteQueryBuilder
execute the query directly, we could have called buildQuery()
to have it generate and return the SQL SELECT
statement we needed, which we could then execute ourselves.
No matter how you execute the query, you get a Cursor
back. This is the Android/SQLite edition of the database cursor, a concept used in many database systems. With the cursor, you can do the following:
• Find out how many rows are in the result set via getCount()
• Iterate over the rows via moveToFirst()
, moveToNext()
, and isAfterLast()
• Find out the names of the columns via getColumnNames()
, convert those into column numbers via getColumnIndex()
, and get values for the current row for a given column via methods like getString()
, getInt()
, etc.
• Re-execute the query that created the cursor, via requery()
• Release the cursor’s resources via close()
For example, here we iterate over the widgets
table entries from the previous snippets:
Cursor result =
db.rawQuery("SELECT ID, name, inventory FROM widgets");
result.moveToFirst();
while (!result.isAfterLast()) {
int id = result.getInt(0);
String name = result.getString(1);
int inventory = result.getInt(2);
// do something useful with these
result.moveToNext();
}
result.close();
There may be circumstances in which you want to use your own Cursor
subclass rather than the stock implementation provided by Android. In those cases, you can use queryWithFactory()
and rawQueryWithFactory()
, which take a SQLiteDatabase.CursorFactory
instance as a parameter. The factory, as one might expect, is responsible for creating new cursors via its newCursor()
implementation.
Finding and implementing a valid use for this facility is left as an exercise for the reader. Suffice it to say that you should not need to create your own cursor classes much, if at all, in ordinary Android development.
If you are used to developing for other databases, you are also probably used to having tools to inspect and manipulate the contents of the database, beyond merely the database’s API. With Android’s emulator, you have two main options for this.
First, the emulator is supposed to bundle in the sqlite3
console program and makes it available from the adb shell
command. Once you are in the emulator’s shell, just execute sqlite3
, providing it the path to your database file. Your database file can be found at the following location:
/data/data/your.app.package/databases/your-db-name
Here your.app.package
is the Java package for your application (e.g., com.commonsware.android
) and your-db-name
is the name of your database, as supplied to createDatabase()
.
Note, however, that the Android 0.9 SDK appears to be missing the sqlite3
command, though it has returned in Android 1.0.
The sqlite3
program works, and if you are used to poking around your tables using a console interface, you are welcome to use it. If you prefer something a little bit friendlier, you can always copy the SQLite database off the device onto your development machine, then use a SQLite-aware client program to putter around. Note, though, that you are working off a copy of the database; if you want your changes to go back to the device, you will need to transfer the database back over to the device.
To get the database off the device, you can use the adb pull
command (or the equivalent in your IDE), which takes the path to the on-device database and the local destination as parameters. To store a modified database on the device, use adb push, which takes the local path to the database and the on-device destination as parameters.
One of the most accessible SQLite clients is the SQLite Manager[23] extension for Firefox (Figure 20-1), as it works across all platforms.
Figure 20-1. The SQLite Manager Firefox extension
You can find other client tools[24] on the SQLite Web site.[25]
Java has as many, if not more, third-party libraries than any other modern programming language. Here, “third-party libraries” refers to the innumerable JARs that you can include in a server or desktop Java application — the things that the Java SDKs themselves do not provide.
In the case of Android, the Dalvik Virtual Machine (Dalvik VM) at its heart is not precisely Java, and what it provides in its SDK is not precisely the same as any traditional Java SDK. That being said, many Java third-party libraries still provide capabilities that Android lacks natively and therefore the ones you can get working with Android’s flavor of Java may be of use to you in your project.
This chapter explains what it will take for you to leverage such libraries, and the limitations on Android’s support for arbitrary third-party code.
Not all available Java code, of course, will work well with Android. There are a number of factors to consider, including the following:
• Expected Platform APIs: Does the code assume a newer JVM than the one Android is based on? Or does the code assume the existence of Java APIs that ship with J2SE but not with Android, such as Swing?
• Size: Existing Java code designed for use on desktops or servers need not worry too much about on-disk size, or even in-RAM size. Android, of course, is short on both. Using third-party Java code, particularly when pre-packaged as JARs, may balloon the size of your application.
• Performance: Does the Java code effectively assume a much more powerful CPU than what you may find on many Android devices? Just because a desktop computer can run it without issue doesn’t mean your average mobile phone will handle it well.
• Interface: Does the Java code assume a console interface? Or is it a pure API that you can wrap your own interface around?
One trick for addressing some of these concerns is to use open-source Java code, and actually work with the code to make it more Android-friendly. For example, if you’re only using 10% of the third-party library, maybe it’s worthwhile to recompile the subset of the project to be only what you need, or at least to remove the unnecessary classes from the JAR. The former approach is safer in that you get compiler help to make sure you’re not discarding some essential piece of code, though it may be more tedious to do.
You have two choices for integrating third-party code into your project: use source code or use pre-packaged JARs.
If you choose to use the third-party source code, all you need to do is copy it into your own source tree (under src/
in your project) so it can sit alongside your existing code, then let the compiler perform its magic.
If you choose to use an existing JAR, perhaps one for which you do not have the source code, you will need to teach your build chain how to use the JAR. If you are using an IDE, that’s a matter of telling it to reference the JAR. If, on the other hand, you are not using an IDE and are relying upon the build.xml
Ant script, put the JAR in the libs/
directory created for you by activityCreator
, and the Ant build process will pick it up.
For example, in a previous draft of this book, we had a MailBuzz project. MailBuzz, as the name suggests, dealt with email. It leveraged the JavaMail APIs and needed two JavaMail JARs: mail-1.4.jar
and activation-1.1.jar
. With both of those in the libs/
directory, the classpath
told javac
to link against those JARs, so any JavaMail references in the MailBuzz code could be correctly resolved. Then, those JARs were listed, along with the MailBuzz compiled classes, in the task that invokes the dex
tool to convert the Java code into Dalvik VM instructions. Without this step, even though your code may compile, it won’t find the JavaMail classes at runtime and will fail with an exception.
As it turned out, though, the Dalvik VM and compiler supplied with the Android 0.9 and newer SDKs no longer supported some Java language features used by JavaMail. And, while the JavaMail source code is available, it is under an open-source license (Common Development and Distribution License; CDDL) that… has issues.
Unlike other mobile-device operating systems, Android has no restrictions on what you can run on it, so long as you can do it in Java using the Dalvik VM. This includes incorporating your own scripting language into your application, something that is expressly prohibited on some other devices.
One possible Java scripting language is BeanShell.[26] BeanShell gives you Java-compatible syntax with implicit typing and no compilation required.
So, to add BeanShell scripting, you need to put the BeanShell interpreter’s JAR file in your libs/
directory. The 2.0b4 JAR available for download from the BeanShell site, unfortunately, does not work out of the box with the Android 0.9 and newer SDKs, perhaps due to the compiler that was used to build it. Instead, you should probably check out the source code from Subversion[27] and execute ant jarcore
to build it, then copy the resulting JAR (in BeanShell’s dist/
directory) to your own project’s libs/
. Or just use the BeanShell JAR that accompanies the source code for this book, in the Java/AndShell
project available in the Source Code area at http://apress.com.
From there, using BeanShell on Android is no different from using BeanShell in any other Java environment:
1. Create an instance of the BeanShell Interpreter
class.
2. Set any globals for the script’s use via Interpreter#set()
.
3. Call Interpreter#eval()
to run the script and, optionally, get the result of the last statement.
For example, here is the XML layout for the world’s smallest BeanShell IDE:
<?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"
>
<Button
android:id="@+id/eval"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Go!"
/>
<EditText
android:id="@+id/script"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:singleLine="false"
android:gravity="top"
/>
</LinearLayout>
Couple that with the following activity implementation:
package com.commonsware.android.andshell;
import android.app.Activity;
import android.app.AlertDialog;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import bsh.Interpreter;
public class MainActivity extends Activity {
private Interpreter i = new Interpreter();
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);
Button btn = (Button)findViewById(R.id.eval);
final EditText script = (EditText)findViewById(R.id.script);
btn.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
String src = script.getText().toString();
try {
i.set("context", MainActivity.this);
i.eval(src);
} catch (bsh.EvalError e) {
AlertDialog.Builder builder =
new AlertDialog.Builder(MainActivity.this);
builder
.setTitle("Exception!").setMessage(e.toString())
.setPositiveButton("OK", null).show();
}
}
});
}
}
Compile and run it (including incorporating the BeanShell JAR as mentioned earlier), and install it on the emulator. Fire it up, and you get a trivial IDE with a large text area for your script and a big Go! button (see Figure 21-1) to execute it.
Figure 21-1. The AndShell BeanShell IDE
import android.widget.Toast;
Toast.makeText(context, "Hello, world!", 5000).show();
Note the use of context
to refer to the activity when making the Toast
. That is the global set by the activity to reference back to itself. You could call this global variable anything you want, so long as the set()
call and the script code use the same name.
When you click the Go! button, you get the result shown in Figure 21-2.
Figure 21-2. The AndShell BeanShell IDE, executing some code
And now, some caveats.
First, not all scripting languages will work. For example, those that implement their own form of just-in-time (JIT) compilation, generating Java bytecodes on the fly, would probably have to be augmented to generate Dalvik VM bytecodes instead of those for stock Java implementations. Simpler languages that execute from parsed scripts, calling Java reflection APIs to call back into compiled classes, will likely work better. Even there, though, not every feature of the language may work if the feature relies upon some facility in a traditional Java API that does not exist in Dalvik — for example, there could be stuff hidden inside BeanShell or the add-on JARs that does not work on today’s Android.
Second, scripting languages without JIT will inevitably be slower than compiled Dalvik applications. Slower may mean users experience sluggishness. Slower definitely means more battery life is consumed for the same amount of work. So, building a whole Android application in BeanShell, simply because you feel it is easier to program in may cause your users to be unhappy.
Third, scripting languages that expose the whole Java API, like BeanShell, can pretty much do anything the underlying Android security model allows. So, if your application has the READ_CONTACTS
permission, expect any BeanShell scripts your application runs to have the same permission.
Last, but certainly not least, is that language interpreter JARs tend to be… portly. The BeanShell JAR used in this example is 200KB. That is not ridiculous, considering what it does, but it will make applications that use BeanShell that much bigger to download, take up that much more space on the device, etc.
Not all Java code will work on Android and Dalvik. Specifically consider the following:
• If the Java code assumes it runs on Java SE, Java ME, or Java EE, it may be missing some APIs that those platforms provide that Android does not. For example, some charting libraries assume the existence of Swing or Abstract Window Toolkit (AWT) drawing primitives, which are generally unavailable on Android.
• The Java code might have a dependency on other Java code that, in turn, might have problems running on Android. For example, you might want to use a JAR that relies upon an earlier (or newer) version of the Apache HTTPComponents than the one that is bundled with Android.
• The Java code may use language capabilities beyond what the Dalvik engine is capable of using.
In all these cases, if you have only a compiled JAR to work with, you may not encounter problems at compile time, but rather when running the application. Hence, where possible it is best to use open-source code with Android so you can build the third-party code alongside your own and find out about difficulties sooner.
The expectation is that most, if not all, Android devices will have built-in Internet access. That could be WiFi, cellular data services (EDGE, 3G, etc.), or possibly something else entirely. Regardless, most people — or at least those with a data plan or WiFi access — will be able to get to the Internet from their Android phone.
Not surprisingly, the Android platform gives developers a wide range of ways to make use of this Internet access. Some offer high-level access, such as the integrated WebKit browser component we saw in Chapter 13. If you want, you can drop all the way down to using raw sockets. Or, in between, you can leverage APIs — both on-device and from 3rd-party JARs — that give you access to specific protocols: HTTP, XMPP, SMTP, and so on.
The emphasis of this book is on the higher-level forms of access: the WebKit component and Internet-access APIs, as busy coders should be trying to reuse existing components versus rolling one’s own on-the-wire protocol wherever possible.
Android does not have built-in SOAP or XML-RPC client APIs. However, it does have the Apache HttpComponents library baked in. You can either layer a SOAP/XML-RPC layer atop this library, or use it “straight” for accessing REST-style Web services. For the purposes of this book, “REST-style Web services” is defined as simple HTTP requests for ordinary URLs over the full range of HTTP verbs, with formatted payloads (XML, JSON, etc.) as responses.
More expansive tutorials, FAQs, and HOWTOs can be found at the HttpComponents Web site.[28] Here, we’ll cover the basics, while checking the weather.
The HTTPClient component of HttpComponents handles all HTTP requests on your behalf. The first step to using HttpClient is, not surprisingly, to create an HttpClient
object. Since HttpClient
is an interface, you will need to actually instantiate some implementation of that interface, such as DefaultHttpClient
.
Those requests are bundled up into HttpRequest instances, with different HttpRequest
implementations for each different HTTP verb (e.g., HttpGet
for HTTP GET
requests). You create an HttpRequest
implementation instance, fill in the URL to retrieve and other configuration data (e.g., form values if you are doing an HTTP POST
via HttpPost
), then pass the method to the client to actually make the HTTP request via execute()
.
What happens at this point can be as simple or as complicated as you want. You can get an HttpResponse
object back, with response code (e.g., 200 for OK), HTTP headers, and the like. Or, you can use a flavor of execute()
that takes a ResponseHandler<String>
as a parameter — the net result there being that execute()
returns just the String
representation of the request body. In practice, this is not a recommended approach, because you really should be checking your HTTP response codes for errors. However, for trivial applications, like book examples, the ResponseHandler<String>
approach works just fine.
For example, let’s take a look at the Internet/Weather
sample project. This implements an activity that retrieves weather data for your current location from the National Weather Service (Note: this probably only works in the US). That data is converted into an HTML page, which is poured into a WebKit
widget for display. Rebuilding this demo using a ListView
is left as an exercise for the reader. Also, since this sample is relatively long, we will only show relevant pieces of the Java code here in this chapter, though you can always download the full source from the CommonsWare Web site.[29]
To make this a bit more interesting, we use the Android location services to figure out where we are… sort of. The full details of how that works is described in Chapter 33.
In the onResume()
method, we toggle on location updates, so we will be informed where we are now and when we move a significant distance (10km). When a location is available — either at the start or based on movement — we retrieve the National Weather Service data via our updateForecast()
method:
private void updateForecast(Location loc) {
String url = String.format(format, loc.getLatitude(), loc.getLongitude());
HttpGet getMethod = new HttpGet(url);
try {
ResponseHandler<String> responseHandler = new BasicResponseHandler();
String responseBody = client.execute(getMethod, responseHandler);
buildForecasts(responseBody);
String page = generatePage();
browser.loadDataWithBaseURL(null, page, "text/html",
"UTF-8", null);
} catch (Throwable t) {
Toast.makeText(this, "Request failed: " + t.toString(), 4000).show();
}
}
The updateForecast()
method takes a Location
as a parameter, obtained from the location update process. For now, all you need to know is that Location sports getLatitude()
and getLongitude()
methods that return the latitude and longitude of the device’s position, respectively.
We hold the URL to the National Weather Service XML in a string resource, and pour in the latitude and longitude at runtime. Given our HttpClient
object created in onCreate()
, we populate an HttpGet
with that customized URL, then execute that method. Given the resulting XML from the REST service, we build the forecast HTML page (see “Parsing Responses”) and pour that into the WebKit
widget. If the HttpClient
blows up with an exception, we provide that error as a Toast
.
The response you get will be formatted using some system — HTML, XML, JSON, whatever. It is up to you, of course, to pick out what information you need and do something useful with it. In the case of the WeatherDemo
, we need to extract the forecast time, temperature, and icon (indicating sky conditions and precipitation) and generate an HTML page from it.
Android includes:
• Three XML parsers: the traditional W3C DOM (org.w3c.dom
), a SAX parser (org.xml.sax
), and the XML pull parser discussed in Chapter 19
• A JSON parser (org.json
)
You are also welcome to use third-party Java code, where possible, to handle other formats, such as a dedicated RSS/Atom parser for a feed reader. The use of third-party Java code is discussed in Chapter 21.
For WeatherDemo
, we use the W3C DOM parser in our buildForecasts()
method:
void buildForecasts(String raw) throws Exception {
DocumentBuilder builder = DocumentBuilderFactory
.newInstance().newDocumentBuilder();
Document doc = builder.parse(new InputSource(new StringReader(raw)));
NodeList times = doc.getElementsByTagName("start-valid-time");
for (int i=0; i<times.getLength(); i++) {
Element time = (Element)times.item(i);
Forecast forecast = new Forecast();
forecasts.add(forecast);
forecast.setTime(time.getFirstChild().getNodeValue());
}
NodeList temps = doc.getElementsByTagName("value");
for (int i=0; i<temps.getLength(); i++) {
Element temp = (Element)temps.item(i);
Forecast forecast = forecasts.get(i);
forecast.setTemp(new Integer(temp.getFirstChild().getNodeValue()));
}
NodeList icons = doc.getElementsByTagName("icon-link");
for (int i=0; i<icons.getLength(); i++) {
Element icon = (Element)icons.item(i);
Forecast forecast = forecasts.get(i);
forecast.setIcon(icon.getFirstChild().getNodeValue());
}
}
The National Weather Service XML format is… curiously structured, relying heavily on sequential position in lists versus the more object-oriented style you find in formats like RSS or Atom. That being said, we can take a few liberties and simplify the parsing somewhat, taking advantage of the fact that the elements we want (start-valid-time
for the forecast time, value for the temperature, and icon-link
for the icon URL) are all unique within the document.
The HTML comes in as an InputStream
and is fed into the DOM parser. From there, we scan for the start-valid-time
elements and populate a set of Forecast
models using those start times. Then, we find the temperature value elements and icon-link
URLs and fill those into the Forecast
objects.
In turn, the generatePage()
method creates a rudimentary HTML table with the forecasts:
String generatePage() {
StringBuffer bufResult = new StringBuffer("<html><body><table>");
bufResult.append("<tr><th width=\"50%\">Time</th>" +
"<th>Temperature</th><th>Forecast</th></tr>");
for (Forecast forecast : forecasts) {
bufResult.append("<tr><td align=\"center\">");
bufResult.append(forecast.getTime());
bufResult.append("</td><td align=\"center\">");
bufResult.append(forecast.getTemp());
bufResult.append("</td><td><img src=\"");
bufResult.append(forecast.getIcon());
bufResult.append("\"></td></tr>");
}
bufResult.append("</table></body></html>");
return(bufResult.toString());
}
The result can be seen in Figure 22-1.
Figure 22-1. The WeatherDemo sample application
If you need to use SSL, bear in mind that the default HttpClient
setup does not include SSL support. Mostly, this is because you need to decide how to handle SSL certificate presentation — do you blindly accept all certificates, even self-signed or expired ones? Or do you want to ask the user if they really want to use some strange certificates?
Similarly, HttpClient
, by default, is designed for single-threaded use. If you will be using HttpClient
from a service or some other place where multiple threads might be an issue, you can readily set up HttpClient
to support multiple threads.
For these sorts of topics, you are best served by checking out the HttpComponents
Web site for documentation and support.
http://en.wikipedia.org/wiki/Nuvola
http://www.xmlpull.org/v1/doc/api/org/xmlpull/v1/package-summary.html
http://en.wikipedia.org/wiki/ISO_639-1
http://code.google.com/android/devel/resources-i18n.html#AlternateResources
http://www.sqlite.org
http://www.amazon.com/Definitive-Guide-SQLite/dp/1590596730
http://www.sqlite.org/different.html
https://addons.mozilla.org/en-US/firefox/addon/5817
http://www.sqlite.org/cvstrac/wiki?p=SqliteTools
http://www.sqlite.org
http://beanshell.org
http://beanshell.org/developer.html
http://hc.apache.org/
http://commonsware.com/Android/