Analyzing the Notepad Application

Not only do you know how to create a new Android application and run it in the emulator, but you also have a feel for the artifacts of an Android application. Next, we are going to look at the Notepad application that ships with the Android SDK. Notepad's complexity falls between that of the "Hello World!" app and a full-blown Android application, so analyzing its components will give you some realistic insight into Android development.

Loading and Running the Notepad Application

In this section, we'll show you how to load the Notepad application into the Eclipse IDE and run it in the emulator. Before we start, you should know that the Notepad application implements several use cases. For example, the user can create a new note, edit an existing note, delete a note, view the list of created notes, and so on. When the user launches the application, there aren't any saved notes yet, so the user sees an empty note list. If the user presses the Menu key, the application presents him with a list of actions, one of which allows him to add a new note. Alter he adds the note, he can edit or delete the note by selecting the corresponding menu option. Follow these steps to load the Notepad sample into the Eclipse IDE:

1. Start Eclipse.

3. In the "New Project" dialog, select Android > Android Project.

4. In the "New Android Project" dialog, select "Create project from existing source" and set the "Location" field to the path of the Notepad application. Note that the Notepad application is located in c:\AndroidSDK\samples\, which you downloaded earlier. After you set the path, the dialog reads the AndroidManifest.xml file and prepopulates the remaining fields in the "New Android Project" dialog box.

5. Click the "Finish" button.

You should now see the NotesList application in your Eclipse IDE. To run the application, you could create a launch configuration (as you did for the "Hello World!" application), or you can simply right-click the project, choose Run As, and select Android Application. This will launch the emulator and install the application on it. After the emulator has completed loading (you'll see the date and time displayed in the center of the emulator's screen), press the Menu button to view the Notepad application. Play around with the application for a few minutes to become familiar with it.

Dissecting the Application

Now let's study the contents of the application (see Figure 2-8).

d \S NotesList

+ Si Android Library - 0srt

- ffi com.example.android.notepad + 0 NoteEditor.java ¿■■[J] Notepad.java j [J] NotePadProvider.jaya Etl ■■[!] NotesList.Java + 0 R.java + 0 TitleEditor.iava & assets G &res

" noteslistjtem.xml 1 [k. title_edltor.:.:ml Q- & values

31 strings.xml CI AndroidManifest.xml ™ sample_note,png ™ sample_notepad,png

Figure 2-8. Contents of the Notepad application

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>

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.category.DEFAULT" /> <data android:mimeTyp="android.intent.action.PICK" /> <category android:name e="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 savedlnstanceState) { super.onCreate(savedlnstanceState);

setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT); Intent intent = getIntent(); if (intent.getData() == null) {

intent.setData(Notes.CONTENT_URI);

getListView().setOnCreateContextMenuListener(this);

Cursor cursor = managedOuery(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.textl }); 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. We'll learn in Chapter 3 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 =

Uri.parse("content://" + AUTHORITY + "/notes");

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 (update or delete) 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 vertically (by default). 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. In other words, 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-2, are interesting.

Table 2-2. Parameters to Activity.managedQueryO

Parameter

Data Type

Description

URI

Uri

URI of the content provider

projection

Stringi]

The column to return (column names)

selection

String

Optional where clause

selectionArgs

Stringi]

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:

SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, R.layout.noteslist_item, cursor, new String[] { Notes.TITLE }, new int[] { android.R.id.text! });

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 moves to the top. Activities lower in the stack can be in a so-called "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 managedOuery() method that we discussed earlier eventually resolves to data in a database, via a content provider. Let's examine how the URI, passed to managedOuery(), results in the execution of a query against a SQLite database. Recall that the URI passed to managedOuery() 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 SOLiteOpenHelper {} ^Override public void onCreate(SOLiteDatabase db) {} ^Override public void onUpgrade(SOLiteDatabase db, int oldVersion, int newVersion) { //...

Clearly, you can see that 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 SOLiteOpenHelper 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 SOLiteOpenHelper does most of the heavy lifting.

Listing 2-5. The DatabaseHelper Class private static class DatabaseHelper extends SOLiteOpenHelper {

DatabaseHelper(Context context) {

super(context, DATABASE_NAME, null, DATABASE_VERSION);

^Override public void onCreate(SOLiteDatabase db) {

db.execSOL("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 insertO Method

SQLiteDatabase db = mOpenHelper.getWritableDatabase();

long rowld = db.insert(NOTES_TABLE_NAME, Notes.NOTE, values); if (rowld > 0) {

Uri noteUri = ContentUris.withAppendedId( NotePad.Notes.CONTENT_URI, rowld);

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.

0 0

Post a comment