Now let’s study the contents of the application (see Figure 2–14).
As you can see, the application contains several .java files, a few .png images, three views (under the layout folder), and the AndroidManifest.xml file. If this were a
command-line application, you would start looking for the class with the Main method. So what’s the equivalent of a Main method in Android?
Android defines an entry-point activity, also called the top-level activity. If you look in the AndroidManifest.xml file, you’ll find one provider and three activities. The NotesList activity defines an intent-filter for the action android.intent.action.MAIN and for the category android.intent.category.LAUNCHER. When an Android application is asked to run, the host loads the application and reads the AndroidManifest.xml file. It then looks for, and starts, an activity or activities with an intent-filter that has the MAIN action with a category of LAUNCHER, as shown here:
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> </intent-filter>
Figure 2–14. Contents of the Notepad application
After the host finds the activity it wants to run, it must resolve the defined activity to an actual class. It does this by combining the root package name and the activity name, which in this case is com.example.android.notepad.NotesList (see Listing 2–1). Listing 2–1. The AndroidManfiest.xml File
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.android.notepad" > <application android:icon="@drawable/app_notes" android:label="@string/app_name" > <provider android:name="NotePadProvider" android:authorities="com.google.provider.NotePad" />
<activity android:name="NotesList" android:label="@string/title_notes_list"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" /> <action android:name="android.intent.action.PICK" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="vnd.android.cursor.dir/vnd.google.note" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.GET_CONTENT" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="vnd.android.cursor.item/vnd.google.note" /> </intent-filter> </activity> … </manfiest>
The application’s root package name is defined as an attribute of the <manifest> element in the AndroidManifest.xml file, and each activity has a name attribute. Once the entry-point activity is determined, the host starts the activity and the onCreate() method is called. Let’s have a look at NotesList.onCreate(), shown in Listing 2–2.
Listing 2–2. The onCreate Method
public class NotesList extends ListActivity { @Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT); Intent intent = getIntent();
if (intent.getData() == null) { intent.setData(Notes.CONTENT_URI); }
getListView().setOnCreateContextMenuListener(this);
Cursor cursor = managedQuery(getIntent().getData(), PROJECTION, null, null,
Notes.DEFAULT_SORT_ORDER);
SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, R.layout.noteslist_item, cursor, new String[] { Notes.TITLE }, new int[] { android.R.id.text1 });
setListAdapter(adapter); }
}
Activities in Android are usually started with an intent, and one activity can start another activity. The onCreate() method checks whether the current activity’s intent has data (notes). If not, it sets the URI to retrieve the data on the intent. In Chapter 3 we’ll show that Android accesses data through content providers that operate on URIs. In this case, the URI provides enough information to retrieve data from a database. The constant Notes.CONTENT_URI is defined as a static final in Notepad.java:
public static final Uri CONTENT_URI =
The Notes class is an inner class of the Notepad class. For now, know that the preceding URI tells the content provider to get all of the notes. If the URI looked something like this:
public static final Uri CONTENT_URI =
Uri.parse("content://" + AUTHORITY + "/notes/11");
then the consuming content provider would return the note with an ID equal to 11. We will discuss content providers and URIs in depth in Chapter 3.
The NotesList class extends the ListActivity class, which knows how to display list- oriented data. The items in the list are managed by an internal ListView (a UI
component), which displays the notes in the list. After setting the URI on the activity’s intent, the activity registers to build the context menu for notes. If you’ve played with the application, you probably noticed that context-sensitive menu items are displayed depending on your selection. For example, if you select an existing note, the application displays “Edit note” and “Edit title.” Similarly, if you don’t select a note, the application shows you the “Add note” option.
Next, we see the activity execute a managed query and get a cursor for the result. A managed query means that Android will manage the returned cursor. As part of managing the cursor, if the application has to be unloaded or reloaded, neither the application nor the activity has to worry about positioning the cursor, loading it, or unloading it. The parameters to managedQuery(), shown in Table 2–3, are interesting. Table 2–3. Parameters to Activity.managedQuery()
Parameter Data Type Description
URI Uri URI of the content provider
projection String[] The column to return (column names)
selection String Optional where clause
selectionArgs String[] The arguments to the selection, if the query contains ?s
sortOrder String Sort order to be used on the result set
We will discuss managedQuery() and its sibling query() later in this section and also in Chapter 3. For now, realize that a query in Android returns tabular data. The projection parameter allows you to define the columns you are interested in. You can also reduce the overall result set and sort the result set using a SQL order-by clause (such as asc or desc). Also note that an Android query must return a column named _ID to support retrieving an individual record. Moreover, you must know the type of data returned by the content provider—whether a column contains a string, int, binary, or the like. After the query is executed, the returned cursor is passed to the constructor of
SimpleCursorAdapter, which adapts records in the dataset to items in the user interface (ListView). Look closely at the parameters passed to the constructor of
SimpleCursorAdapter adapter =
new SimpleCursorAdapter(this, R.layout.noteslist_item,
cursor, new String[] { Notes.TITLE }, new int[] { android.R.id.text1 });
Specifically, look at the second parameter: an identifier to the view that represents the items in the ListView. As you’ll see in Chapter 3, Android provides an auto-generated utility class that provides references to the resources in your project. This utility class is called the R class because its name is R.java. When you compile your project, the AAPT generates the R class for you from the resources defined within your res folder. For example, you could put all your string resources into the values folder and the AAPT will generate a public static identifier for each string. Android supports this generically for all of your resources. For example, in the constructor of SimpleCursorAdapter, the NotesList activity passes in the identifier of the view that displays an item from the notes list. The benefit of this utility class is that you don’t have to hard-code your resources and you get compile-time reference checking. In other words, if a resource is deleted, the R class will lose the reference and any code referring to the resource will not compile.
Let’s look at another important concept in Android that we alluded to earlier: the onListItemClick() method of NotesList (see Listing 2–3).
Listing 2–3. The onListItemClick Method @Override
protected void onListItemClick(ListView l, View v, int position, long id) { Uri uri = ContentUris.withAppendedId(getIntent().getData(), id);
String action = getIntent().getAction(); if (Intent.ACTION_PICK.equals(action) || Intent.ACTION_GET_CONTENT.equals(action)) {
setResult(RESULT_OK, new Intent().setData(uri)); } else {
startActivity(new Intent(Intent.ACTION_EDIT, uri)); }
}
The onListItemClick() method is called when a user selects a note in the UI. The method demonstrates that one activity can start another activity. When a note is selected, the method creates a URI by taking the base URI and appending the selected note’s ID to it. The URI is then passed to startActivity() with a new intent.
startActivity() is one way to start an activity: it starts an activity but doesn’t report on the results of the activity after it completes. Another way to start an activity is to use startActivityForResult(). With this method, you can start another activity and register a callback to be used when the activity completes. For example, you’ll want to use startActivityForResult() to start an activity to select a contact because you want that contact after the activity completes.
At this point, you might be wondering about user interaction with respect to activities. For example, if the running activity starts another activity, and that activity starts an activity (and so on), then what activity can the user work with? Can she manipulate all the activities simultaneously, or is she restricted to a single activity? Actually, activities have a defined lifecycle. They’re maintained on an activity stack, with the running activity
at the top. If the running activity starts another activity, the first running activity moves down the stack and the new activity is placed on the top. Activities lower in the stack can be in a “paused” or “stopped” state. A paused activity is partially or fully visible to the user; a stopped activity is not visible to the user. The system can kill paused or stopped activities if it deems that resources are needed elsewhere.
Let’s move on to data persistence now. The notes that a user creates are saved to an actual database on the device. Specifically, the Notepad application’s backing store is a SQLite database. The managedQuery() method that we discussed earlier eventually
resolves to data in a database, via a content provider. Let’s examine how the URI, passed to managedQuery(), results in the execution of a query against a SQLite
database. Recall that the URI passed to managedQuery() looks like this: public static final Uri CONTENT_URI =
Uri.parse("content://" + AUTHORITY + "/notes");
Content URIs always have this form: content://, followed by the authority, followed by
a general segment (context-specific). Because the URI doesn’t contain the actual data, it somehow results in the execution of code that produces data. What is this connection? How is the URI reference resolved to code that produces data? Is the URI an HTTP service or a web service? Actually, the URI, or the authority portion of the URI, is configured in the AndroidManifest.xml file as a content provider:
<provider android:name="NotePadProvider"
android:authorities="com.google.provider.NotePad"/>
When Android sees a URI that needs to be resolved, it pulls out the authority portion of it and looks up the ContentProvider class configured for the authority. In the Notepad
application, the AndroidManifest.xml file contains a class called NotePadProvider
configured for the com.google.provider.NotePad authority. Listing 2–4 shows a small
portion of the class.
Listing 2–4. The NotePadProvider Class
public class NotePadProvider extends ContentProvider {
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,String sortOrder) {}
@Override
public Uri insert(Uri uri, ContentValues initialValues) {} @Override
public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {}
@Override
public int delete(Uri uri, String where, String[] whereArgs) {} @Override
public String getType(Uri uri) {} @Override
public boolean onCreate() {}
private static class DatabaseHelper extends SQLiteOpenHelper {} @Override
public void onCreate(SQLiteDatabase db) {} @Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
//... }
} }
The NotePadProvider class extends the ContentProvider class. The ContentProvider class defines six abstract methods, four of which are CRUD (Create, Read, Update, Delete) operations. The other two abstract methods are onCreate() and getType(). onCreate() is called when the content provider is created for the first time. getType() provides the MIME type for the result set (you’ll see how MIME types work when you read Chapter 3).
The other interesting thing about the NotePadProvider class is the internal
DatabaseHelper class, which extends the SQLiteOpenHelper class. Together, the two classes take care of initializing the Notepad database, opening and closing it, and performing other database tasks. Interestingly, the DatabaseHelper class is just a few lines of custom code (see Listing 2–5), while the Android implementation of
SQLiteOpenHelper does most of the heavy lifting. Listing 2–5. The DatabaseHelper Class
private static class DatabaseHelper extends SQLiteOpenHelper { DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION); }
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + NOTES_TABLE_NAME + " (" + Notes._ID + " INTEGER PRIMARY KEY," + Notes.TITLE + " TEXT," + Notes.NOTE + " TEXT," + Notes.CREATED_DATE + " INTEGER," + Notes.MODIFIED_DATE + " INTEGER" + ");"); } //… }
As shown in Listing 2–5, the onCreate() method creates the Notepad table. Notice that the class’s constructor calls the superclass’s constructor with the name of the table. The superclass will call the onCreate() method only if the table does not exist in the
database. Also notice that one of the columns in the Notepad table is the _ID column we discussed in the section “Dissecting the Application.”
Now let’s look at one of the CRUD operations: the insert() method (see Listing 2–6). Listing 2–6. The insert() Method
//…
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
long rowId = db.insert(NOTES_TABLE_NAME, Notes.NOTE, values); if (rowId > 0) {
Uri noteUri = ContentUris.withAppendedId( NotePad.Notes.CONTENT_URI, rowId);
getContext().getContentResolver().notifyChange(noteUri, null); return noteUri;
}
The insert() method uses its internal DatabaseHelper instance to access the database and then inserts a notes record. The returned row ID is then appended to the URI and a new URI is returned to the caller.
At this point, you should be familiar with how an Android application is laid out. You should be able to navigate your way around Notepad, as well as some of the other samples in the Android SDK. You should be able to run the samples and play with them. Now let’s look at the overall lifecycle of an Android application.