diff options
| author | Joe Malin <jmalin@google.com> | 2010-08-24 17:44:55 -0700 |
|---|---|---|
| committer | Joe Malin <jmalin@google.com> | 2010-08-24 17:44:55 -0700 |
| commit | 88bcee2e891c5551d9db7c50572c1aa9aa0d6839 (patch) | |
| tree | e5a57c7c05744b4c37df3a048efade776aa1e075 /samples/NotePad | |
| parent | 6e94ff6be96e3fe65054a49d498278631abee5e2 (diff) | |
| download | android_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.mk | 16 | ||||
| -rw-r--r-- | samples/NotePad/AndroidManifest.xml | 2 | ||||
| -rw-r--r-- | samples/NotePad/src/com/example/android/notepad/NoteEditor.java | 452 | ||||
| -rw-r--r-- | samples/NotePad/src/com/example/android/notepad/NotePad.java | 100 | ||||
| -rw-r--r-- | samples/NotePad/src/com/example/android/notepad/NotePadProvider.java | 690 | ||||
| -rw-r--r-- | samples/NotePad/src/com/example/android/notepad/NotesList.java | 422 | ||||
| -rw-r--r-- | samples/NotePad/src/com/example/android/notepad/NotesLiveFolder.java | 86 | ||||
| -rw-r--r-- | samples/NotePad/src/com/example/android/notepad/TitleEditor.java | 128 | ||||
| -rw-r--r-- | samples/NotePad/src/com/google/provider/NotePad.java | 65 | ||||
| -rw-r--r-- | samples/NotePad/tests/Android.mk | 14 | ||||
| -rw-r--r-- | samples/NotePad/tests/build.properties | 1 | ||||
| -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.java | 585 |
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; + + } + } +} |
