diff options
| author | Joe Malin <jmalin@google.com> | 2010-06-07 16:26:25 -0700 |
|---|---|---|
| committer | Joe Malin <jmalin@google.com> | 2010-08-09 15:30:01 -0700 |
| commit | 4124e0a1f07e4e54c37b0cfbb1b7438806ff02a6 (patch) | |
| tree | 044d57a235802491100ea89ace9c8f56c9471e30 /samples/NotePad/src/com/example/android/notepad/NoteEditor.java | |
| parent | 4779ab6f9aa4d6b691f051e069ffac31475f850a (diff) | |
Revised Note Pad sample, new test app for Note Pad
Change-Id: Ia41a33d935ead704c1de439a0cfb0a55806cfe12
Diffstat (limited to 'samples/NotePad/src/com/example/android/notepad/NoteEditor.java')
| -rw-r--r-- | samples/NotePad/src/com/example/android/notepad/NoteEditor.java | 555 |
1 files changed, 376 insertions, 179 deletions
diff --git a/samples/NotePad/src/com/example/android/notepad/NoteEditor.java b/samples/NotePad/src/com/example/android/notepad/NoteEditor.java index 57b464678..d01974e7a 100644 --- a/samples/NotePad/src/com/example/android/notepad/NoteEditor.java +++ b/samples/NotePad/src/com/example/android/notepad/NoteEditor.java @@ -16,13 +16,10 @@ 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; -import android.content.ClippedData; import android.content.ComponentName; -import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; @@ -39,28 +36,31 @@ 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}, or create a note {@link Intent#ACTION_INSERT}. + * + * 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_NOTE }; + /** The index of the note column */ private static final int COLUMN_INDEX_NOTE = 1; - /** The index of the title column */ - private static final int COLUMN_INDEX_TITLE = 2; - + // This is our state data that is stored when freezing. private static final String ORIGINAL_CONTENT = "origContent"; @@ -72,73 +72,111 @@ public class NoteEditor extends Activity { // The different distinct states the activity can be run in. private static final int STATE_EDIT = 0; private static final int STATE_INSERT = 1; - private static final int STATE_PASTE = 2; + // Global variables private int mState; - private boolean mNoteOnly = false; private Uri mUri; private Cursor mCursor; private EditText mText; 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(); - } 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. + + // For an insert action: + } else if (Intent.ACTION_INSERT.equals(action)) { + + // 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; } @@ -147,266 +185,425 @@ public class NoteEditor extends Activity { // set the result to be returned. setResult(RESULT_OK, (new Intent()).setAction(mUri.toString())); - // If pasting, initialize data from clipboard. - if (Intent.ACTION_PASTE.equals(action)) { - performPaste(); - // Switch to paste mode; can no longer modify title. - 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" setTitle(getText(R.string.title_edit)); - } else if (mState == STATE_INSERT || mState == STATE_PASTE) { + } else if (mState == STATE_INSERT) { + + // Sets the title to "create" 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 + // Saves away the original text, so we still have it if the activity // needs to be killed while paused. 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) { - 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 (isFinishing() && (length == 0) && !mNoteOnly) { + // Get the current note text. + String text = mText.getText().toString(); + int note_length = text.length(); + + /* + * 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() && (note_length == 0)) { 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 { - updateNote(text, null, !mNoteOnly); + // Creates a map to contain the new values for the columns + ContentValues values = new ContentValues(); + + // In the values map, sets the modification date column to the current time. + values.put(NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE, System.currentTimeMillis()); + + // Creates a title for a newly-inserted note. + if (mState == STATE_INSERT) { + + // Gets the first 30 characters of the note, or the entire note if it's + // less than 30 characters long. + String title = text.substring(0, Math.min(30, note_length)); + + // If the note's entire length is greater than 30, then the title is 30 + // characters long. Finds the last occurrence of blank in the title, and + // removes all characters to the right of it from the title string. + if (note_length > 30) { + int lastSpace = title.lastIndexOf(' '); + if (lastSpace > 0) { + title = title.substring(0, lastSpace); + } + } + // In the values map, set the title column to the new title. + values.put(NotePad.Notes.COLUMN_NAME_TITLE, title); + } + + // In the values map, sets the note text column to the text in the View. + 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. + ); } } } + /** + * 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) { - menu.add(0, DELETE_ID, 0, R.string.menu_delete) - .setShortcut('1', 'd') - .setIcon(android.R.drawable.ic_menu_delete); - } + .setShortcut('0', 'r') + .setIcon(android.R.drawable.ic_menu_revert); + + // 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 { + + // Adds the 'discard' menu item, using the 'delete' shortcuts and icon. 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. - if (!mNoteOnly) { - Intent intent = new Intent(null, getIntent().getData()); - intent.addCategory(Intent.CATEGORY_ALTERNATIVE); - menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0, - new ComponentName(this, NoteEditor.class), null, intent, 0, null); - } - + /* + * Appends menu items for any Activity declarations that implement an alternative action + * for this Activity's MIME type, one menu item for each Activity. + */ + // 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); + + /* + * 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; + + // Discards the new note. case DISCARD_ID: cancelNote(); break; + + // Discards any changes to an edited note. case REVERT_ID: cancelNote(); break; } - return super.onOptionsItemSelected(item); - } - -//BEGIN_INCLUDE(paste) - /** - * Replace the note's data with the current contents of the clipboard. - */ - private final void performPaste() { - ClipboardManager clipboard = (ClipboardManager) - getSystemService(Context.CLIPBOARD_SERVICE); - ContentResolver cr = getContentResolver(); - - ClippedData clip = clipboard.getPrimaryClip(); - if (clip != null) { - String text=null, title=null; - - ClippedData.Item item = clip.getItem(0); - Uri uri = item.getUri(); - 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); - if (orig != null) { - if (orig.moveToFirst()) { - text = orig.getString(COLUMN_INDEX_NOTE); - title = orig.getString(COLUMN_INDEX_TITLE); - } - orig.close(); - } - } - - // If we weren't able to load the clipped data as a note, then - // convert whatever it is to text. - if (text == null) { - text = item.coerceToText(this).toString(); - } - - updateNote(text, title, true); - } - } -//END_INCLUDE(paste) - - /** - * Replace the current note contents with the given data. - */ - private final void updateNote(String text, String title, boolean updateTitle) { - ContentValues values = new ContentValues(); - - // This stuff is only done when working with a full-fledged note. - if (updateTitle) { - // Bump the modification time to now. - values.put(Notes.MODIFIED_DATE, System.currentTimeMillis()); - - // If we are creating a new note, then we want to also create - // an initial title for it. - if (mState == STATE_INSERT) { - if (title == null) { - int length = text.length(); - title = text.substring(0, Math.min(30, length)); - if (length > 30) { - int lastSpace = title.lastIndexOf(' '); - if (lastSpace > 0) { - title = title.substring(0, lastSpace); - } - } - } - values.put(Notes.TITLE, title); - } - } - - // Write our text back into the provider. - values.put(Notes.NOTE, text); - - // 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); + // 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); } /** - * Take care of canceling work on a note. Deletes the note if we + * Takes care of canceling work on a note. Deletes the note if we * had created it, otherwise reverts to the original text. */ private final void cancelNote() { + + /* + * Tests to see that the original 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) { + + /* + * If the user is editing a note, and asked to discard or revert, this puts the + * previous note contents back into the note. + */ if (mState == STATE_EDIT) { - // Put the original note text back into the database + + // Closes the previous cursor prior to updating the provider mCursor.close(); mCursor = null; + + // Creates a new values map ContentValues values = new ContentValues(); - values.put(Notes.NOTE, mOriginalContent); - getContentResolver().update(mUri, values, null, null); + + // Puts the original notes content into the values map. The variable was set in + // onResume(). + values.put(NotePad.Notes.COLUMN_NAME_NOTE, mOriginalContent); + + /* + * Update the provider with the reverted note content. + * + * 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 of the note or notes. + values, // The reverted values to put into the provider. + null, // No selection criteria, so no where columns are needed. + null // No where columns are used, so no where values are needed. + ); + + /* + * If the user was inserting a note and decides to discard it, this deletes the note. + */ } else if (mState == STATE_INSERT) { - // We inserted an empty note, make sure to delete it + // Deletes the note. deleteNote(); } } + + // Returns a result of CANCELED to the calling Activity. setResult(RESULT_CANCELED); + + // Finishes the Activity. Once the user deletes or discards, nothing more can be done, so + // return to the calling Activity, either NotesList or some other Activity. finish(); } /** - * Take care of deleting a note. Simply deletes the entry. + * This method deletes a note from the provider. */ private final void deleteNote() { + /* + * Tests to see that the original 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) { + + // Gets rid of all the Cursor's resources, and deactivates it. mCursor.close(); mCursor = null; - getContentResolver().delete(mUri, null, null); + + /* + * Deletes the note based on the ID in the URI. + * + * Note: This is being done on the UI thread. It will block the thread until the + * delete 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 android.os.AsyncTask. + */ + + getContentResolver().delete( + mUri, // The URI of the note to delete. + null, // No selection criteria are specified, so no where columns are needed. + null // No where columns are specified, so no where values are needed. + ); + + // Throws away any text currently showing in the View. mText.setText(""); } } |
