summaryrefslogtreecommitdiffstats
path: root/samples/NotePad
diff options
context:
space:
mode:
authorJoe Malin <jmalin@google.com>2010-08-24 17:44:55 -0700
committerJoe Malin <jmalin@google.com>2010-08-24 17:44:55 -0700
commit88bcee2e891c5551d9db7c50572c1aa9aa0d6839 (patch)
treee5a57c7c05744b4c37df3a048efade776aa1e075 /samples/NotePad
parent6e94ff6be96e3fe65054a49d498278631abee5e2 (diff)
downloadandroid_development-88bcee2e891c5551d9db7c50572c1aa9aa0d6839.tar.gz
android_development-88bcee2e891c5551d9db7c50572c1aa9aa0d6839.tar.bz2
android_development-88bcee2e891c5551d9db7c50572c1aa9aa0d6839.zip
Note Pad: merged jmalin and hackbod chgs
Change-Id: Ica0fff191b806981bdca05977174a6ed3bb6d9f1
Diffstat (limited to 'samples/NotePad')
-rw-r--r--samples/NotePad/Android.mk16
-rw-r--r--samples/NotePad/AndroidManifest.xml2
-rw-r--r--samples/NotePad/src/com/example/android/notepad/NoteEditor.java452
-rw-r--r--samples/NotePad/src/com/example/android/notepad/NotePad.java100
-rw-r--r--samples/NotePad/src/com/example/android/notepad/NotePadProvider.java690
-rw-r--r--samples/NotePad/src/com/example/android/notepad/NotesList.java422
-rw-r--r--samples/NotePad/src/com/example/android/notepad/NotesLiveFolder.java86
-rw-r--r--samples/NotePad/src/com/example/android/notepad/TitleEditor.java128
-rw-r--r--samples/NotePad/src/com/google/provider/NotePad.java65
-rw-r--r--samples/NotePad/tests/Android.mk14
-rw-r--r--samples/NotePad/tests/build.properties1
-rw-r--r--samples/NotePad/tests/src/com/example/android/notepad/NotePadActivityTest.java (renamed from samples/NotePad/tests/src/com/example/android/notepad/NotePadTest.java)6
-rw-r--r--samples/NotePad/tests/src/com/example/android/notepad/NotePadProviderTest.java585
13 files changed, 2090 insertions, 477 deletions
diff --git a/samples/NotePad/Android.mk b/samples/NotePad/Android.mk
deleted file mode 100644
index 793921270..000000000
--- a/samples/NotePad/Android.mk
+++ /dev/null
@@ -1,16 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_MODULE_TAGS := samples
-
-# Only compile source java files in this apk.
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-LOCAL_PACKAGE_NAME := NotePad
-
-LOCAL_SDK_VERSION := current
-
-include $(BUILD_PACKAGE)
-
-# Use the following include to make our test apk.
-include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/samples/NotePad/AndroidManifest.xml b/samples/NotePad/AndroidManifest.xml
index 7d41dd52e..7d3ce4342 100644
--- a/samples/NotePad/AndroidManifest.xml
+++ b/samples/NotePad/AndroidManifest.xml
@@ -113,6 +113,6 @@
</application>
- <uses-sdk android:targetSdkVersion="4" android:minSdkVersion="3"/>
+ <uses-sdk android:targetSdkVersion="8" android:minSdkVersion="8"/>
</manifest>
diff --git a/samples/NotePad/src/com/example/android/notepad/NoteEditor.java b/samples/NotePad/src/com/example/android/notepad/NoteEditor.java
index 57b464678..78a97210e 100644
--- a/samples/NotePad/src/com/example/android/notepad/NoteEditor.java
+++ b/samples/NotePad/src/com/example/android/notepad/NoteEditor.java
@@ -16,7 +16,7 @@
package com.example.android.notepad;
-import com.example.android.notepad.NotePad.Notes;
+import com.example.android.notepad.NotePad;
import android.app.Activity;
import android.content.ClipboardManager;
@@ -39,41 +39,50 @@ import android.view.MenuItem;
import android.widget.EditText;
/**
- * A generic activity for editing a note in a database. This can be used
- * either to simply view a note {@link Intent#ACTION_VIEW}, view and edit a note
- * {@link Intent#ACTION_EDIT}, or create a new empty note
- * {@link Intent#ACTION_INSERT}, or create a new note from the current contents
- * of the clipboard {@link Intent#ACTION_PASTE}.
+ * This Activity handles "editing" a note, where editing is responding to
+ * {@link Intent#ACTION_VIEW} (request to view data), edit a note
+ * {@link Intent#ACTION_EDIT}, create a note {@link Intent#ACTION_INSERT}, or
+ * create a new note from the current contents of the clipboard {@link Intent#ACTION_PASTE}.
+ *
+ * NOTE: Notice that the provider operations in this Activity are taking place on the UI thread.
+ * This is not a good practice. It is only done here to make the code more readable. A real
+ * application should use the {@link android.content.AsyncQueryHandler}
+ * or {@link android.os.AsyncTask} object to perform operations asynchronously on a separate thread.
*/
public class NoteEditor extends Activity {
+ // For logging and debugging purposes
private static final String TAG = "Notes";
- /**
- * Standard projection for the interesting columns of a normal note.
+ /*
+ * Creates a projection that returns the note ID and the note contents.
*/
- private static final String[] PROJECTION = new String[] {
- Notes._ID, // 0
- Notes.NOTE, // 1
- Notes.TITLE, // 2
+ private static final String[] PROJECTION =
+ new String[] {
+ NotePad.Notes._ID,
+ NotePad.Notes.COLUMN_NAME_TITLE,
+ NotePad.Notes.COLUMN_NAME_NOTE
};
- /** The index of the note column */
+ // The index of the note column
private static final int COLUMN_INDEX_NOTE = 1;
- /** The index of the title column */
+
+ // The index of the title column
private static final int COLUMN_INDEX_TITLE = 2;
-
- // This is our state data that is stored when freezing.
+
+ // A label for the saved state of the activity
private static final String ORIGINAL_CONTENT = "origContent";
- // Identifiers for our menu items.
+ // Menu item identifiers
private static final int REVERT_ID = Menu.FIRST;
private static final int DISCARD_ID = Menu.FIRST + 1;
private static final int DELETE_ID = Menu.FIRST + 2;
- // The different distinct states the activity can be run in.
+ // This Activity can be started by more than one action. Each action is represented
+ // as a "state" constant
private static final int STATE_EDIT = 0;
private static final int STATE_INSERT = 1;
private static final int STATE_PASTE = 2;
+ // Global mutable variables
private int mState;
private boolean mNoteOnly = false;
private Uri mUri;
@@ -82,137 +91,235 @@ public class NoteEditor extends Activity {
private String mOriginalContent;
/**
- * A custom EditText that draws lines between each line of text that is displayed.
+ * Defines a custom EditText View that draws lines between each line of text that is displayed.
*/
public static class LinedEditText extends EditText {
private Rect mRect;
private Paint mPaint;
- // we need this constructor for LayoutInflater
+ // This constructor is used by LayoutInflater
public LinedEditText(Context context, AttributeSet attrs) {
super(context, attrs);
-
+
+ // Creates a Rect and a Paint object, and sets the style and color of the Paint object.
mRect = new Rect();
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(0x800000FF);
}
-
+
+ /**
+ * This is called to draw the LinedEditText object
+ * @param canvas The canvas on which the background is drawn.
+ */
@Override
protected void onDraw(Canvas canvas) {
+
+ // Gets the number of lines of text in the View.
int count = getLineCount();
+
+ // Gets the global Rect and Paint objects
Rect r = mRect;
Paint paint = mPaint;
+ /*
+ * Draws one line in the rectangle for every line of text in the EditText
+ */
for (int i = 0; i < count; i++) {
+
+ // Gets the baseline coordinates for the current line of text
int baseline = getLineBounds(i, r);
+ /*
+ * Draws a line in the background from the left of the rectangle to the right,
+ * at a vertical position one dip below the baseline, using the "paint" object
+ * for details.
+ */
canvas.drawLine(r.left, baseline + 1, r.right, baseline + 1, paint);
}
+ // Finishes up by calling the parent method
super.onDraw(canvas);
}
}
+ /**
+ * This method is called by Android when the Activity is first started. From the incoming
+ * Intent, it determines what kind of editing is desired, and then does it.
+ */
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ /*
+ * Creates an Intent to use when the Activity object's result is sent back to the
+ * caller.
+ */
final Intent intent = getIntent();
- // Do some setup based on the action being performed.
+ /*
+ * Sets up for the edit, based on the action specified for the incoming Intent.
+ */
+ // Gets the action that triggered the intent filter for this Activity
final String action = intent.getAction();
+
+ // For an edit action:
if (Intent.ACTION_EDIT.equals(action)) {
- // Requested to edit: set that state, and the data being edited.
+
+ // Sets the Activity state to EDIT, and gets the URI for the data to be edited.
mState = STATE_EDIT;
mUri = intent.getData();
+
+ // For an insert or paste action:
} else if (Intent.ACTION_INSERT.equals(action)
|| Intent.ACTION_PASTE.equals(action)) {
- // Requested to insert: set that state, and create a new entry
- // in the container.
+
+ // Sets the Activity state to INSERT, gets the general note URI, and inserts an
+ // empty record in the provider
mState = STATE_INSERT;
mUri = getContentResolver().insert(intent.getData(), null);
- // If we were unable to create a new note, then just finish
- // this activity. A RESULT_CANCELED will be sent back to the
- // original activity if they requested a result.
+ /*
+ * If the attempt to insert the new note fails, shuts down this Activity. The
+ * originating Activity receives back RESULT_CANCELED if it requested a result.
+ * Logs that the insert failed.
+ */
if (mUri == null) {
+
+ // Writes the log identifier, a message, and the URI that failed.
Log.e(TAG, "Failed to insert new note into " + getIntent().getData());
+
+ // Closes the activity.
finish();
return;
}
- // The new entry was created, so assume all will end well and
+ // Since the new entry was created, this sets the result to be returned
// set the result to be returned.
setResult(RESULT_OK, (new Intent()).setAction(mUri.toString()));
- // If pasting, initialize data from clipboard.
+ // For a paste, initializes the data from clipboard.
if (Intent.ACTION_PASTE.equals(action)) {
+
+ // Does the paste
performPaste();
- // Switch to paste mode; can no longer modify title.
+ // Switches the state to PASTE. The title can not be modified.
mState = STATE_PASTE;
}
+ // If the action was other than EDIT or INSERT:
} else {
- // Whoops, unknown action! Bail.
+
+ // Logs an error that the action was not understood, finishes the Activity, and
+ // returns RESULT_CANCELED to an originating Activity.
Log.e(TAG, "Unknown action, exiting");
finish();
return;
}
- // Set the layout for this activity. You can find it in res/layout/note_editor.xml
+ // Sets the layout for this Activity. See res/layout/note_editor.xml
setContentView(R.layout.note_editor);
-
- // The text view for our note, identified by its ID in the XML file.
- mText = (EditText) findViewById(R.id.note);
- // Get the note!
- mCursor = managedQuery(mUri, PROJECTION, null, null, null);
+ // Gets a handle to the EditText in the the layout.
+ mText = (EditText) findViewById(R.id.note);
- // If an instance of this activity had previously stopped, we can
- // get the original text it started with.
+ /*
+ * Using the URI passed in with the triggering Intent, gets the note or notes in
+ * the provider.
+ * Note: This is being done on the UI thread. It will block the thread until the query
+ * completes. In a sample app, going against a simple provider based on a local database,
+ * the block will be momentary, but in a real app you should use
+ * android.content.AsyncQueryHandler or android.os.AsyncTask.
+ */
+ mCursor = managedQuery(
+ mUri, // The URI that gets multiple notes from the provider.
+ PROJECTION, // A projection that returns the note ID and note content for each note.
+ null, // No "where" clause selection criteria.
+ null, // No "where" clause selection values.
+ null // Use the default sort order (modification date, descending)
+ );
+
+ /*
+ * If this Activity had stopped previously, its state was written the ORIGINAL_CONTENT
+ * location in the saved Instance state. This gets the state.
+ */
if (savedInstanceState != null) {
mOriginalContent = savedInstanceState.getString(ORIGINAL_CONTENT);
}
}
+ /**
+ * This method is called when the Activity is about to come to the foreground. This happens
+ * when the Activity comes to the top of the task stack, OR when it is first starting.
+ *
+ * Moves to the first note in the list, sets an appropriate title for the action chosen by
+ * the user, puts the note contents into the TextView, and saves the original text as a
+ * backup.
+ */
@Override
protected void onResume() {
super.onResume();
- // If we didn't have any trouble retrieving the data, it is now
- // time to get at the stuff.
+ /*
+ * mCursor is initialized, since onCreate() always precedes onResume for any running
+ * process. This tests that it's not null, since it should always contain data.
+ */
if (mCursor != null) {
- // Make sure we are at the one and only row in the cursor.
+
+ /* Moves to the first record. Always call moveToFirst() before accessing data in
+ * a Cursor for the first time. The semantics of using a Cursor are that when it is
+ * created, its internal index is pointing to a "place" immediately before the first
+ * record.
+ */
mCursor.moveToFirst();
- // Modify our overall title depending on the mode we are running in.
+ // Modifies the window title for the Activity according to the current Activity state.
if (mState == STATE_EDIT) {
+
+ // Sets the title to Edit for edits
setTitle(getText(R.string.title_edit));
+
+ // Sets the title to "create" for inserts and pastes
} else if (mState == STATE_INSERT || mState == STATE_PASTE) {
setTitle(getText(R.string.title_create));
}
- // This is a little tricky: we may be resumed after previously being
- // paused/stopped. We want to put the new text in the text view,
- // but leave the user where they were (retain the cursor position
- // etc). This version of setText does that for us.
+ /*
+ * onResume() may have been called after the Activity lost focus (was paused).
+ * The user was either editing or creating a note when the Activity paused.
+ * The Activity should re-display the text that had been retrieved previously, but
+ * it should not move the cursor. This helps the user to continue editing or entering.
+ */
+
+ // Gets the note text from the Cursor and puts it in the TextView, but doesn't change
+ // the text cursor's position.
String note = mCursor.getString(COLUMN_INDEX_NOTE);
mText.setTextKeepState(note);
-
- // If we hadn't previously retrieved the original text, do so
- // now. This allows the user to revert their changes.
+
+ // Stores the original note text, to allow the user to revert changes.
if (mOriginalContent == null) {
mOriginalContent = note;
}
+ /*
+ * Something is wrong. The Cursor should always contain data. Report an error in the
+ * note.
+ */
} else {
setTitle(getText(R.string.error_title));
mText.setText(getText(R.string.error_message));
}
}
+ /**
+ * This method is called when an Activity loses focus during its normal operation, and is then
+ * later on killed. The Activity has a chance to save its state so that the system can restore
+ * it.
+ *
+ * Notice that this method isn't a normal part of the Activity lifecycle. It won't be called
+ * if the user simply navigates away from the Activity.
+ */
@Override
protected void onSaveInstanceState(Bundle outState) {
// Save away the original text, so we still have it if the activity
@@ -220,142 +327,271 @@ public class NoteEditor extends Activity {
outState.putString(ORIGINAL_CONTENT, mOriginalContent);
}
+ /**
+ * This method is called when the Activity loses focus.
+ *
+ * For Activity objects that edit information, onPause() may be the one place where changes are
+ * saved. The Android application model is predicated on the idea that "save" and "exit" aren't
+ * required actions. When users navigate away from an Activity, they shouldn't have to go back
+ * to it to complete their work. The act of going away should save everything and leave the
+ * Activity in a state where Android can destroy it if necessary.
+ *
+ * If the user hasn't done anything, then this deletes or clears out the note, otherwise it
+ * writes the user's work to the provider.
+ */
@Override
protected void onPause() {
super.onPause();
- // The user is going somewhere else, so make sure their current
- // changes are safely saved away in the provider. We don't need
- // to do this if only editing.
+ /*
+ * Tests to see that the query operation didn't fail (see onCreate()). The Cursor object
+ * will exist, even if no records were returned, unless the query failed because of some
+ * exception or error.
+ *
+ */
if (mCursor != null) {
+
+ // Get the current note text.
String text = mText.getText().toString();
int length = text.length();
- // If this activity is finished, and there is no text, then we
- // do something a little special: simply delete the note entry.
- // Note that we do this both for editing and inserting... it
- // would be reasonable to only do it when inserting.
+ /*
+ * If the Activity is in the midst of finishing and there is no text in the current
+ * note, returns a result of CANCELED to the caller, and deletes the note. This is done
+ * even if the note was being edited, the assumption being that the user wanted to
+ * "clear out" (delete) the note.
+ */
if (isFinishing() && (length == 0) && !mNoteOnly) {
setResult(RESULT_CANCELED);
deleteNote();
- // Get out updates into the provider.
+ /*
+ * Writes the edits to the provider. The note has been edited if an existing note was
+ * retrieved into the editor *or* if a new note was inserted. In the latter case,
+ * onCreate() inserted a new empty note into the provider, and it is this new note
+ * that is being edited.
+ */
} else {
+
+ // Creates a map to contain the new values for the columns
updateNote(text, null, !mNoteOnly);
}
}
}
+ /**
+ * This method is called when the user clicks the device's Menu button the first time for
+ * this Activity. Android passes in a Menu object that is populated with items.
+ *
+ * Builds the menus for editing and inserting, and adds in alternative actions that
+ * registered themselves to handle the MIME types for this application.
+ *
+ * @param menu A Menu object to which items should be added.
+ * @return True to display the menu.
+ */
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
- // Build the menus that are shown when editing.
+ // Builds the menus that are shown when editing. These are 'revert' to undo changes, and
+ // 'delete' to delete the note.
if (mState == STATE_EDIT) {
+
+ // Adds the 'revert' menu item, and sets its shortcut to numeric 0, letter 'r' and its
+ // icon to the Android standard revert icon.
menu.add(0, REVERT_ID, 0, R.string.menu_revert)
.setShortcut('0', 'r')
.setIcon(android.R.drawable.ic_menu_revert);
if (!mNoteOnly) {
+
+ // Adds the 'delete' menu item, and sets its shortcut to numeric 1, letter 'd'
+ // and its icon to the Android standard delete icon
menu.add(0, DELETE_ID, 0, R.string.menu_delete)
.setShortcut('1', 'd')
.setIcon(android.R.drawable.ic_menu_delete);
}
- // Build the menus that are shown when inserting.
+ // Builds the menus that are shown when inserting. The only option is 'Discard' to throw
+ // away the new note.
} else {
menu.add(0, DISCARD_ID, 0, R.string.menu_discard)
.setShortcut('0', 'd')
.setIcon(android.R.drawable.ic_menu_delete);
}
- // If we are working on a full note, then append to the
- // menu items for any other activities that can do stuff with it
- // as well. This does a query on the system for any activities that
- // implement the ALTERNATIVE_ACTION for our data, adding a menu item
- // for each one that is found.
+ /*
+ * Appends menu items for any Activity declarations that implement an alternative action
+ * for this Activity's MIME type, one menu item for each Activity.
+ */
if (!mNoteOnly) {
+
+ // Makes a new Intent with the URI data passed to this Activity
Intent intent = new Intent(null, getIntent().getData());
+
+ // Adds the ALTERNATIVE category to the Intent.
intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
- menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0,
- new ComponentName(this, NoteEditor.class), null, intent, 0, null);
+
+ /*
+ * Constructs a new ComponentName object that represents the current Activity.
+ */
+ ComponentName component = new ComponentName(
+ this,
+ NoteEditor.class);
+
+ /*
+ * In the ALTERNATIVE menu group, adds an option for each Activity that is registered to
+ * handle this Activity's MIME type. The Intent describes what type of items should be
+ * added to the menu; in this case, Activity declarations with category ALTERNATIVE.
+ */
+ menu.addIntentOptions(
+ Menu.CATEGORY_ALTERNATIVE, // The menu group to add the items to.
+ Menu.NONE, // No unique ID is needed.
+ Menu.NONE, // No ordering is needed.
+ component, // The current Activity object's component name
+ null, // No specific items need to be placed first.
+ intent, // The intent containing the type of items to add.
+ Menu.NONE, // No flags are necessary.
+ null // No need to generate an array of menu items.
+ );
}
+ // The method returns TRUE, so that further menu processing is not done.
return true;
}
+ /**
+ * This method is called when a menu item is selected. Android passes in the selected item.
+ * The switch statement in this method calls the appropriate method to perform the action the
+ * user chose.
+ *
+ * @param item The selected MenuItem
+ * @return True to indicate that the item was processed, and no further work is necessary. False
+ * to proceed to further processing as indicated in the MenuItem object.
+ */
@Override
public boolean onOptionsItemSelected(MenuItem item) {
- // Handle all of the possible menu actions.
+
+ // Chooses the action to perform
switch (item.getItemId()) {
+
+ // Deletes the note and close the Activity.
case DELETE_ID:
- deleteNote();
- finish();
- break;
+ deleteNote();
+ finish();
+ break;
+
+ // Discards the new note.
case DISCARD_ID:
- cancelNote();
- break;
+ cancelNote();
+ break;
+
+ // Discards any changes to an edited note.
case REVERT_ID:
- cancelNote();
- break;
+ cancelNote();
+ break;
}
+
+ // Continues with processing the menu item. In effect, if the item was an alternative
+ // action, this invokes the Activity for that action.
return super.onOptionsItemSelected(item);
}
//BEGIN_INCLUDE(paste)
/**
- * Replace the note's data with the current contents of the clipboard.
+ * A helper method that replaces the note's data with the contents of the clipboard.
*/
private final void performPaste() {
+
+ // Gets a handle to the Clipboard Manager
ClipboardManager clipboard = (ClipboardManager)
getSystemService(Context.CLIPBOARD_SERVICE);
+
+ // Gets a content resolver instance
ContentResolver cr = getContentResolver();
+ // Gets the clipboard data from the clipboard
ClippedData clip = clipboard.getPrimaryClip();
if (clip != null) {
- String text=null, title=null;
+ String text=null;
+ String title=null;
+
+ // Gets the first item from the clipboard data
ClippedData.Item item = clip.getItem(0);
+
+ // Tries to get the item's contents as a URI pointing to a note
Uri uri = item.getUri();
+
+ // Tests to see that the item actually is an URI, and that the URI
+ // is a content URI pointing to a provider whose MIME type is the same
+ // as the MIME type supported by the Note pad provider.
if (uri != null && NotePad.Notes.CONTENT_ITEM_TYPE.equals(cr.getType(uri))) {
- // The clipboard holds a reference to a note. Copy it.
- Cursor orig = cr.query(uri, PROJECTION, null, null, null);
+
+ // The clipboard holds a reference to data with a note MIME type. This copies it.
+ Cursor orig = cr.query(
+ uri, // URI for the content provider
+ PROJECTION, // Get the columns referred to in the projection
+ null, // No selection variables
+ null, // No selection variables, so no criteria are needed
+ null // Use the default sort order
+ );
+
+ // If the Cursor is not null, and it contains at least one record
+ // (moveToFirst() returns true), then this gets the note data from it.
if (orig != null) {
if (orig.moveToFirst()) {
text = orig.getString(COLUMN_INDEX_NOTE);
title = orig.getString(COLUMN_INDEX_TITLE);
}
+
+ // Closes the cursor.
orig.close();
}
}
- // If we weren't able to load the clipped data as a note, then
- // convert whatever it is to text.
+ // If the contents of the clipboard wasn't a reference to a note, then
+ // this converts whatever it is to text.
if (text == null) {
text = item.coerceToText(this).toString();
}
+ // Updates the current note with the retrieved title and text.
updateNote(text, title, true);
}
}
//END_INCLUDE(paste)
/**
- * Replace the current note contents with the given data.
+ * Replaces the current note contents with the text and title provided as arguments.
+ * @param text The new note contents to use.
+ * @param title The new note title to use
+ * @param updateTitle <em>true</em> if the title should be updated. This also updates the
+ * modification timestamp to the current time.
*/
private final void updateNote(String text, String title, boolean updateTitle) {
+
+ // Sets up a map to contain values to be updated in the provider.
ContentValues values = new ContentValues();
- // This stuff is only done when working with a full-fledged note.
+ // If updateTitle is true, sets the modification date/time stamp to now.
if (updateTitle) {
- // Bump the modification time to now.
- values.put(Notes.MODIFIED_DATE, System.currentTimeMillis());
+ values.put(NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE, System.currentTimeMillis());
- // If we are creating a new note, then we want to also create
- // an initial title for it.
+ // If the action is to insert a new note, this creates an initial title for it.
if (mState == STATE_INSERT) {
+
+ // If no title was provided as an argument, create one from the note text.
if (title == null) {
+
+ // Get the note's length
int length = text.length();
+
+ // Sets the title by getting a substring of the text that is 31 characters long
+ // or the number of characters in the note plus one, whichever is smaller.
title = text.substring(0, Math.min(30, length));
+
+ // If the resulting length is more than 30 characters, chops off any
+ // trailing spaces
if (length > 30) {
int lastSpace = title.lastIndexOf(' ');
if (lastSpace > 0) {
@@ -363,32 +599,50 @@ public class NoteEditor extends Activity {
}
}
}
- values.put(Notes.TITLE, title);
+
+ // In the values map, sets the value of the title
+ values.put(NotePad.Notes.COLUMN_NAME_TITLE, title);
}
}
- // Write our text back into the provider.
- values.put(Notes.NOTE, text);
+ // This puts the desired notes text into the map.
+ values.put(NotePad.Notes.COLUMN_NAME_NOTE, text);
+
+ /*
+ * Updates the provider with the new values in the map. The ListView is updated
+ * automatically. The provider sets this up by setting the notification URI for
+ * query Cursor objects to the incoming URI. The content resolver is thus
+ * automatically notified when the Cursor for the URI changes, and the UI is
+ * updated.
+ * Note: This is being done on the UI thread. It will block the thread until the
+ * update completes. In a sample app, going against a simple provider based on a
+ * local database, the block will be momentary, but in a real app you should use
+ * android.content.AsyncQueryHandler or android.os.AsyncTask.
+ */
+ getContentResolver().update(
+ mUri, // The URI for the record to update.
+ values, // The map of column names and new values to apply to them.
+ null, // No selection criteria are used, so no where columns are necessary.
+ null // No where columns are used, so no where arguments are necessary.
+ );
- // Commit all of our changes to persistent storage. When the update completes
- // the content provider will notify the cursor of the change, which will
- // cause the UI to be updated.
- getContentResolver().update(mUri, values, null, null);
}
/**
- * Take care of canceling work on a note. Deletes the note if we
- * had created it, otherwise reverts to the original text.
+ * This helper method cancels the work done on a note. It deletes the note if it was
+ * newly created, or reverts to the original text of the note i
*/
private final void cancelNote() {
+
+ // If
if (mCursor != null) {
if (mState == STATE_EDIT) {
// Put the original note text back into the database
mCursor.close();
mCursor = null;
ContentValues values = new ContentValues();
- values.put(Notes.NOTE, mOriginalContent);
+ values.put(NotePad.Notes.COLUMN_NAME_NOTE, mOriginalContent);
getContentResolver().update(mUri, values, null, null);
} else if (mState == STATE_INSERT) {
// We inserted an empty note, make sure to delete it
diff --git a/samples/NotePad/src/com/example/android/notepad/NotePad.java b/samples/NotePad/src/com/example/android/notepad/NotePad.java
index 25be23e6e..624be90a7 100644
--- a/samples/NotePad/src/com/example/android/notepad/NotePad.java
+++ b/samples/NotePad/src/com/example/android/notepad/NotePad.java
@@ -20,25 +20,92 @@ import android.net.Uri;
import android.provider.BaseColumns;
/**
- * Convenience definitions for NotePadProvider
+ * Defines a contract between the Note Pad content provider and its clients. A contract defines the
+ * information that a client needs to access the provider as one or more data tables. A contract
+ * is a public, non-extendable (final) class that contains constants defining column names and
+ * URIs. A well-written client depends only on the constants in the contract.
*/
public final class NotePad {
public static final String AUTHORITY = "com.google.provider.NotePad";
// This class cannot be instantiated
- private NotePad() {}
-
+ private NotePad() {
+ }
+
/**
- * Notes table
+ * Notes table contract
*/
public static final class Notes implements BaseColumns {
+
// This class cannot be instantiated
private Notes() {}
/**
+ * The table name offered by this provider
+ */
+ public static final String TABLE_NAME = "notes";
+
+ /*
+ * URI definitions
+ */
+
+ /**
+ * The scheme part for this provider's URI
+ */
+ private static final String SCHEME = "content://";
+
+ /**
+ * Path parts for the URIs
+ */
+
+ /**
+ * Path part for the Notes URI
+ */
+ private static final String PATH_NOTES = "/notes";
+
+ /**
+ * Path part for the Note ID URI
+ */
+ private static final String PATH_NOTE_ID = "/notes/";
+
+ /**
+ * 0-relative position of a note ID segment in the path part of a note ID URI
+ */
+ public static final int NOTE_ID_PATH_POSITION = 1;
+
+ /**
+ * Path part for the Live Folder URI
+ */
+ private static final String PATH_LIVE_FOLDER = "/live_folders/notes";
+
+ /**
* The content:// style URL for this table
*/
- public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/notes");
+ public static final Uri CONTENT_URI = Uri.parse(SCHEME + AUTHORITY + PATH_NOTES);
+
+ /**
+ * The content URI base for a single note. Callers must
+ * append a numeric note id to this Uri to retrieve a note
+ */
+ public static final Uri CONTENT_ID_URI_BASE
+ = Uri.parse(SCHEME + AUTHORITY + PATH_NOTE_ID);
+
+ /**
+ * The content URI match pattern for a single note, specified by its ID. Use this to match
+ * incoming URIs or to construct an Intent.
+ */
+ public static final Uri CONTENT_ID_URI_PATTERN
+ = Uri.parse(SCHEME + AUTHORITY + PATH_NOTE_ID + "/#");
+
+ /**
+ * The content Uri pattern for a notes listing for live folders
+ */
+ public static final Uri LIVE_FOLDER_URI
+ = Uri.parse(SCHEME + AUTHORITY + PATH_LIVE_FOLDER);
+
+ /*
+ * MIME type definitions
+ */
/**
* The MIME type of {@link #CONTENT_URI} providing a directory of notes.
@@ -46,7 +113,8 @@ public final class NotePad {
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.google.note";
/**
- * The MIME type of a {@link #CONTENT_URI} sub-directory of a single note.
+ * The MIME type of a {@link #CONTENT_URI} sub-directory of a single
+ * note.
*/
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.google.note";
@@ -55,28 +123,32 @@ public final class NotePad {
*/
public static final String DEFAULT_SORT_ORDER = "modified DESC";
+ /*
+ * Column definitions
+ */
+
/**
- * The title of the note
+ * Column name for the title of the note
* <P>Type: TEXT</P>
*/
- public static final String TITLE = "title";
+ public static final String COLUMN_NAME_TITLE = "title";
/**
- * The note itself
+ * Column name of the note content
* <P>Type: TEXT</P>
*/
- public static final String NOTE = "note";
+ public static final String COLUMN_NAME_NOTE = "note";
/**
- * The timestamp for when the note was created
+ * Column name for the creation timestamp
* <P>Type: INTEGER (long from System.curentTimeMillis())</P>
*/
- public static final String CREATED_DATE = "created";
+ public static final String COLUMN_NAME_CREATE_DATE = "created";
/**
- * The timestamp for when the note was last modified
+ * Column name for the modification timestamp
* <P>Type: INTEGER (long from System.curentTimeMillis())</P>
*/
- public static final String MODIFIED_DATE = "modified";
+ public static final String COLUMN_NAME_MODIFICATION_DATE = "modified";
}
}
diff --git a/samples/NotePad/src/com/example/android/notepad/NotePadProvider.java b/samples/NotePad/src/com/example/android/notepad/NotePadProvider.java
index d79be2427..f1215536d 100644
--- a/samples/NotePad/src/com/example/android/notepad/NotePadProvider.java
+++ b/samples/NotePad/src/com/example/android/notepad/NotePadProvider.java
@@ -16,7 +16,7 @@
package com.example.android.notepad;
-import com.example.android.notepad.NotePad.Notes;
+import com.example.android.notepad.NotePad;
import android.content.ContentProvider;
import android.content.ContentUris;
@@ -51,176 +51,404 @@ import java.util.HashMap;
* itself, a creation date and a modified data.
*/
public class NotePadProvider extends ContentProvider implements PipeDataWriter<Cursor> {
-
+ // Used for debugging and logging
private static final String TAG = "NotePadProvider";
+ /**
+ * The database that the provider uses as its underlying data store
+ */
private static final String DATABASE_NAME = "note_pad.db";
+
+ /**
+ * The database version
+ */
private static final int DATABASE_VERSION = 2;
- private static final String NOTES_TABLE_NAME = "notes";
+ /**
+ * A projection map used to select columns from the database
+ */
private static HashMap<String, String> sNotesProjectionMap;
+
+ /**
+ * A projection map used to select columns from the database
+ */
private static HashMap<String, String> sLiveFolderProjectionMap;
+ /**
+ * Standard projection for the interesting columns of a normal note.
+ */
+ private static final String[] READ_NOTE_PROJECTION = new String[] {
+ NotePad.Notes._ID, // Projection position 0, the note's id
+ NotePad.Notes.COLUMN_NAME_NOTE, // Projection position 1, the note's content
+ NotePad.Notes.COLUMN_NAME_TITLE, // Projection position 2, the note's title
+ };
+ private static final int READ_NOTE_NOTE_INDEX = 1;
+ private static final int READ_NOTE_TITLE_INDEX = 2;
+
+ /*
+ * Constants used by the Uri matcher to choose an action based on the pattern
+ * of the incoming URI
+ */
+ // The incoming URI matches the Notes URI pattern
private static final int NOTES = 1;
+
+ // The incoming URI matches the Note ID URI pattern
private static final int NOTE_ID = 2;
+
+ // The incoming URI matches the Live Folder URI pattern
private static final int LIVE_FOLDER_NOTES = 3;
+ /**
+ * A UriMatcher instance
+ */
private static final UriMatcher sUriMatcher;
+ // Handle to a new DatabaseHelper.
+ private DatabaseHelper mOpenHelper;
+
+
/**
- * This class helps open, create, and upgrade the database file.
+ * A block that instantiates and sets static objects
*/
- private static class DatabaseHelper extends SQLiteOpenHelper {
-
- DatabaseHelper(Context context) {
- super(context, DATABASE_NAME, null, DATABASE_VERSION);
- }
+ static {
- @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"
- + ");");
- }
+ /*
+ * Creates and initializes the URI matcher
+ */
+ // Create a new instance
+ sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
- @Override
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
- + newVersion + ", which will destroy all old data");
- db.execSQL("DROP TABLE IF EXISTS notes");
- onCreate(db);
- }
- }
+ // Add a pattern that routes URIs terminated with "notes" to a NOTES operation
+ sUriMatcher.addURI(NotePad.AUTHORITY, "notes", NOTES);
- private DatabaseHelper mOpenHelper;
+ // Add a pattern that routes URIs terminated with "notes" plus an integer
+ // to a note ID operation
+ sUriMatcher.addURI(NotePad.AUTHORITY, "notes/#", NOTE_ID);
- @Override
- public boolean onCreate() {
- mOpenHelper = new DatabaseHelper(getContext());
- return true;
- }
+ // Add a pattern that routes URIs terminated with live_folders/notes to a
+ // live folder operation
+ sUriMatcher.addURI(NotePad.AUTHORITY, "live_folders/notes", LIVE_FOLDER_NOTES);
- @Override
- public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
- String sortOrder) {
- SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
- qb.setTables(NOTES_TABLE_NAME);
+ /*
+ * Creates and initializes a projection map that returns all columns
+ */
- switch (sUriMatcher.match(uri)) {
- case NOTES:
- qb.setProjectionMap(sNotesProjectionMap);
- break;
+ // Creates a new projection map instance. The map returns a column name
+ // given a string. The two are usually equal.
+ sNotesProjectionMap = new HashMap<String, String>();
- case NOTE_ID:
- qb.setProjectionMap(sNotesProjectionMap);
- qb.appendWhere(Notes._ID + "=" + uri.getPathSegments().get(1));
- break;
+ // Maps the string "_ID" to the column name "_ID"
+ sNotesProjectionMap.put(NotePad.Notes._ID, NotePad.Notes._ID);
- case LIVE_FOLDER_NOTES:
- qb.setProjectionMap(sLiveFolderProjectionMap);
- break;
+ // Maps "title" to "title"
+ sNotesProjectionMap.put(NotePad.Notes.COLUMN_NAME_TITLE, NotePad.Notes.COLUMN_NAME_TITLE);
- default:
- throw new IllegalArgumentException("Unknown URI " + uri);
- }
+ // Maps "note" to "note"
+ sNotesProjectionMap.put(NotePad.Notes.COLUMN_NAME_NOTE, NotePad.Notes.COLUMN_NAME_NOTE);
- // If no sort order is specified use the default
- String orderBy;
- if (TextUtils.isEmpty(sortOrder)) {
- orderBy = NotePad.Notes.DEFAULT_SORT_ORDER;
- } else {
- orderBy = sortOrder;
- }
+ // Maps "created" to "created"
+ sNotesProjectionMap.put(NotePad.Notes.COLUMN_NAME_CREATE_DATE,
+ NotePad.Notes.COLUMN_NAME_CREATE_DATE);
- // Get the database and run the query
- SQLiteDatabase db = mOpenHelper.getReadableDatabase();
- Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, orderBy);
+ // Maps "modified" to "modified"
+ sNotesProjectionMap.put(
+ NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE,
+ NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE);
- // Tell the cursor what uri to watch, so it knows when its source data changes
- c.setNotificationUri(getContext().getContentResolver(), uri);
- return c;
- }
+ /*
+ * Creates an initializes a projection map for handling Live Folders
+ */
- @Override
- public String getType(Uri uri) {
- switch (sUriMatcher.match(uri)) {
- case NOTES:
- case LIVE_FOLDER_NOTES:
- return Notes.CONTENT_TYPE;
+ // Creates a new projection map instance
+ sLiveFolderProjectionMap = new HashMap<String, String>();
- case NOTE_ID:
- return Notes.CONTENT_ITEM_TYPE;
+ // Maps "_ID" to "_ID AS _ID" for a live folder
+ sLiveFolderProjectionMap.put(LiveFolders._ID, NotePad.Notes._ID + " AS " + LiveFolders._ID);
- default:
- throw new IllegalArgumentException("Unknown URI " + uri);
- }
+ // Maps "NAME" to "title AS NAME"
+ sLiveFolderProjectionMap.put(LiveFolders.NAME, NotePad.Notes.COLUMN_NAME_TITLE + " AS " +
+ LiveFolders.NAME);
}
+ /**
+ *
+ * This class helps open, create, and upgrade the database file. Set to package visibility
+ * for testing purposes.
+ */
+ static class DatabaseHelper extends SQLiteOpenHelper {
+
+ DatabaseHelper(Context context) {
+
+ // calls the super constructor, requesting the default cursor factory.
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ /**
+ *
+ * Creates the underlying database with table name and column names taken from the
+ * NotePad class.
+ */
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE " + NotePad.Notes.TABLE_NAME + " ("
+ + NotePad.Notes._ID + " INTEGER PRIMARY KEY,"
+ + NotePad.Notes.COLUMN_NAME_TITLE + " TEXT,"
+ + NotePad.Notes.COLUMN_NAME_NOTE + " TEXT,"
+ + NotePad.Notes.COLUMN_NAME_CREATE_DATE + " INTEGER,"
+ + NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE + " INTEGER"
+ + ");");
+ }
+
+ /**
+ *
+ * Demonstrates that the provider must consider what happens when the
+ * underlying datastore is changed. In this sample, the database is upgraded the database
+ * by destroying the existing data.
+ * A real application should upgrade the database in place.
+ */
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+
+ // Logs that the database is being upgraded
+ Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
+ + newVersion + ", which will destroy all old data");
+
+ // Kills the table and existing data
+ db.execSQL("DROP TABLE IF EXISTS notes");
+
+ // Recreates the database with a new version
+ onCreate(db);
+ }
+ }
+
+ /**
+ *
+ * Initializes the provider by creating a new DatabaseHelper. onCreate() is called
+ * automatically when Android creates the provider in response to a resolver request from a
+ * client.
+ */
+ @Override
+ public boolean onCreate() {
+
+ // Creates a new helper object. Note that the database itself isn't opened until
+ // something tries to access it, and it's only created if it doesn't already exist.
+ mOpenHelper = new DatabaseHelper(getContext());
+
+ // Assumes that any failures will be reported by a thrown exception.
+ return true;
+ }
+
+ /**
+ * This method is called when a client calls
+ * {@link android.content.ContentResolver#query(Uri, String[], String, String[], String)}.
+ * Queries the database and returns a cursor containing the results.
+ *
+ * @return A cursor containing the results of the query. The cursor exists but is empty if
+ * the query returns no results or an exception occurs.
+ * @throws IllegalArgumentException if the incoming URI pattern is invalid.
+ */
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+
+ // Constructs a new query builder and sets its table name
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables(NotePad.Notes.TABLE_NAME);
+
+ /**
+ * Choose the projection and adjust the "where" clause based on URI pattern-matching.
+ */
+ switch (sUriMatcher.match(uri)) {
+ // If the incoming URI is for notes, chooses the Notes projection
+ case NOTES:
+ qb.setProjectionMap(sNotesProjectionMap);
+ break;
+
+ /* If the incoming URI is for a single note identified by its ID, chooses the
+ * note ID projection, and appends "_ID = <noteID>" to the where clause, so that
+ * it selects that single note
+ */
+ case NOTE_ID:
+ qb.setProjectionMap(sNotesProjectionMap);
+ qb.appendWhere(
+ NotePad.Notes._ID + // the name of the ID column
+ "=" +
+ // the position of the note ID itself in the incoming URI
+ uri.getPathSegments().get(NotePad.Notes.NOTE_ID_PATH_POSITION));
+ break;
+
+ case LIVE_FOLDER_NOTES:
+ // If the incoming URI is from a live folder, chooses the live folder projection.
+ qb.setProjectionMap(sLiveFolderProjectionMap);
+ break;
+
+ default:
+ // If the URI doesn't match any of the known patterns, throw an exception.
+ throw new IllegalArgumentException("Unknown URI " + uri);
+ }
+
+
+ String orderBy;
+ // If no sort order is specified, uses the default
+ if (TextUtils.isEmpty(sortOrder)) {
+ orderBy = NotePad.Notes.DEFAULT_SORT_ORDER;
+ } else {
+ // otherwise, uses the incoming sort order
+ orderBy = sortOrder;
+ }
+
+ // Opens the database object in "read" mode, since no writes need to be done.
+ SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+
+ /*
+ * Performs the query. If no problems occur trying to read the database, then a Cursor
+ * object is returned; otherwise, the cursor variable contains null. If no records were
+ * selected, then the Cursor object is empty, and Cursor.getCount() returns 0.
+ */
+ Cursor c = qb.query(
+ db, // The database to query
+ projection, // The columns to return from the query
+ selection, // The columns for the where clause
+ selectionArgs, // The values for the where clause
+ null, // don't group the rows
+ null, // don't filter by row groups
+ orderBy // The sort order
+ );
+
+ // Tells the Cursor what URI to watch, so it knows when its source data changes
+ c.setNotificationUri(getContext().getContentResolver(), uri);
+ return c;
+ }
+
+ /**
+ * This is called when a client calls {@link android.content.ContentResolver#getType(Uri)}.
+ * Returns the MIME data type of the URI given as a parameter.
+ *
+ * @param uri The URI whose MIME type is desired.
+ * @return The MIME type of the URI.
+ * @throws IllegalArgumentException if the incoming URI pattern is invalid.
+ */
+ @Override
+ public String getType(Uri uri) {
+
+ /**
+ * Chooses the MIME type based on the incoming URI pattern
+ */
+ switch (sUriMatcher.match(uri)) {
+
+ // If the pattern is for notes or live folders, returns the general content type.
+ case NOTES:
+ case LIVE_FOLDER_NOTES:
+ return NotePad.Notes.CONTENT_TYPE;
+
+ // If the pattern is for note IDs, returns the note ID content type.
+ case NOTE_ID:
+ return NotePad.Notes.CONTENT_ITEM_TYPE;
+
+ // If the URI pattern doesn't match any permitted patterns, throws an exception.
+ default:
+ throw new IllegalArgumentException("Unknown URI " + uri);
+ }
+ }
+
//BEGIN_INCLUDE(stream)
/**
- * Return the types of data streams we can return. Currently we only
- * support URIs to specific notes, and can convert such a note to a
- * plain text stream.
+ * Returns the types of available data streams. URIs to specific notes are supported.
+ * The application can convert such a note to a plain text stream.
+ *
+ * @param uri the URI to analyze
+ * @param mimeTypeFilter The MIME type to check for. This method only returns a data stream
+ * type for MIME types that match the filter. Currently, only text/plain MIME types match.
+ * @return a data stream MIME type. Currently, only text/plan is returned.
+ * @throws IllegalArgumentException if the URI pattern doesn't match any supported patterns.
*/
@Override
public String[] getStreamTypes(Uri uri, String mimeTypeFilter) {
+ /**
+ * Chooses the data stream type based on the incoming URI pattern.
+ */
switch (sUriMatcher.match(uri)) {
+
+ // If the pattern is for notes or live folders, return null. Data streams are not
+ // supported for this type of URI.
case NOTES:
case LIVE_FOLDER_NOTES:
return null;
+ // If the pattern is for note IDs and the MIME filter is text/plain, then return
+ // text/plain
case NOTE_ID:
if (compareMimeTypes("text/plain", mimeTypeFilter)) {
return new String[] { "text/plain" };
}
+
+ // If the URI is for a note id, but the MIME filter isn't text/plain, return null.
return null;
+ // If the URI pattern doesn't match any permitted patterns, throws an exception.
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
}
- /**
- * Standard projection for the interesting columns of a normal note.
- */
- private static final String[] READ_NOTE_PROJECTION = new String[] {
- Notes._ID, // 0
- Notes.NOTE, // 1
- NotePad.Notes.TITLE, // 2
- };
- private static final int READ_NOTE_NOTE_INDEX = 1;
- private static final int READ_NOTE_TITLE_INDEX = 2;
/**
- * Implement the other side of getStreamTypes: for each stream time we
- * report to support, we need to actually be able to return a stream of
- * data. This function simply retrieves a cursor for the URI of interest,
- * and uses ContentProvider's openPipeHelper() to start the work of
- * convering the data off into another thread.
+ * Returns a stream of data for each supported stream type. This method does a query on the
+ * incomding URI, then uses
+ * {@link android.content.ContentProvider#openPipeHelper(Uri, String, Bundle, Object,
+ * PipeDataWriter)} to start another thread in which to convert the data into a stream.
+ *
+ * @param uri The URI pattern that points to the data stream
+ * @param mimeTypeFilter A String containing a MIME type. This method tries to get a stream of
+ * data with this MIME type.
+ * @param opts The access mode for the data stream. This may be "r" for read-only, "w" for write
+ * with overwrite, "wa" for write with append, "rw" for read and write access for existing data,
+ * and "rwt" that truncates the existing file if the new data is smaller.
+ * @return AssetFileDescriptor A handle to the file.
+ * @throws FileNotFoundException if there is no file associated with the incoming URI.
*/
@Override
public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
throws FileNotFoundException {
- // Check if we support a stream MIME type for this URI.
+
+ // Checks to see if the MIME type filter matches a supported MIME type.
String[] mimeTypes = getStreamTypes(uri, mimeTypeFilter);
+
+ // If the MIME type is supported
if (mimeTypes != null) {
- // Retrieve the note for this URI.
- Cursor c = query(uri, READ_NOTE_PROJECTION, null, null, null);
+
+ // Retrieves the note for this URI. Uses the query method defined for this provider,
+ // rather than using the database query method.
+ Cursor c = query(
+ uri, // The URI of a note
+ READ_NOTE_PROJECTION, // Gets a projection containing the note's ID, title,
+ // and contents
+ null, // No WHERE clause, get all matching records
+ null, // Since there is no WHERE clause, no selection criteria
+ null // Use the default sort order (modification date,
+ // descending
+ );
+
+
+ // If the query fails or the cursor is empty, stop
if (c == null || !c.moveToFirst()) {
+
+ // If the cursor is empty, simply close the cursor and return
if (c != null) {
c.close();
}
+
+ // If the cursor is null, throw an exception
throw new FileNotFoundException("Unable to query " + uri);
}
- // Start a thread to pipe the data back to the client.
+
+ // Start a new thread that pipes the stream data back to the caller.
return new AssetFileDescriptor(
openPipeHelper(uri, mimeTypes[0], opts, c, this), 0,
AssetFileDescriptor.UNKNOWN_LENGTH);
}
+
+ // If the MIME type is not supported, return a read-only handle to the file.
return super.openTypedAssetFile(uri, mimeTypeFilter, opts);
}
@@ -256,116 +484,276 @@ public class NotePadProvider extends ContentProvider implements PipeDataWriter<C
}
//END_INCLUDE(stream)
+ /**
+ * This is called when a client calls
+ * {@link android.content.ContentResolver#insert(Uri, ContentValues)}.
+ * Inserts a new row into the database. This method sets up default values for any
+ * columns that are not included in the incoming map.
+ * If rows were inserted, then listeners are notified of the change.
+ * @return The row ID of the inserted row.
+ * @throws SQLException if the insertion fails.
+ */
@Override
public Uri insert(Uri uri, ContentValues initialValues) {
- // Validate the requested uri
+
+ // Validates the incoming URI. Only the full provider URI is allowed for inserts.
if (sUriMatcher.match(uri) != NOTES) {
throw new IllegalArgumentException("Unknown URI " + uri);
}
+ // A map to hold the new record's values.
ContentValues values;
+
+ // If the incoming values map is not null, uses it for the new values.
if (initialValues != null) {
values = new ContentValues(initialValues);
+
} else {
+ // Otherwise, create a new value map
values = new ContentValues();
}
+ // Gets the current system time in milliseconds
Long now = Long.valueOf(System.currentTimeMillis());
- // Make sure that the fields are all set
- if (values.containsKey(NotePad.Notes.CREATED_DATE) == false) {
- values.put(NotePad.Notes.CREATED_DATE, now);
+ // If the values map doesn't contain the creation date, sets the value to the current time.
+ if (values.containsKey(NotePad.Notes.COLUMN_NAME_CREATE_DATE) == false) {
+ values.put(NotePad.Notes.COLUMN_NAME_CREATE_DATE, now);
}
- if (values.containsKey(NotePad.Notes.MODIFIED_DATE) == false) {
- values.put(NotePad.Notes.MODIFIED_DATE, now);
+ // If the values map doesn't contain the modification date, sets the value to the current
+ // time.
+ if (values.containsKey(NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE) == false) {
+ values.put(NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE, now);
}
- if (values.containsKey(NotePad.Notes.TITLE) == false) {
+ // If the values map doesn't contain a title, sets the value to the default title.
+ if (values.containsKey(NotePad.Notes.COLUMN_NAME_TITLE) == false) {
Resources r = Resources.getSystem();
- values.put(NotePad.Notes.TITLE, r.getString(android.R.string.untitled));
+ values.put(NotePad.Notes.COLUMN_NAME_TITLE, r.getString(android.R.string.untitled));
}
- if (values.containsKey(NotePad.Notes.NOTE) == false) {
- values.put(NotePad.Notes.NOTE, "");
+ // If the values map doesn't contain note text, sets the value to an empty string.
+ if (values.containsKey(NotePad.Notes.COLUMN_NAME_NOTE) == false) {
+ values.put(NotePad.Notes.COLUMN_NAME_NOTE, "");
}
+ // Opens the database object in "write" mode.
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
- long rowId = db.insert(NOTES_TABLE_NAME, Notes.NOTE, values);
+
+ // Performs the insert and returns the ID of the new note.
+ long rowId = db.insert(
+ NotePad.Notes.TABLE_NAME, // The table to insert into.
+ NotePad.Notes.COLUMN_NAME_NOTE, // A hack, SQLite sets this column value to null
+ // if values is empty.
+ values // A map of column names, and the values to insert
+ // into the columns.
+ );
+
+ // If the insert succeeded, the row ID exists.
if (rowId > 0) {
- Uri noteUri = ContentUris.withAppendedId(NotePad.Notes.CONTENT_URI, rowId);
+ // Creates a URI with the note ID pattern and the new row ID appended to it.
+ Uri noteUri = ContentUris.withAppendedId(NotePad.Notes.CONTENT_ID_URI_BASE, rowId);
+
+ // Notifies observers registered against this provider that the data changed.
getContext().getContentResolver().notifyChange(noteUri, null);
return noteUri;
}
+ // If the insert didn't succeed, then the rowID is <= 0. Throws an exception.
throw new SQLException("Failed to insert row into " + uri);
}
+ /**
+ * This is called when a client calls
+ * {@link android.content.ContentResolver#delete(Uri, String, String[])}.
+ * Deletes records from the database. If the incoming URI matches the note ID URI pattern,
+ * this method deletes the one record specified by the ID in the URI. Otherwise, it deletes a
+ * a set of records. The record or records must also match the input selection criteria
+ * specified by where and whereArgs.
+ *
+ * If rows were deleted, then listeners are notified of the change.
+ * @return If a "where" clause is used, the number of rows affected is returned, otherwise
+ * 0 is returned. To delete all rows and get a row count, use "1" as the where clause.
+ * @throws IllegalArgumentException if the incoming URI pattern is invalid.
+ */
@Override
public int delete(Uri uri, String where, String[] whereArgs) {
+
+ // Opens the database object in "write" mode.
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
int count;
+
+ // Does the delete based on the incoming URI pattern.
switch (sUriMatcher.match(uri)) {
- case NOTES:
- count = db.delete(NOTES_TABLE_NAME, where, whereArgs);
- break;
- case NOTE_ID:
- String noteId = uri.getPathSegments().get(1);
- count = db.delete(NOTES_TABLE_NAME, Notes._ID + "=" + noteId
- + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs);
- break;
+ // If the incoming pattern matches the general pattern for notes, does a delete
+ // based on the incoming "where" columns and arguments.
+ case NOTES:
+ count = db.delete(
+ NotePad.Notes.TABLE_NAME, // The database table name
+ where, // The incoming where clause column names
+ whereArgs // The incoming where clause values
+ );
+ break;
+
+ // If the incoming URI matches a single note ID, does the delete based on the
+ // incoming data, but modifies the where clause to restrict it to the
+ // particular note ID.
+ case NOTE_ID:
+ // From the incoming URI, get the note ID
+ String noteId = uri.getPathSegments().get(NotePad.Notes.NOTE_ID_PATH_POSITION);
+
+ // If no where clause was passed in, uses the note ID column name
+ // for a column and the note ID for a value.
+ if (TextUtils.isEmpty(where)) {
+ where = NotePad.Notes._ID + " = ?";
+ whereArgs[0] = noteId;
+
+ } else {
+
+ /*
+ * If where clause columns were passed in, appends the note ID column name to
+ * the list of columns using a replaceable parameter. This works even if the
+ * other columns have actual values.
+ */
+ // Appends the note ID column name as an AND condition using a replaceable
+ // parameter.
+ where = where + " AND " + NotePad.Notes._ID + " = ?";
+
+ // Appends the note ID value to the end of the where clause values.
+ whereArgs[whereArgs.length] = noteId;
+ }
+
+ // Performs the delete.
+ count = db.delete(
+ NotePad.Notes.TABLE_NAME, // The database table name.
+ where, // The incoming where clause column names.
+ whereArgs // The incoming where clause values.
+ );
+ break;
- default:
- throw new IllegalArgumentException("Unknown URI " + uri);
+ // If the incoming pattern is invalid, throws an exception.
+ default:
+ throw new IllegalArgumentException("Unknown URI " + uri);
}
+ /*Gets a handle to the content resolver object for the current context, and notifies it
+ * that the incoming URI changed. The object passes this along to the resolver framework,
+ * and observers that have registered themselves for the provider are notified.
+ */
getContext().getContentResolver().notifyChange(uri, null);
+
+ // Returns the number of rows deleted.
return count;
}
+ /**
+ * This is called when a client calls
+ * {@link android.content.ContentResolver#update(Uri,ContentValues,String,String[])}
+ * Updates records in the database. The column names specified by the keys in the values map
+ * are updated with new data specified by the values in the map. If the incoming URI matches the
+ * note ID URI pattern, then the method updates the one record specified by the ID in the URI;
+ * otherwise, it updates a set of records. The record or records must match the input
+ * selection criteria specified by where and whereArgs.
+ * If rows were updated, then listeners are notified of the change.
+ *
+ * @param uri The URI pattern to match and update.
+ * @param values A map of column names (keys) and new values (values).
+ * @param where An SQL "WHERE" clause that selects records based on their column values. If this
+ * is null, then all records that match the URI pattern are selected.
+ * @param whereArgs An array of selection criteria. If the "where" param contains value
+ * placeholders ("?"), then each placeholder is replaced by the corresponding element in the
+ * array.
+ * @return The number of rows updated.
+ * @throws IllegalArgumentException if the incoming URI pattern is invalid.
+ */
@Override
public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
+
+ // Opens the database object in "write" mode.
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
int count;
+
+ // Does the update based on the incoming URI pattern
switch (sUriMatcher.match(uri)) {
- case NOTES:
- count = db.update(NOTES_TABLE_NAME, values, where, whereArgs);
- break;
- case NOTE_ID:
- String noteId = uri.getPathSegments().get(1);
- count = db.update(NOTES_TABLE_NAME, values, Notes._ID + "=" + noteId
- + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs);
- break;
+ // If the incoming URI matches the general notes pattern, does the update based on
+ // the incoming data.
+ case NOTES:
- default:
- throw new IllegalArgumentException("Unknown URI " + uri);
+ // Does the update and returns the number of rows updated.
+ count = db.update(
+ NotePad.Notes.TABLE_NAME, // The database table name.
+ values, // A map of column names and new values to use.
+ where, // The where clause column names.
+ whereArgs // The where clause column values to select on.
+ );
+ break;
+
+ // If the incoming URI matches a single note ID, does the update based on the incoming
+ // data, but modifies the where clause to restrict it to the particular note ID.
+ case NOTE_ID:
+ // From the incoming URI, get the note ID
+ String noteId = uri.getPathSegments().get(NotePad.Notes.NOTE_ID_PATH_POSITION);
+
+ // If no where clause was passed in, uses the note ID column name
+ // for a column and the note ID for a value.
+ if (TextUtils.isEmpty(where)) {
+ where = NotePad.Notes._ID + " = ?";
+ whereArgs[0] = noteId;
+
+ // If where clause columns were passed in, appends the note ID to the where
+ // clause
+ } else {
+
+ /*
+ * Appends the note ID column name to the list of columns, with a replaceable
+ * parameter. This will work even if the rest of the columns have been set with
+ * actual values.
+ */
+ // Appends the note ID column name as an AND condition with a replaceable
+ // parameter.
+ where = where + " AND " + NotePad.Notes._ID + " = ?";
+
+ // Appends the note ID value to the end of the where clause values.
+ whereArgs[whereArgs.length] = noteId;
+ }
+
+ // Does the update.
+ // Does the update and returns the number of rows updated.
+ count = db.update(
+ NotePad.Notes.TABLE_NAME, // The database table name.
+ values, // A map of column names and new values to use.
+ where, // The where clause column names.
+ whereArgs // The where clause column values to select on.
+ );
+ break;
+ // If the incoming pattern is invalid, throws an exception.
+ default:
+ throw new IllegalArgumentException("Unknown URI " + uri);
}
+ /*Gets a handle to the content resolver object for the current context, and notifies it
+ * that the incoming URI changed. The object passes this along to the resolver framework,
+ * and observers that have registered themselves for the provider are notified.
+ */
getContext().getContentResolver().notifyChange(uri, null);
+
+ // Returns the number of rows updated.
return count;
}
- static {
- sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
- sUriMatcher.addURI(NotePad.AUTHORITY, "notes", NOTES);
- sUriMatcher.addURI(NotePad.AUTHORITY, "notes/#", NOTE_ID);
- sUriMatcher.addURI(NotePad.AUTHORITY, "live_folders/notes", LIVE_FOLDER_NOTES);
-
- sNotesProjectionMap = new HashMap<String, String>();
- sNotesProjectionMap.put(Notes._ID, Notes._ID);
- sNotesProjectionMap.put(Notes.TITLE, Notes.TITLE);
- sNotesProjectionMap.put(Notes.NOTE, Notes.NOTE);
- sNotesProjectionMap.put(Notes.CREATED_DATE, Notes.CREATED_DATE);
- sNotesProjectionMap.put(Notes.MODIFIED_DATE, Notes.MODIFIED_DATE);
-
- // Support for Live Folders.
- sLiveFolderProjectionMap = new HashMap<String, String>();
- sLiveFolderProjectionMap.put(LiveFolders._ID, Notes._ID + " AS " +
- LiveFolders._ID);
- sLiveFolderProjectionMap.put(LiveFolders.NAME, Notes.TITLE + " AS " +
- LiveFolders.NAME);
- // Add more columns here for more robust Live Folders.
+ /**
+ * A test package can call this to get a handle to the database underlying NotePadProvider,
+ * so it can insert test data into the database. The test case class is responsible for
+ * instantiating the provider in a test context; {@link android.test.ProviderTestCase2} does
+ * this during the call to setUp()
+ *
+ * @return a handle to the database helper object for the provider's data.
+ */
+ DatabaseHelper getOpenHelperForTest() {
+ return mOpenHelper;
}
}
diff --git a/samples/NotePad/src/com/example/android/notepad/NotesList.java b/samples/NotePad/src/com/example/android/notepad/NotesList.java
index bbcb936a3..39be81ca2 100644
--- a/samples/NotePad/src/com/example/android/notepad/NotesList.java
+++ b/samples/NotePad/src/com/example/android/notepad/NotesList.java
@@ -16,7 +16,7 @@
package com.example.android.notepad;
-import com.example.android.notepad.NotePad.Notes;
+import com.example.android.notepad.NotePad;
import android.app.ListActivity;
import android.content.ClipboardManager;
@@ -40,10 +40,17 @@ import android.widget.SimpleCursorAdapter;
/**
* Displays a list of notes. Will display notes from the {@link Uri}
- * provided in the intent if there is one, otherwise defaults to displaying the
- * contents of the {@link NotePadProvider}
+ * provided in the incoming Intent if there is one, otherwise it defaults to displaying the
+ * contents of the {@link NotePadProvider}.
+ *
+ * NOTE: Notice that the provider operations in this Activity are taking place on the UI thread.
+ * This is not a good practice. It is only done here to make the code more readable. A real
+ * application should use the {@link android.content.AsyncQueryHandler} or
+ * {@link android.os.AsyncTask} object to perform operations asynchronously on a separate thread.
*/
public class NotesList extends ListActivity {
+
+ // For logging and debugging
private static final String TAG = "NotesList";
// Menu item ids
@@ -53,69 +60,168 @@ public class NotesList extends ListActivity {
public static final int MENU_ITEM_PASTE = Menu.FIRST + 3;
/**
- * The columns we are interested in from the database
+ * The columns needed by the cursor adapter
*/
private static final String[] PROJECTION = new String[] {
- Notes._ID, // 0
- Notes.TITLE, // 1
+ NotePad.Notes._ID, // 0
+ NotePad.Notes.COLUMN_NAME_TITLE, // 1
};
/** The index of the title column */
private static final int COLUMN_INDEX_TITLE = 1;
-
+
private MenuItem mPasteItem;
+ /**
+ * onCreate is called when Android starts this Activity from scratch.
+ */
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ // The user does not need to hold down the key to use menu shortcuts.
setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT);
- // If no data was given in the intent (because we were started
- // as a MAIN activity), then use our default content provider.
+ /* If no data is given in the Intent that started this Activity, then this Activity
+ * was started when the intent filter matched a MAIN action. We should use the default
+ * provider URI.
+ */
+ // Gets the intent that started this Activity.
Intent intent = getIntent();
+
+ // If there is no data associated with the Intent, sets the data to the default URI, which
+ // accesses a list of notes.
if (intent.getData() == null) {
- intent.setData(Notes.CONTENT_URI);
+ intent.setData(NotePad.Notes.CONTENT_URI);
}
- // Inform the list we provide context menus for items
+ /*
+ * Sets the callback for context menu activation for the ListView. The listener is set
+ * to be this Activity. The effect is that context menus are enabled for items in the
+ * ListView, and the context menu is handled by a method in NotesList.
+ */
getListView().setOnCreateContextMenuListener(this);
-
- // Perform a managed query. The Activity will handle closing and requerying the cursor
- // when needed.
- Cursor cursor = managedQuery(getIntent().getData(), PROJECTION, null, null,
- Notes.DEFAULT_SORT_ORDER);
-
- // Used to map notes entries from the database to views
- SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, R.layout.noteslist_item, cursor,
- new String[] { Notes.TITLE }, new int[] { android.R.id.text1 });
+
+ /* Performs a managed query. The Activity handles closing and requerying the cursor
+ * when needed.
+ *
+ * Please see the introductory note about performing provider operations on the UI thread.
+ */
+ Cursor cursor = managedQuery(
+ getIntent().getData(), // Use the default content URI for the provider.
+ PROJECTION, // Return the note ID and title for each note.
+ null, // No where clause, return all records.
+ null, // No where clause, therefore no where column values.
+ NotePad.Notes.DEFAULT_SORT_ORDER // Use the default sort order.
+ );
+
+ /*
+ * The following two arrays create a "map" between columns in the cursor and view IDs
+ * for items in the ListView. Each element in the dataColumns array represents
+ * a column name; each element in the viewID array represents the ID of a View.
+ * The SimpleCursorAdapter maps them in ascending order to determine where each column
+ * value will appear in the ListView.
+ */
+
+ // The names of the cursor columns to display in the view, initialized to the title column
+ String[] dataColumns = { NotePad.Notes.COLUMN_NAME_TITLE } ;
+
+ // The view IDs that will display the cursor columns, initialized to the TextView in
+ // noteslist_item.xml
+ int[] viewIDs = { android.R.id.text1 };
+
+ // Creates the backing adapter for the ListView.
+ SimpleCursorAdapter adapter
+ = new SimpleCursorAdapter(
+ this, // The Context for the ListView
+ R.layout.noteslist_item, // Points to the XML for a list item
+ cursor, // The cursor to get items from
+ dataColumns,
+ viewIDs
+ );
+
+ // Sets the ListView's adapter to be the cursor adapter that was just created.
setListAdapter(adapter);
}
+ /**
+ * Called when the user clicks the device's Menu button the first time for
+ * this Activity. Android passes in a Menu object that is populated with items.
+ *
+ * Sets up a menu that provides the Insert option plus a list of alternative actions for
+ * this Activity. Other applications that want to handle notes can "register" themselves in
+ * Android by providing an intent filter that includes the category ALTERNATIVE and the
+ * mimeTYpe NotePad.Notes.CONTENT_TYPE. If they do this, the code in onCreateOptionsMenu()
+ * will add the Activity that contains the intent filter to its list of options. In effect,
+ * the menu will offer the user other applications that can handle notes.
+ * @param menu A Menu object, to which menu items should be added.
+ * @return True, always. The menu should be displayed.
+ */
@Override
public boolean onCreateOptionsMenu(Menu menu) {
- super.onCreateOptionsMenu(menu);
- // This is our one standard application action -- inserting a
- // new note into the list.
- menu.add(0, MENU_ITEM_INSERT, 0, R.string.menu_insert)
- .setShortcut('3', 'a')
- .setIcon(android.R.drawable.ic_menu_add);
-
- // If there is currently data in the clipboard, we can paste it
- // as a new note.
- mPasteItem = menu.add(0, MENU_ITEM_PASTE, 0, R.string.menu_paste)
- .setShortcut('4', 'p');
+ // The parent method creates the default method with the standard system items.
+ super.onCreateOptionsMenu(menu);
- // Generate any additional actions that can be performed on the
+ // Adds an Insert menu item
+ MenuItem menuItem = menu.add(
+ Menu.NONE, // No menu group.
+ MENU_ITEM_INSERT, // Unique ID for this item.
+ Menu.NONE, // No order within the group.
+ R.string.menu_insert // Displayed text for the menu item.
+ );
+
+ // Sets keyboard shortcuts for the menu item, either "3" or "a";
+ menuItem.setShortcut('3', 'a');
+
+ // Sets the icon for the menu item
+ menuItem.setIcon(android.R.drawable.ic_menu_add);
+
+ // If there is currently data in the clipboard, this adds a PASTE menu item to the menu
+ // so that the user can paste in the data.
+ mPasteItem = menu.add(
+ Menu.NONE, // No menu group.
+ MENU_ITEM_PASTE, // A unique ID for this item
+ Menu.NONE, // No menu order
+ R.string.menu_paste // The displayed text
+ ).setShortcut('4', 'p'); // Set the keyboard shortcuts for this new item.
+
+ // Generates any additional actions that can be performed on the
// overall list. In a normal install, there are no additional
// actions found here, but this allows other applications to extend
// our menu with their own actions.
+
+ /* Creates a new Intent with the same incoming data and no defined action.
+ * It also sets its category to ALTERNATIVE. This prepares the Intent as a place
+ * to group alternative options in the menu.
+ */
Intent intent = new Intent(null, getIntent().getData());
intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
- menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0,
- new ComponentName(this, NotesList.class), null, intent, 0, null);
+ /* Creates a ComponentName from the current Context and this Activity object's
+ * class object.
+ */
+ ComponentName component = new ComponentName(this, NotesList.class);
+
+ /*
+ * Adds any other activities that want to be alternatives for this view. In effect,
+ * any application can add itself as an alternative on the options menu.
+ */
+ menu.addIntentOptions(
+ Menu.CATEGORY_ALTERNATIVE, // Add the options to the Alternatives group
+ Menu.NONE, // Do not use a unique ID
+ Menu.NONE, // No need to order the options
+ component, // The ComponentName of the Activity making the request.
+ // This Activity is excluded from the list of alternatives.
+ null, // No specific items are listed first.
+ intent, // The Intent to resolve to, in effect, an Intent listing
+ // the alternatives
+ Menu.NONE, // no flags are needed
+ null // Since no specifics were used, so a menu item array is
+ // not needed.
+ );
+
+ // Returns true so that the menu is displayed.
return true;
}
@@ -126,12 +232,17 @@ public class NotesList extends ListActivity {
// The paste menu item is enabled if there is data on the clipboard.
ClipboardManager clipboard = (ClipboardManager)
getSystemService(Context.CLIPBOARD_SERVICE);
+
+ // If the clipboard contains an item, enables the Paste option on the menu.
if (clipboard.hasPrimaryClip()) {
mPasteItem.setEnabled(true);
} else {
+
+ // If the clipboard is empty, disables the menu's Paste option.
mPasteItem.setEnabled(false);
}
+ // Gets the number of notes currently being displayed.
final boolean haveItems = getListAdapter().getCount() > 0;
// If there are any notes in the list (which implies that one of
@@ -140,97 +251,261 @@ public class NotesList extends ListActivity {
// of our own specific actions along with any extensions that can be
// found.
if (haveItems) {
+
// This is the selected item.
Uri uri = ContentUris.withAppendedId(getIntent().getData(), getSelectedItemId());
- // Build menu... always starts with the EDIT action...
+ // Creates an array of Intents with one element. This will be used to send an Intent
+ // based on the selected menu item.
Intent[] specifics = new Intent[1];
+
+ // Sets the Intent in the array to be an EDIT action on the URI of the selected note.
specifics[0] = new Intent(Intent.ACTION_EDIT, uri);
+
+ // Creates an array of menu items with one element. This will contain the EDIT option.
MenuItem[] items = new MenuItem[1];
- // ... is followed by whatever other actions are available...
+ // Creates an Intent with no specific action, using the URI of the selected note.
Intent intent = new Intent(null, uri);
+
+ /* Adds the category ALTERNATIVE to the Intent, with the note ID URI as its
+ * data. This prepares the Intent as a place to group alternative options in the
+ * menu.
+ */
intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
- menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0, null, specifics, intent, 0,
- items);
- // Give a shortcut to the edit action.
- if (items[0] != null) {
- items[0].setShortcut('1', 'e');
+
+ /*
+ * Add alternatives to the menu
+ */
+ menu.addIntentOptions(
+ Menu.CATEGORY_ALTERNATIVE, // Add the Intents as options in the alternatives group.
+ Menu.NONE, // A unique item ID is not required.
+ Menu.NONE, // The alternatives don't need to be in order.
+ null, // The caller's name is not excluded from the group.
+ specifics, // These specific options must appear first.
+ intent, // These Intent objects map to the options in specifics.
+ Menu.NONE, // No flags are required.
+ items // The menu items generated from the specifics-to-
+ // Intents mapping
+ );
+ // If the Edit menu item exists, adds shortcuts for it.
+ if (items[0] != null) {
+
+ // Sets the Edit menu item shortcut to numeric "1", letter "e"
+ items[0].setShortcut('1', 'e');
+ }
+ } else {
+ // If the list is empty, removes any existing alternative actions from the menu
+ menu.removeGroup(Menu.CATEGORY_ALTERNATIVE);
}
- } else {
- menu.removeGroup(Menu.CATEGORY_ALTERNATIVE);
- }
+ // Displays the menu
return true;
}
+ /**
+ * This method is called when the user selects an option from the menu, but no item
+ * in the list is selected. If the option was INSERT, then a new Intent is sent out with action
+ * ACTION_INSERT. The data from the incoming Intent is put into the new Intent. In effect,
+ * this triggers the NoteEditor activity in the NotePad application.
+ *
+ * If the item was not INSERT, then most likely it was an alternative option from another
+ * application. The parent method is called to process the item.
+ * @param item The menu item that was selected by the user
+ * @return True, if the INSERT menu item was selected; otherwise, the result of calling
+ * the parent method.
+ */
@Override
public boolean onOptionsItemSelected(MenuItem item) {
+
+ // Gets the ID of the selected item
switch (item.getItemId()) {
+
+ // If the menu item is Insert
case MENU_ITEM_INSERT:
- // Launch activity to insert a new item
+
+ /*
+ * Launches a new Activity using an Intent. The intent filter for the Activity
+ * has to have action ACTION_INSERT. No category is set, so DEFAULT is assumed.
+ * In effect, this starts the NoteEditor Activity in NotePad.
+ */
startActivity(new Intent(Intent.ACTION_INSERT, getIntent().getData()));
return true;
+
+ // If the menu item is Paste
case MENU_ITEM_PASTE:
- // Launch activity to insert a new item
+
+ /*
+ * Launches a new Activity using an Intent. The intent filter for the Activity
+ * has to have action ACTION_PASTE. No category is set, so DEFAULT is assumed.
+ * In effect, this starts the NoteEditor Activity in NotePad.
+ */
startActivity(new Intent(Intent.ACTION_PASTE, getIntent().getData()));
return true;
}
+
+ // If the menu item selected is not Insert or Paste, then this calls the regular
+ // processing to handle the item.
return super.onOptionsItemSelected(item);
}
+ /**
+ * This method is called when the user context-clicks a note in the list. NotesList registers
+ * itself as the handler for context menus in its ListView (this is done in onCreate()).
+ *
+ * The only available options are COPY and DELETE.
+ *
+ * Context-click is equivalent to long-press.
+ *
+ * @param menu A ContexMenu object to which items should be added.
+ * @param view The View for which the context menu is being constructed.
+ * @param menuInfo Data associated with view.
+ * @throws ClassCastException
+ */
@Override
public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
+
+ // The data from the menu item.
AdapterView.AdapterContextMenuInfo info;
+
+ // Tries to get the position of the item in the ListView that was long-pressed.
try {
- info = (AdapterView.AdapterContextMenuInfo) menuInfo;
+
+ // Casts the incoming data object into the type for AdapterView objects.
+ info = (AdapterView.AdapterContextMenuInfo) menuInfo;
} catch (ClassCastException e) {
+
+ // If the menu object can't be cast, logs an error.
Log.e(TAG, "bad menuInfo", e);
return;
}
+ /*
+ * Gets the data associated with the item at the selected position. getItem() returns
+ * whatever the backing adapter of the ListView has associated with the item. In NotesList,
+ * the adapter associated all of the data for a note with its list item. As a result,
+ * getItem() returns that data as a Cursor.
+ */
Cursor cursor = (Cursor) getListAdapter().getItem(info.position);
+
+ // If the cursor is empty, then for some reason the adapter can't get the data from the
+ // provider, so returns null to the caller.
if (cursor == null) {
+
// For some reason the requested item isn't available, do nothing
return;
}
- // Setup the menu header
+ // Sets the menu header to be the title of the selected note.
menu.setHeaderTitle(cursor.getString(COLUMN_INDEX_TITLE));
- // Add a menu item to copy the note
- menu.add(0, MENU_ITEM_COPY, 0, R.string.menu_copy);
-
- // Add a menu item to delete the note
- menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_delete);
+ // Adds a menu item to copy the note to the context menu
+ menu.add(
+ Menu.NONE, // No grouping is needed for this menu
+ MENU_ITEM_COPY, // A unique ID for this menu item.
+ Menu.NONE, // No ordering is necessary in this menu
+ R.string.menu_copy // The resource ID for the string to display for this item.
+ );
+
+ // Adds a menu item to delete the note to the context menu
+ menu.add(
+ Menu.NONE, // No grouping is needed for this menu
+ MENU_ITEM_DELETE, // A unique ID for this menu item.
+ Menu.NONE, // No ordering is necessary in this menu
+ R.string.menu_delete // The resource ID for the string to display for this item.
+ );
}
-
+
+ /**
+ * This method is called when the user selects an item from the context menu
+ * (see onCreateContextMenu()). The only menu items that are actually handled are DELETE and
+ * COPY. Anything else is an alternative option, for which default handling should be done.
+ *
+ * @param item The selected menu item
+ * @return True if the menu item was DELETE, and no default processing is need, otherwise false,
+ * which triggers the default handling of the item.
+ * @throws ClassCastException
+ */
@Override
public boolean onContextItemSelected(MenuItem item) {
+
+ // The data from the menu item.
AdapterView.AdapterContextMenuInfo info;
+
+ /*
+ * Gets the extra info from the menu item. When an note in the Notes list is long-pressed, a
+ * context menu appears. The menu items for the menu automatically get the data
+ * associated with the note that was long-pressed. The data comes from the provider that
+ * backs the list.
+ *
+ * The note's data is passed to the context menu creation routine in a ContextMenuInfo
+ * object.
+ *
+ * When one of the context menu items is clicked, the same data is passed, along with the
+ * note ID, to onContextItemSelected() via the item parameter.
+ */
try {
- info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
+
+ // Casts the data object in the item into the type for AdapterView objects.
+ info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
} catch (ClassCastException e) {
+
+ // If the object can't be cast, logs an error
Log.e(TAG, "bad menuInfo", e);
+
+ // Triggers default processing of the menu item.
return false;
}
+ /*
+ * Gets the menu item's ID and compares it to known actions. The only actions that are
+ * implemented are COPY and DELETE (set in onCreateContextMenu()).
+ */
switch (item.getItemId()) {
+
+ // Deletes the selected note
case MENU_ITEM_DELETE: {
- // Delete the note that the context menu is for
+
+ // Appends the selected note's ID to the URI sent with the incoming Intent.
Uri noteUri = ContentUris.withAppendedId(getIntent().getData(), info.id);
- getContentResolver().delete(noteUri, null, null);
+
+ // Deletes the note from the provider by passing in a URI in note ID format.
+ // Please see the introductory note about performing provider operations on the
+ // UI thread.
+ getContentResolver().delete(
+ noteUri, // The URI of the provider
+ null, // No where clause is needed, since only a single note ID is being
+ // passed in.
+ null // No where clause is used, so no where arguments are needed.
+ );
+
+ // Returns to the caller and skips further processing.
return true;
}
+
//BEGIN_INCLUDE(copy)
+
+ // Copies the selected note to the clipboard
case MENU_ITEM_COPY: {
- // Copy the note that the context menu is for on to the clipboard
+
+ // Gets a handle to the clipboard service.
ClipboardManager clipboard = (ClipboardManager)
getSystemService(Context.CLIPBOARD_SERVICE);
+
+ // Appends the selected note's ID to the URI sent with the incoming Intent.
Uri noteUri = ContentUris.withAppendedId(getIntent().getData(), info.id);
- clipboard.setPrimaryClip(new ClippedData(null, null, new ClippedData.Item(
- noteUri)));
+
+ // Copies the notes URI to the clipboard. In effect, this copies the note itself
+ clipboard.setPrimaryClip(
+ new ClippedData( // creates a new clipboard item
+ null, // no visible label
+ null, // no visible icon
+ new ClippedData.Item(noteUri) // A clipboard Item that is a URI.
+ )
+ );
+
+ // Returns to the caller and skips further processing.
return true;
}
//END_INCLUDE(copy)
@@ -238,17 +513,36 @@ public class NotesList extends ListActivity {
return false;
}
+ /**
+ * This method is called when the user clicks a note in the displayed list.
+ *
+ * This method handles incoming actions of either PICK (get data from the provider) or
+ * GET_CONTENT (get or create data). If the incoming action is EDIT, this method sends a
+ * new Intent to start NoteEditor.
+ * @param l The ListView that contains the clicked item
+ * @param v The View of the individual item
+ * @param position The position of v in the displayed list
+ * @param id The row ID of the clicked item
+ */
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
+
+ // Constructs a new URI from the incoming URI and the row ID
Uri uri = ContentUris.withAppendedId(getIntent().getData(), id);
-
+
+ // Gets the action from the incoming Intent
String action = getIntent().getAction();
+
+ // Handles requests for note data
if (Intent.ACTION_PICK.equals(action) || Intent.ACTION_GET_CONTENT.equals(action)) {
- // The caller is waiting for us to return a note selected by
- // the user. The have clicked on one, so return it now.
+
+ // Sets the result to return to the component that called this Activity. The
+ // result contains the new URI
setResult(RESULT_OK, new Intent().setData(uri));
} else {
- // Launch activity to view/edit the currently selected item
+
+ // Sends out an Intent to start an Activity that can handle ACTION_EDIT. The
+ // Intent's data is the note ID URI. The effect is to call NoteEdit.
startActivity(new Intent(Intent.ACTION_EDIT, uri));
}
}
diff --git a/samples/NotePad/src/com/example/android/notepad/NotesLiveFolder.java b/samples/NotePad/src/com/example/android/notepad/NotesLiveFolder.java
index 00eb31459..24afaa0d4 100644
--- a/samples/NotePad/src/com/example/android/notepad/NotesLiveFolder.java
+++ b/samples/NotePad/src/com/example/android/notepad/NotesLiveFolder.java
@@ -16,50 +16,98 @@
package com.example.android.notepad;
+import com.example.android.notepad.NotePad;
+
import android.app.Activity;
import android.content.Intent;
-import android.net.Uri;
+import android.content.Intent.ShortcutIconResource;
import android.os.Bundle;
import android.provider.LiveFolders;
+/**
+ * This Activity creates a live folder Intent and
+ * sends it back to HOME. From the data in the Intent, HOME creates a live folder and displays
+ * its icon in the Home view.
+ * When the user clicks the icon, Home uses the data it got from the Intent to retrieve information
+ * from a content provider and display it in a View.
+ *
+ * The intent filter for this Activity is set to ACTION_CREATE_LIVE_FOLDER, which
+ * HOME sends in response to a long press and selection of Live Folder.
+ */
public class NotesLiveFolder extends Activity {
+
/**
- * The URI for the Notes Live Folder content provider.
+ * All of the work is done in onCreate(). The Activity doesn't actually display a UI.
+ * Instead, it sets up an Intent and returns it to its caller (the HOME activity).
*/
- public static final Uri CONTENT_URI = Uri.parse("content://"
- + NotePad.AUTHORITY + "/live_folders/notes");
-
- public static final Uri NOTE_URI = Uri.parse("content://"
- + NotePad.AUTHORITY + "/notes/#");
-
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ /*
+ * Gets the incoming Intent and its action. If the incoming Intent was
+ * ACTION_CREATE_LIVE_FOLDER, then create an outgoing Intent with the
+ * necessary data and send back OK. Otherwise, send back CANCEL.
+ */
final Intent intent = getIntent();
final String action = intent.getAction();
if (LiveFolders.ACTION_CREATE_LIVE_FOLDER.equals(action)) {
- // Build the live folder intent.
+
+ // Creates a new Intent.
final Intent liveFolderIntent = new Intent();
- liveFolderIntent.setData(CONTENT_URI);
- liveFolderIntent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_NAME,
- getString(R.string.live_folder_name));
- liveFolderIntent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_ICON,
- Intent.ShortcutIconResource.fromContext(this,
- R.drawable.live_folder_notes));
- liveFolderIntent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_DISPLAY_MODE,
+ /*
+ * The following statements put data into the outgoing Intent. Please see
+ * {@link android.provider.LiveFolders for a detailed description of these
+ * data values. From this data, HOME sets up a live folder.
+ */
+ // Sets the URI pattern for the content provider backing the folder.
+ liveFolderIntent.setData(NotePad.Notes.LIVE_FOLDER_URI);
+
+ // Adds the display name of the live folder as an Extra string.
+ String foldername = getString(R.string.live_folder_name);
+ liveFolderIntent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_NAME, foldername);
+
+ // Adds the display icon of the live folder as an Extra resource.
+ ShortcutIconResource foldericon =
+ Intent.ShortcutIconResource.fromContext(this, R.drawable.live_folder_notes);
+ liveFolderIntent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_ICON, foldericon);
+
+ // Add the display mode of the live folder as an integer. The specified
+ // mode causes the live folder to display as a list.
+ liveFolderIntent.putExtra(
+ LiveFolders.EXTRA_LIVE_FOLDER_DISPLAY_MODE,
LiveFolders.DISPLAY_MODE_LIST);
- liveFolderIntent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_BASE_INTENT,
- new Intent(Intent.ACTION_EDIT, NOTE_URI));
- // The result of this activity should be a live folder intent.
+ /*
+ * Adds a base action for items in the live folder list, as an Intent. When the
+ * user clicks an individual note in the list, the live folder fires this Intent.
+ *
+ * Its action is ACTION_EDIT, so it triggers the Note Editor activity. Its
+ * data is the URI pattern for a single note identified by its ID. The live folder
+ * automatically adds the ID value of the selected item to the URI pattern.
+ *
+ * As a result, Note Editor is triggered and gets a single note to retrieve by ID.
+ */
+ Intent returnIntent
+ = new Intent(Intent.ACTION_EDIT, NotePad.Notes.CONTENT_ID_URI_PATTERN);
+ liveFolderIntent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_BASE_INTENT, returnIntent);
+
+ /* Creates an ActivityResult object to propagate back to HOME. Set its result indicator
+ * to OK, and sets the returned Intent to the live folder Intent that was just
+ * constructed.
+ */
setResult(RESULT_OK, liveFolderIntent);
+
} else {
+
+ // If the original action was not ACTION_CREATE_LIVE_FOLDER, creates an
+ // ActivityResult with the indicator set to CANCELED, but do not return an Intent
setResult(RESULT_CANCELED);
}
+ // Closes the Activity. The ActivityObject is propagated back to the caller.
finish();
}
}
diff --git a/samples/NotePad/src/com/example/android/notepad/TitleEditor.java b/samples/NotePad/src/com/example/android/notepad/TitleEditor.java
index 50d38e5f6..743759114 100644
--- a/samples/NotePad/src/com/example/android/notepad/TitleEditor.java
+++ b/samples/NotePad/src/com/example/android/notepad/TitleEditor.java
@@ -16,7 +16,7 @@
package com.example.android.notepad;
-import com.example.android.notepad.NotePad.Notes;
+import com.example.android.notepad.NotePad;
import android.app.Activity;
import android.content.ContentValues;
@@ -28,8 +28,13 @@ import android.widget.Button;
import android.widget.EditText;
/**
- * An activity that will edit the title of a note. Displays a floating
- * window with a text field.
+ * This Activity allows the user to edit a note's title. It displays a floating window
+ * containing an EditText.
+ *
+ * NOTE: Notice that the provider operations in this Activity are taking place on the UI thread.
+ * This is not a good practice. It is only done here to make the code more readable. A real
+ * application should use the {@link android.content.AsyncQueryHandler}
+ * or {@link android.os.AsyncTask} object to perform operations asynchronously on a separate thread.
*/
public class TitleEditor extends Activity implements View.OnClickListener {
@@ -38,78 +43,141 @@ public class TitleEditor extends Activity implements View.OnClickListener {
*/
public static final String EDIT_TITLE_ACTION = "com.android.notepad.action.EDIT_TITLE";
- /**
- * An array of the columns we are interested in.
- */
+ // Creates a projection that returns the note ID and the note contents.
private static final String[] PROJECTION = new String[] {
NotePad.Notes._ID, // 0
- NotePad.Notes.TITLE, // 1
+ NotePad.Notes.COLUMN_NAME_TITLE, // 1
};
- /** Index of the title column */
+
+ // The position of the title column in a Cursor returned by the provider.
private static final int COLUMN_INDEX_TITLE = 1;
- /**
- * Cursor which will provide access to the note whose title we are editing.
- */
+ // A Cursor object that will contain the results of querying the provider for a note.
private Cursor mCursor;
- /**
- * The EditText field from our UI. Keep track of this so we can extract the
- * text when we are finished.
- */
+ // An EditText object for preserving the edited title.
private EditText mText;
- /**
- * The content URI to the note that's being edited.
- */
+ // A URI object for the note whose title is being edited.
private Uri mUri;
+ /**
+ * This method is called by Android when the Activity is first started. From the incoming
+ * Intent, it determines what kind of editing is desired, and then does it.
+ */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ // Set the View for this Activity object's UI.
setContentView(R.layout.title_editor);
- // Get the uri of the note whose title we want to edit
+ // Get the Intent that activated this Activity, and from it get the URI of the note whose
+ // title we need to edit.
mUri = getIntent().getData();
- // Get a cursor to access the note
- mCursor = managedQuery(mUri, PROJECTION, null, null, null);
-
- // Set up click handlers for the text field and button
+ /*
+ * Using the URI passed in with the triggering Intent, gets the note.
+ *
+ * Note: This is being done on the UI thread. It will block the thread until the query
+ * completes. In a sample app, going against a simple provider based on a local database,
+ * the block will be momentary, but in a real app you should use
+ * android.content.AsyncQueryHandler or android.os.AsyncTask.
+ */
+ mCursor = managedQuery(
+ mUri, // The URI for the note that is to be retrieved.
+ PROJECTION, // The columns to retrieve
+ null, // No selection criteria are used, so no where columns are needed.
+ null, // No where columns are used, so no where values are needed.
+ null // No sort order is needed.
+ );
+
+ // Sets up a listener for the EditText. Gets the EditText by its ID, then sets its
+ // onClickListener to this Activity.
mText = (EditText) this.findViewById(R.id.title);
mText.setOnClickListener(this);
-
+
+ // Sets up a listener for the OK button. Gets the Button by its ID, then sets its
+ // onClickListener to this Activity.
Button b = (Button) findViewById(R.id.ok);
b.setOnClickListener(this);
}
+ /**
+ * This method is called when the Activity is about to come to the foreground. This happens
+ * when the Activity comes to the top of the task stack, OR when it is first starting.
+ *
+ * Displays the current title for the selected note.
+ */
@Override
protected void onResume() {
super.onResume();
- // Initialize the text with the title column from the cursor
+ // Verifies that the query made in onCreate() actually worked. If it worked, then the
+ // Cursor object is not null. If it is *empty*, then mCursor.getCount() == 0.
if (mCursor != null) {
+
+ // The Cursor was just retrieved, so its index is set to one record *before* the first
+ // record retrieved. This moves it to the first record.
mCursor.moveToFirst();
+
+ // Displays the current title text in the EditText object.
mText.setText(mCursor.getString(COLUMN_INDEX_TITLE));
}
}
+ /**
+ * This method is called when the Activity loses focus.
+ *
+ * For Activity objects that edit information, onPause() may be the one place where changes are
+ * saved. The Android application model is predicated on the idea that "save" and "exit" aren't
+ * required actions. When users navigate away from an Activity, they shouldn't have to go back
+ * to it to complete their work. The act of going away should save everything and leave the
+ * Activity in a state where Android can destroy it if necessary.
+ *
+ * Updates the note with the text currently in the text box.
+ */
@Override
protected void onPause() {
super.onPause();
+ // Verifies that the query made in onCreate() actually worked. If it worked, then the
+ // Cursor object is not null. If it is *empty*, then mCursor.getCount() == 0.
if (mCursor != null) {
- // Write the title back to the note
+
+ // Creates a values map for updating the provider.
ContentValues values = new ContentValues();
- values.put(Notes.TITLE, mText.getText().toString());
- getContentResolver().update(mUri, values, null, null);
+
+ // In the values map, sets the title to the current contents of the edit box.
+ values.put(NotePad.Notes.COLUMN_NAME_TITLE, mText.getText().toString());
+
+ /*
+ * Updates the provider with the note's new title.
+ *
+ * Note: This is being done on the UI thread. It will block the thread until the
+ * update completes. In a sample app, going against a simple provider based on a
+ * local database, the block will be momentary, but in a real app you should use
+ * android.content.AsyncQueryHandler or android.os.AsyncTask.
+ */
+ getContentResolver().update(
+ mUri, // The URI for the note to update.
+ values, // The values map containing the columns to update and the values to use.
+ null, // No selection criteria is used, so no "where" columns are needed.
+ null // No "where" columns are used, so no "where" values are needed.
+ );
}
}
+ /**
+ * This method is called when the user clicks anywhere in the title text box.
+ *
+ * It calls finish(), which immediately triggers the onPause() method in this Activity. In
+ * turn, onPause() saves the text currently in the title text box to the note.
+ */
public void onClick(View v) {
- // When the user clicks, just finish this activity.
- // onPause will be called, and we save our data there.
+
+ // Calls finish to force the Activity to shut down. In the lifecycle, this forces a call to
+ // onPause(), which saves the work the user has done.
finish();
}
}
diff --git a/samples/NotePad/src/com/google/provider/NotePad.java b/samples/NotePad/src/com/google/provider/NotePad.java
deleted file mode 100644
index f8de69b6b..000000000
--- a/samples/NotePad/src/com/google/provider/NotePad.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright (C) 2007 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.provider;
-
-import android.net.Uri;
-import android.provider.BaseColumns;
-
-/**
- * Convenience definitions for NotePadProvider
- */
-public final class NotePad {
- /**
- * Notes table
- */
- public static final class Notes implements BaseColumns {
- /**
- * The content:// style URL for this table
- */
- public static final Uri CONTENT_URI
- = Uri.parse("content://com.google.provider.NotePad/notes");
-
- /**
- * The default sort order for this table
- */
- public static final String DEFAULT_SORT_ORDER = "modified DESC";
-
- /**
- * The title of the note
- * <P>Type: TEXT</P>
- */
- public static final String TITLE = "title";
-
- /**
- * The note itself
- * <P>Type: TEXT</P>
- */
- public static final String NOTE = "note";
-
- /**
- * The timestamp for when the note was created
- * <P>Type: INTEGER (long)</P>
- */
- public static final String CREATED_DATE = "created";
-
- /**
- * The timestamp for when the note was last modified
- * <P>Type: INTEGER (long)</P>
- */
- public static final String MODIFIED_DATE = "modified";
- }
-}
diff --git a/samples/NotePad/tests/Android.mk b/samples/NotePad/tests/Android.mk
deleted file mode 100644
index 43efafcf0..000000000
--- a/samples/NotePad/tests/Android.mk
+++ /dev/null
@@ -1,14 +0,0 @@
-LOCAL_PATH:= $(call my-dir)
-include $(CLEAR_VARS)
-
-LOCAL_SRC_FILES := $(call all-subdir-java-files)
-
-LOCAL_JAVA_LIBRARIES := android.test.runner
-
-LOCAL_PACKAGE_NAME := NotePadTests
-
-LOCAL_MODULE_TAGS := tests
-
-LOCAL_INSTRUMENTATION_FOR := NotePad
-
-include $(BUILD_PACKAGE)
diff --git a/samples/NotePad/tests/build.properties b/samples/NotePad/tests/build.properties
deleted file mode 100644
index e0c39def1..000000000
--- a/samples/NotePad/tests/build.properties
+++ /dev/null
@@ -1 +0,0 @@
-tested.project.dir=..
diff --git a/samples/NotePad/tests/src/com/example/android/notepad/NotePadTest.java b/samples/NotePad/tests/src/com/example/android/notepad/NotePadActivityTest.java
index 80f71d289..c8acdf043 100644
--- a/samples/NotePad/tests/src/com/example/android/notepad/NotePadTest.java
+++ b/samples/NotePad/tests/src/com/example/android/notepad/NotePadActivityTest.java
@@ -24,13 +24,13 @@ import com.example.android.notepad.NotesList;
* Make sure that the main launcher activity opens up properly, which will be
* verified by {@link #testActivityTestCaseSetUpProperly}.
*/
-public class NotePadTest extends ActivityInstrumentationTestCase2<NotesList> {
+public class NotePadActivityTest extends ActivityInstrumentationTestCase2<NotesList> {
/**
* Creates an {@link ActivityInstrumentationTestCase2} for the {@link NotesList} activity.
*/
- public NotePadTest() {
- super(NotesList.class);
+ public NotePadActivityTest() {
+ super("com.example.android.notepad.test",NotesList.class);
}
/**
diff --git a/samples/NotePad/tests/src/com/example/android/notepad/NotePadProviderTest.java b/samples/NotePad/tests/src/com/example/android/notepad/NotePadProviderTest.java
new file mode 100644
index 000000000..4790e4273
--- /dev/null
+++ b/samples/NotePad/tests/src/com/example/android/notepad/NotePadProviderTest.java
@@ -0,0 +1,585 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.notepad;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.test.ProviderTestCase2;
+import android.test.mock.MockContentResolver;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+
+/*
+ */
+/**
+ * This class tests the content provider for the Note Pad sample application.
+ *
+ * To learn how to run an entire test package or one of its classes, please see
+ * "Testing in Eclipse, with ADT" or "Testing in Other IDEs" in the Developer Guide.
+ */
+public class NotePadProviderTest extends ProviderTestCase2<NotePadProvider> {
+
+ // A URI that the provider does not offer, for testing error handling.
+ private static final Uri INVALID_URI =
+ Uri.withAppendedPath(NotePad.Notes.CONTENT_URI, "invalid");
+
+ // Contains a reference to the mocked content resolver for the provider under test.
+ private MockContentResolver mMockResolver;
+
+ // Contains an SQLite database, used as test data
+ private SQLiteDatabase mDb;
+
+ // Contains the test data, as an array of NoteInfo instances.
+ private final NoteInfo[] TEST_NOTES = {
+ new NoteInfo("Note0", "This is note 0"),
+ new NoteInfo("Note1", "This is note 1"),
+ new NoteInfo("Note2", "This is note 2"),
+ new NoteInfo("Note3", "This is note 3"),
+ new NoteInfo("Note4", "This is note 4"),
+ new NoteInfo("Note5", "This is note 5"),
+ new NoteInfo("Note6", "This is note 6"),
+ new NoteInfo("Note7", "This is note 7"),
+ new NoteInfo("Note8", "This is note 8"),
+ new NoteInfo("Note9", "This is note 9") };
+
+ // Number of milliseconds in one day (milliseconds * seconds * minutes * hours)
+ private static final long ONE_DAY_MILLIS = 1000 * 60 * 60 * 24;
+
+ // Number of milliseconds in one week
+ private static final long ONE_WEEK_MILLIS = ONE_DAY_MILLIS * 7;
+
+ // Creates a calendar object equal to January 1, 2010 at 12 midnight
+ private static final GregorianCalendar TEST_CALENDAR =
+ new GregorianCalendar(2010, Calendar.JANUARY, 1, 0, 0, 0);
+
+ // Stores a timestamp value, set to an arbitrary starting point
+ private final static long START_DATE = TEST_CALENDAR.getTimeInMillis();
+
+ /*
+ * Constructor for the test case class.
+ * Calls the super constructor with the class name of the provider under test and the
+ * authority name of the provider.
+ */
+ public NotePadProviderTest() {
+ super(NotePadProvider.class, NotePad.AUTHORITY);
+ }
+
+
+ /*
+ * Sets up the test environment before each test method. Creates a mock content resolver,
+ * gets the provider under test, and creates a new database for the provider.
+ */
+ @Override
+ protected void setUp() throws Exception {
+ // Calls the base class implementation of this method.
+ super.setUp();
+
+ // Gets the resolver for this test.
+ mMockResolver = getMockContentResolver();
+
+ /*
+ * Gets a handle to the database underlying the provider. Gets the provider instance
+ * created in super.setUp(), gets the DatabaseOpenHelper for the provider, and gets
+ * a database object from the helper.
+ */
+ mDb = getProvider().getOpenHelperForTest().getWritableDatabase();
+ }
+
+ /*
+ * This method is called after each test method, to clean up the current fixture. Since
+ * this sample test case runs in an isolated context, no cleanup is necessary.
+ */
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ /*
+ * Sets up test data.
+ * The test data is in an SQL database. It is created in setUp() without any data,
+ * and populated in insertData if necessary.
+ */
+ private void insertData() {
+ // Creates an instance of the ContentValues map type expected by database insertions
+ ContentValues values = new ContentValues();
+
+ // Sets up test data
+ for (int index = 0; index < TEST_NOTES.length; index++) {
+
+ // Set the creation and modification date for the note
+ TEST_NOTES[index].setCreationDate(START_DATE + (index * ONE_DAY_MILLIS));
+ TEST_NOTES[index].setModificationDate(START_DATE + (index * ONE_WEEK_MILLIS));
+
+ // Adds a record to the database.
+ mDb.insertOrThrow(
+ NotePad.Notes.TABLE_NAME, // the table name for the insert
+ NotePad.Notes.COLUMN_NAME_TITLE, // column set to null if empty values map
+ TEST_NOTES[index].getContentValues() // the values map to insert
+ );
+ }
+ }
+
+ /*
+ * Tests the provider's publicly available URIs. If the URI is not one that the provider
+ * understands, the provider should throw an exception. It also tests the provider's getType()
+ * method for each URI, which should return the MIME type associated with the URI.
+ */
+ public void testUriAndGetType() {
+ // Tests the MIME type for the notes table URI.
+ String mimeType = mMockResolver.getType(NotePad.Notes.CONTENT_URI);
+ assertEquals(NotePad.Notes.CONTENT_TYPE, mimeType);
+
+ // Tests the MIME type for the live folder URI.
+ mimeType = mMockResolver.getType(NotePad.Notes.LIVE_FOLDER_URI);
+ assertEquals(NotePad.Notes.CONTENT_TYPE, mimeType);
+
+ // Creates a URI with a pattern for note ids. The id doesn't have to exist.
+ Uri noteIdUri = ContentUris.withAppendedId(NotePad.Notes.CONTENT_ID_URI_BASE, 1);
+
+ // Gets the note ID URI MIME type.
+ mimeType = mMockResolver.getType(noteIdUri);
+ assertEquals(NotePad.Notes.CONTENT_ITEM_TYPE, mimeType);
+
+ // Tests an invalid URI. This should throw an IllegalArgumentException.
+ mimeType = mMockResolver.getType(INVALID_URI);
+ }
+
+ /*
+ * Tests the provider's public API for querying data in the table, using the URI for
+ * a dataset of records.
+ */
+ public void testQueriesOnNotesUri() {
+ // Defines a projection of column names to return for a query
+ final String[] TEST_PROJECTION = {
+ NotePad.Notes.COLUMN_NAME_TITLE,
+ NotePad.Notes.COLUMN_NAME_NOTE,
+ NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE
+ };
+
+ // Defines a selection column for the query. When the selection columns are passed
+ // to the query, the selection arguments replace the placeholders.
+ final String TITLE_SELECTION = NotePad.Notes.COLUMN_NAME_TITLE + " = " + "?";
+
+ // Defines the selection columns for a query.
+ final String SELECTION_COLUMNS =
+ TITLE_SELECTION + " OR " + TITLE_SELECTION + " OR " + TITLE_SELECTION;
+
+ // Defines the arguments for the selection columns.
+ final String[] SELECTION_ARGS = { "Note0", "Note1", "Note5" };
+
+ // Defines a query sort order
+ final String SORT_ORDER = NotePad.Notes.COLUMN_NAME_TITLE + " ASC";
+
+ // Query subtest 1.
+ // If there are no records in the table, the returned cursor from a query should be empty.
+ Cursor cursor = mMockResolver.query(
+ NotePad.Notes.CONTENT_URI, // the URI for the main data table
+ null, // no projection, get all columns
+ null, // no selection criteria, get all records
+ null, // no selection arguments
+ null // use default sort order
+ );
+
+ // Asserts that the returned cursor contains no records
+ assertEquals(0, cursor.getCount());
+
+ // Query subtest 2.
+ // If the table contains records, the returned cursor from a query should contain records.
+
+ // Inserts the test data into the provider's underlying data source
+ insertData();
+
+ // Gets all the columns for all the rows in the table
+ cursor = mMockResolver.query(
+ NotePad.Notes.CONTENT_URI, // the URI for the main data table
+ null, // no projection, get all columns
+ null, // no selection criteria, get all records
+ null, // no selection arguments
+ null // use default sort order
+ );
+
+ // Asserts that the returned cursor contains the same number of rows as the size of the
+ // test data array.
+ assertEquals(TEST_NOTES.length, cursor.getCount());
+
+ // Query subtest 3.
+ // A query that uses a projection should return a cursor with the same number of columns
+ // as the projection, with the same names, in the same order.
+ Cursor projectionCursor = mMockResolver.query(
+ NotePad.Notes.CONTENT_URI, // the URI for the main data table
+ TEST_PROJECTION, // get the title, note, and mod date columns
+ null, // no selection columns, get all the records
+ null, // no selection criteria
+ null // use default the sort order
+ );
+
+ // Asserts that the number of columns in the cursor is the same as in the projection
+ assertEquals(TEST_PROJECTION.length, projectionCursor.getColumnCount());
+
+ // Asserts that the names of the columns in the cursor and in the projection are the same.
+ // This also verifies that the names are in the same order.
+ assertEquals(TEST_PROJECTION[0], projectionCursor.getColumnName(0));
+ assertEquals(TEST_PROJECTION[1], projectionCursor.getColumnName(1));
+ assertEquals(TEST_PROJECTION[2], projectionCursor.getColumnName(2));
+
+ // Query subtest 4
+ // A query that uses selection criteria should return only those rows that match the
+ // criteria. Use a projection so that it's easy to get the data in a particular column.
+ projectionCursor = mMockResolver.query(
+ NotePad.Notes.CONTENT_URI, // the URI for the main data table
+ TEST_PROJECTION, // get the title, note, and mod date columns
+ SELECTION_COLUMNS, // select on the title column
+ SELECTION_ARGS, // select titles "Note0", "Note1", or "Note5"
+ SORT_ORDER // sort ascending on the title column
+ );
+
+ // Asserts that the cursor has the same number of rows as the number of selection arguments
+ assertEquals(SELECTION_ARGS.length, projectionCursor.getCount());
+
+ int index = 0;
+
+ while (projectionCursor.moveToNext()) {
+
+ // Asserts that the selection argument at the current index matches the value of
+ // the title column (column 0) in the current record of the cursor
+ assertEquals(SELECTION_ARGS[index], projectionCursor.getString(0));
+
+ index++;
+ }
+
+ // Asserts that the index pointer is now the same as the number of selection arguments, so
+ // that the number of arguments tested is exactly the same as the number of rows returned.
+ assertEquals(SELECTION_ARGS.length, index);
+
+ }
+
+ /*
+ * Tests queries against the provider, using the note id URI. This URI encodes a single
+ * record ID. The provider should only return 0 or 1 record.
+ */
+ public void testQueriesOnNoteIdUri() {
+ // Defines the selection column for a query. The "?" is replaced by entries in the
+ // selection argument array
+ final String SELECTION_COLUMNS = NotePad.Notes.COLUMN_NAME_TITLE + " = " + "?";
+
+ // Defines the argument for the selection column.
+ final String[] SELECTION_ARGS = { "Note1" };
+
+ // A sort order for the query.
+ final String SORT_ORDER = NotePad.Notes.COLUMN_NAME_TITLE + " ASC";
+
+ // Creates a projection includes the note id column, so that note id can be retrieved.
+ final String[] NOTE_ID_PROJECTION = {
+ NotePad.Notes._ID, // The Notes class extends BaseColumns,
+ // which includes _ID as the column name for the
+ // record's id in the data model
+ NotePad.Notes.COLUMN_NAME_TITLE}; // The note's title
+
+ // Query subtest 1.
+ // Tests that a query against an empty table returns null.
+
+ // Constructs a URI that matches the provider's notes id URI pattern, using an arbitrary
+ // value of 1 as the note ID.
+ Uri noteIdUri = ContentUris.withAppendedId(NotePad.Notes.CONTENT_ID_URI_BASE, 1);
+
+ // Queries the table with the notes ID URI. This should return an empty cursor.
+ Cursor cursor = mMockResolver.query(
+ noteIdUri, // URI pointing to a single record
+ null, // no projection, get all the columns for each record
+ null, // no selection criteria, get all the records in the table
+ null, // no need for selection arguments
+ null // default sort, by ascending title
+ );
+
+ // Asserts that the cursor is null.
+ assertEquals(0,cursor.getCount());
+
+ // Query subtest 2.
+ // Tests that a query against a table containing records returns a single record whose ID
+ // is the one requested in the URI provided.
+
+ // Inserts the test data into the provider's underlying data source.
+ insertData();
+
+ // Queries the table using the URI for the full table.
+ cursor = mMockResolver.query(
+ NotePad.Notes.CONTENT_URI, // the base URI for the table
+ NOTE_ID_PROJECTION, // returns the ID and title columns of rows
+ SELECTION_COLUMNS, // select based on the title column
+ SELECTION_ARGS, // select title of "Note1"
+ SORT_ORDER // sort order returned is by title, ascending
+ );
+
+ // Asserts that the cursor contains only one row.
+ assertEquals(1, cursor.getCount());
+
+ // Moves to the cursor's first row, and asserts that this did not fail.
+ assertTrue(cursor.moveToFirst());
+
+ // Saves the record's note ID.
+ int inputNoteId = cursor.getInt(0);
+
+ // Builds a URI based on the provider's content ID URI base and the saved note ID.
+ noteIdUri = ContentUris.withAppendedId(NotePad.Notes.CONTENT_ID_URI_BASE, inputNoteId);
+
+ // Queries the table using the content ID URI, which returns a single record with the
+ // specified note ID, matching the selection criteria provided.
+ cursor = mMockResolver.query(noteIdUri, // the URI for a single note
+ NOTE_ID_PROJECTION, // same projection, get ID and title columns
+ SELECTION_COLUMNS, // same selection, based on title column
+ SELECTION_ARGS, // same selection arguments, title = "Note1"
+ SORT_ORDER // same sort order returned, by title, ascending
+ );
+
+ // Asserts that the cursor contains only one row.
+ assertEquals(1, cursor.getCount());
+
+ // Moves to the cursor's first row, and asserts that this did not fail.
+ assertTrue(cursor.moveToFirst());
+
+ // Asserts that the note ID passed to the provider is the same as the note ID returned.
+ assertEquals(inputNoteId, cursor.getInt(0));
+ }
+
+ /*
+ * Tests inserts into the data model.
+ */
+ public void testInserts() {
+ // Creates a new note instance with ID of 30.
+ NoteInfo note = new NoteInfo(
+ "Note30", // the note's title
+ "Test inserting a note" // the note's content
+ );
+
+ // Sets the note's creation and modification times
+ note.setCreationDate(START_DATE + (10 * ONE_DAY_MILLIS));
+ note.setModificationDate(START_DATE + (2 * ONE_WEEK_MILLIS));
+
+ // Insert subtest 1.
+ // Inserts a row using the new note instance.
+ // No assertion will be done. The insert() method either works or throws an Exception
+ Uri rowUri = mMockResolver.insert(
+ NotePad.Notes.CONTENT_URI, // the main table URI
+ note.getContentValues() // the map of values to insert as a new record
+ );
+
+ // Parses the returned URI to get the note ID of the new note. The ID is used in subtest 2.
+ long noteId = ContentUris.parseId(rowUri);
+
+ // Does a full query on the table. Since insertData() hasn't yet been called, the
+ // table should only contain the record just inserted.
+ Cursor cursor = mMockResolver.query(
+ NotePad.Notes.CONTENT_URI, // the main table URI
+ null, // no projection, return all the columns
+ null, // no selection criteria, return all the rows in the model
+ null, // no selection arguments
+ null // default sort order
+ );
+
+ // Asserts that there should be only 1 record.
+ assertEquals(1, cursor.getCount());
+
+ // Moves to the first (and only) record in the cursor and asserts that this worked.
+ assertTrue(cursor.moveToFirst());
+
+ // Since no projection was used, get the column indexes of the returned columns
+ int titleIndex = cursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_TITLE);
+ int noteIndex = cursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_NOTE);
+ int crdateIndex = cursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_CREATE_DATE);
+ int moddateIndex = cursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE);
+
+ // Tests each column in the returned cursor against the data that was inserted, comparing
+ // the field in the NoteInfo object to the data at the column index in the cursor.
+ assertEquals(note.title, cursor.getString(titleIndex));
+ assertEquals(note.note, cursor.getString(noteIndex));
+ assertEquals(note.createDate, cursor.getLong(crdateIndex));
+ assertEquals(note.modDate, cursor.getLong(moddateIndex));
+
+ // Insert subtest 2.
+ // Tests that we can't insert a record whose id value already exists.
+
+ // Defines a ContentValues object so that the test can add a note ID to it.
+ ContentValues values = note.getContentValues();
+
+ // Adds the note ID retrieved in subtest 1 to the ContentValues object.
+ values.put(NotePad.Notes._ID, (int) noteId);
+
+ // Tries to insert this record into the table. This should fail and drop into the
+ // catch block. If it succeeds, issue a failure message.
+ try {
+ rowUri = mMockResolver.insert(NotePad.Notes.CONTENT_URI, values);
+ fail("Expected insert failure for existing record but insert succeeded.");
+ } catch (Exception e) {
+ // succeeded, so do nothing.
+ }
+ }
+
+ /*
+ * Tests deletions from the data model.
+ */
+ public void testDeletes() {
+ // Subtest 1.
+ // Tries to delete a record from a data model that is empty.
+
+ // Sets the selection column to "title"
+ final String SELECTION_COLUMNS = NotePad.Notes.COLUMN_NAME_TITLE + " = " + "?";
+
+ // Sets the selection argument "Note0"
+ final String[] SELECTION_ARGS = { "Note0" };
+
+ // Tries to delete rows matching the selection criteria from the data model.
+ int rowsDeleted = mMockResolver.delete(
+ NotePad.Notes.CONTENT_URI, // the base URI of the table
+ SELECTION_COLUMNS, // select based on the title column
+ SELECTION_ARGS // select title = "Note0"
+ );
+
+ // Assert that the deletion did not work. The number of deleted rows should be zero.
+ assertEquals(0, rowsDeleted);
+
+ // Subtest 2.
+ // Tries to delete an existing record. Repeats the previous subtest, but inserts data first.
+
+ // Inserts data into the model.
+ insertData();
+
+ // Uses the same parameters to try to delete the row with title "Note0"
+ rowsDeleted = mMockResolver.delete(
+ NotePad.Notes.CONTENT_URI, // the base URI of the table
+ SELECTION_COLUMNS, // same selection column, "title"
+ SELECTION_ARGS // same selection arguments, title = "Note0"
+ );
+
+ // The number of deleted rows should be 1.
+ assertEquals(1, rowsDeleted);
+
+ // Tests that the record no longer exists. Tries to get it from the table, and
+ // asserts that nothing was returned.
+
+ // Queries the table with the same selection column and argument used to delete the row.
+ Cursor cursor = mMockResolver.query(
+ NotePad.Notes.CONTENT_URI, // the base URI of the table
+ null, // no projection, return all columns
+ SELECTION_COLUMNS, // select based on the title column
+ SELECTION_ARGS, // select title = "Note0"
+ null // use the default sort order
+ );
+
+ // Asserts that the cursor is empty since the record had already been deleted.
+ assertEquals(0, cursor.getCount());
+ }
+
+ /*
+ * Tests updates to the data model.
+ */
+ public void testUpdates() {
+ // Selection column for identifying a record in the data model.
+ final String SELECTION_COLUMNS = NotePad.Notes.COLUMN_NAME_TITLE + " = " + "?";
+
+ // Selection argument for the selection column.
+ final String[] selectionArgs = { "Note1" };
+
+ // Defines a map of column names and values
+ ContentValues values = new ContentValues();
+
+ // Subtest 1.
+ // Tries to update a record in an empty table.
+
+ // Sets up the update by putting the "note" column and a value into the values map.
+ values.put(NotePad.Notes.COLUMN_NAME_NOTE, "Testing an update with this string");
+
+ // Tries to update the table
+ int rowsUpdated = mMockResolver.update(
+ NotePad.Notes.CONTENT_URI, // the URI of the data table
+ values, // a map of the updates to do (column title and value)
+ SELECTION_COLUMNS, // select based on the title column
+ selectionArgs // select "title = Note1"
+ );
+
+ // Asserts that no rows were updated.
+ assertEquals(0, rowsUpdated);
+
+ // Subtest 2.
+ // Builds the table, and then tries the update again using the same arguments.
+
+ // Inserts data into the model.
+ insertData();
+
+ // Does the update again, using the same arguments as in subtest 1.
+ rowsUpdated = mMockResolver.update(
+ NotePad.Notes.CONTENT_URI, // The URI of the data table
+ values, // the same map of updates
+ SELECTION_COLUMNS, // same selection, based on the title column
+ selectionArgs // same selection argument, to select "title = Note1"
+ );
+
+ // Asserts that only one row was updated. The selection criteria evaluated to
+ // "title = Note1", and the test data should only contain one row that matches that.
+ assertEquals(1, rowsUpdated);
+ }
+
+ // A utility for converting note data to a ContentValues map.
+ private static class NoteInfo {
+ String title;
+ String note;
+ long createDate;
+ long modDate;
+
+ /*
+ * Constructor for a NoteInfo instance. This class helps create a note and
+ * return its values in a ContentValues map expected by data model methods.
+ * The note's id is created automatically when it is inserted into the data model.
+ */
+ public NoteInfo(String t, String n) {
+ title = t;
+ note = n;
+ createDate = 0;
+ modDate = 0;
+ }
+
+ // Sets the creation date for a test note
+ public void setCreationDate(long c) {
+ createDate = c;
+ }
+
+ // Sets the modification date for a test note
+ public void setModificationDate(long m) {
+ modDate = m;
+ }
+
+ /*
+ * Returns a ContentValues instance (a map) for this NoteInfo instance. This is useful for
+ * inserting a NoteInfo into a database.
+ */
+ public ContentValues getContentValues() {
+ // Gets a new ContentValues object
+ ContentValues v = new ContentValues();
+
+ // Adds map entries for the user-controlled fields in the map
+ v.put(NotePad.Notes.COLUMN_NAME_TITLE, title);
+ v.put(NotePad.Notes.COLUMN_NAME_NOTE, note);
+ v.put(NotePad.Notes.COLUMN_NAME_CREATE_DATE, createDate);
+ v.put(NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE, modDate);
+ return v;
+
+ }
+ }
+}