diff --git a/samples/NotePad/AndroidManifest.xml b/samples/NotePad/AndroidManifest.xml index ead782925..51e848d8e 100644 --- a/samples/NotePad/AndroidManifest.xml +++ b/samples/NotePad/AndroidManifest.xml @@ -107,14 +107,6 @@ - - - - - - - diff --git a/samples/NotePad/res/values/strings.xml b/samples/NotePad/res/values/strings.xml index 26d23d072..508fa439f 100644 --- a/samples/NotePad/res/values/strings.xml +++ b/samples/NotePad/res/values/strings.xml @@ -40,4 +40,5 @@ Error Error loading note There is nothing to save + Blank title not saved \ No newline at end of file diff --git a/samples/NotePad/src/com/example/android/notepad/NoteEditor.java b/samples/NotePad/src/com/example/android/notepad/NoteEditor.java index 59d6f1290..b8b070f20 100644 --- a/samples/NotePad/src/com/example/android/notepad/NoteEditor.java +++ b/samples/NotePad/src/com/example/android/notepad/NoteEditor.java @@ -17,13 +17,16 @@ package com.example.android.notepad; import android.app.Activity; +import android.app.LoaderManager; import android.content.ClipData; import android.content.ClipboardManager; import android.content.ComponentName; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; +import android.content.CursorLoader; import android.content.Intent; +import android.content.Loader; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Canvas; @@ -37,19 +40,15 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.widget.EditText; +import com.example.android.notepad.NotePad.Notes; /** * 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 { +public class NoteEditor extends Activity implements LoaderManager.LoaderCallbacks { // For logging and debugging purposes private static final String TAG = "NoteEditor"; @@ -71,10 +70,11 @@ public class NoteEditor extends Activity { private static final int STATE_EDIT = 0; private static final int STATE_INSERT = 1; + private static final int LOADER_ID = 1; + // Global mutable variables private int mState; private Uri mUri; - private Cursor mCursor; private EditText mText; private String mOriginalContent; @@ -139,6 +139,11 @@ public class NoteEditor extends Activity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // Recovering the instance state from a previously destroyed Activity instance + if (savedInstanceState != null) { + mOriginalContent = savedInstanceState.getString(ORIGINAL_CONTENT); + } + /* * Creates an Intent to use when the Activity object's result is sent back to the * caller. @@ -166,6 +171,8 @@ public class NoteEditor extends Activity { // Sets the Activity state to INSERT, gets the general note URI, and inserts an // empty record in the provider mState = STATE_INSERT; + setTitle(getText(R.string.title_create)); + mUri = getContentResolver().insert(intent.getData(), null); /* @@ -197,24 +204,10 @@ public class NoteEditor extends Activity { return; } - /* - * 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) - ); + // Initialize the LoaderManager and start the query + getLoaderManager().initLoader(LOADER_ID, null, this); // For a paste, initializes the data from clipboard. - // (Must be done after mCursor is initialized.) if (Intent.ACTION_PASTE.equals(action)) { // Does the paste performPaste(); @@ -227,87 +220,12 @@ public class NoteEditor extends Activity { // Gets a handle to the EditText in the the layout. mText = (EditText) findViewById(R.id.note); - - /* - * 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(); - - /* - * 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) { - // Requery in case something changed while paused (such as the title) - mCursor.requery(); - - /* 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(); - - // Modifies the window title for the Activity according to the current Activity state. - if (mState == STATE_EDIT) { - // Set the title of the Activity to include the note title - int colTitleIndex = mCursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_TITLE); - String title = mCursor.getString(colTitleIndex); - Resources res = getResources(); - String text = String.format(res.getString(R.string.title_edit), title); - setTitle(text); - // Sets the title to "create" for inserts - } else if (mState == STATE_INSERT) { - setTitle(getText(R.string.title_create)); - } - - /* - * 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. - int colNoteIndex = mCursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_NOTE); - String note = mCursor.getString(colNoteIndex); - mText.setTextKeepState(note); - - // 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 + * This method is called when an Activity loses focus during its normal operation. + * 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 @@ -316,37 +234,52 @@ public class NoteEditor extends Activity { @Override protected void onSaveInstanceState(Bundle outState) { // Save away the original text, so we still have it if the activity - // needs to be killed while paused. + // needs to be re-created. outState.putString(ORIGINAL_CONTENT, mOriginalContent); + // Call the superclass to save the any view hierarchy state + super.onSaveInstanceState(outState); } /** * 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. + * While there is no need to override this method in this app, it is shown here to highlight + * that we are not saving any state in onPause, but have moved app state saving to onStop + * callback. + * In earlier versions of this app and popular literature it had been shown that onPause is good + * place to persist any unsaved work, however, this is not really a good practice because of how + * application and process lifecycle behave. + * As a general guideline apps should have a way of saving their business logic that does not + * solely rely on Activity (or other component) lifecyle state transitions. + * As a backstop you should save any app state, not saved during lifetime of the Activity, in + * onStop(). + * For a more detailed explanation of this recommendation please read + * + * Processes and Application Life Cycle . + * + * Pausing and Resuming an Activity . + */ + @Override + protected void onPause() { + super.onPause(); + } + + /** + * This method is called when the Activity becomes invisible. + * + * For Activity objects that edit information, onStop() may be the one place where changes maybe + * saved. * * 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(); + protected void onStop() { + super.onStop(); - /* - * 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(); + // Get the current note text. + String text = mText.getText().toString(); + int length = text.length(); /* * If the Activity is in the midst of finishing and there is no text in the current @@ -354,23 +287,22 @@ public class NoteEditor extends Activity { * even if the note was being edited, the assumption being that the user wanted to * "clear out" (delete) the note. */ - if (isFinishing() && (length == 0)) { - setResult(RESULT_CANCELED); - deleteNote(); + if (isFinishing() && (length == 0)) { + setResult(RESULT_CANCELED); + deleteNote(); /* - * 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. + * 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 if (mState == STATE_EDIT) { - // Creates a map to contain the new values for the columns - updateNote(text, null); - } else if (mState == STATE_INSERT) { - updateNote(text, text); - mState = STATE_EDIT; - } + } else if (mState == STATE_EDIT) { + // Creates a map to contain the new values for the columns + updateNote(text, null); + } else if (mState == STATE_INSERT) { + updateNote(text, text); + mState = STATE_EDIT; } } @@ -409,8 +341,16 @@ public class NoteEditor extends Activity { @Override public boolean onPrepareOptionsMenu(Menu menu) { // Check if note has changed and enable/disable the revert option - int colNoteIndex = mCursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_NOTE); - String savedNote = mCursor.getString(colNoteIndex); + Cursor cursor = getContentResolver().query( + 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. + ); + cursor.moveToFirst(); + int colNoteIndex = cursor.getColumnIndex(Notes.COLUMN_NAME_NOTE); + String savedNote = cursor.getString(colNoteIndex); String currentNote = mText.getText().toString(); if (savedNote.equals(currentNote)) { menu.findItem(R.id.menu_revert).setVisible(false); @@ -493,8 +433,8 @@ public class NoteEditor extends Activity { // (moveToFirst() returns true), then this gets the note data from it. if (orig != null) { if (orig.moveToFirst()) { - int colNoteIndex = mCursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_NOTE); - int colTitleIndex = mCursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_TITLE); + int colNoteIndex = orig.getColumnIndex(NotePad.Notes.COLUMN_NAME_NOTE); + int colTitleIndex = orig.getColumnIndex(NotePad.Notes.COLUMN_NAME_TITLE); text = orig.getString(colNoteIndex); title = orig.getString(colTitleIndex); } @@ -571,13 +511,11 @@ public class NoteEditor extends Activity { * 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. - ); - - + 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. + ); } /** @@ -585,19 +523,17 @@ public class NoteEditor extends Activity { * newly created, or reverts to the original text of the note i */ private final void cancelNote() { - 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(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 - deleteNote(); - } + + if (mState == STATE_EDIT) { + // Put the original note text back into the database + ContentValues values = new ContentValues(); + 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 + deleteNote(); } + setResult(RESULT_CANCELED); finish(); } @@ -606,11 +542,50 @@ public class NoteEditor extends Activity { * Take care of deleting a note. Simply deletes the entry. */ private final void deleteNote() { - if (mCursor != null) { - mCursor.close(); - mCursor = null; - getContentResolver().delete(mUri, null, null); - mText.setText(""); + getContentResolver().delete(mUri, null, null); + mText.setText(""); + } + + // LoaderManager callbacks + @Override + public Loader onCreateLoader(int i, Bundle bundle) { + return new CursorLoader( + this, + 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. + ); + } + + @Override + public void onLoadFinished(Loader cursorLoader, Cursor cursor) { + + // Modifies the window title for the Activity according to the current Activity state. + if (cursor != null && cursor.moveToFirst() && mState == STATE_EDIT) { + // Set the title of the Activity to include the note title + int colTitleIndex = cursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_TITLE); + int colNoteIndex = cursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_NOTE); + + // Gets the title and sets it + String title = cursor.getString(colTitleIndex); + Resources res = getResources(); + String text = String.format(res.getString(R.string.title_edit), title); + setTitle(text); + + // Gets the note text from the Cursor and puts it in the TextView, but doesn't change + // the text cursor's position. + + String note = cursor.getString(colNoteIndex); + mText.setTextKeepState(note); + // Stores the original note text, to allow the user to revert changes. + if (mOriginalContent == null) { + mOriginalContent = note; + } } } + + @Override + public void onLoaderReset(Loader cursorLoader) {} } diff --git a/samples/NotePad/src/com/example/android/notepad/NotePadProvider.java b/samples/NotePad/src/com/example/android/notepad/NotePadProvider.java index 183964563..f81e22d1f 100644 --- a/samples/NotePad/src/com/example/android/notepad/NotePadProvider.java +++ b/samples/NotePad/src/com/example/android/notepad/NotePadProvider.java @@ -70,11 +70,6 @@ public class NotePadProvider extends ContentProvider implements PipeDataWriter sNotesProjectionMap; - /** - * A projection map used to select columns from the database - */ - private static HashMap sLiveFolderProjectionMap; - /** * Standard projection for the interesting columns of a normal note. */ @@ -96,9 +91,6 @@ public class NotePadProvider extends ContentProvider implements PipeDataWriter(); - - // Maps "_ID" to "_ID AS _ID" for a live folder - sLiveFolderProjectionMap.put(LiveFolders._ID, NotePad.Notes._ID + " AS " + LiveFolders._ID); - - // Maps "NAME" to "title AS NAME" - sLiveFolderProjectionMap.put(LiveFolders.NAME, NotePad.Notes.COLUMN_NAME_TITLE + " AS " + - LiveFolders.NAME); } /** @@ -278,11 +252,6 @@ public class NotePadProvider extends ContentProvider implements PipeDataWriter { // For logging and debugging private static final String TAG = "NotesList"; + private static final int LOADER_ID = 0; + /** * The columns needed by the cursor adapter */ @@ -65,6 +64,8 @@ public class NotesList extends ListActivity { /** The index of the title column */ private static final int COLUMN_INDEX_TITLE = 1; + private SimpleCursorAdapter mAdapter; + /** * onCreate is called when Android starts this Activity from scratch. */ @@ -95,18 +96,6 @@ public class NotesList extends ListActivity { */ getListView().setOnCreateContextMenuListener(this); - /* 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 @@ -124,17 +113,19 @@ public class NotesList extends ListActivity { 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 - ); + mAdapter = new SimpleCursorAdapter( + this, // The Context for the ListView + R.layout.noteslist_item, // Points to the XML for a list item + null, // The cursor is set by CursorLoader when loaded + dataColumns, + viewIDs, + CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER + ); // Sets the ListView's adapter to be the cursor adapter that was just created. - setListAdapter(adapter); + setListAdapter(mAdapter); + // Initialize the LoaderManager and start the query + getLoaderManager().initLoader(LOADER_ID, null, this); } /** @@ -464,4 +455,28 @@ public class NotesList extends ListActivity { startActivity(new Intent(Intent.ACTION_EDIT, uri)); } } + + // LoaderManager callbacks + @Override + public Loader onCreateLoader(int i, Bundle bundle) { + return new CursorLoader( + this, + 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. + ); + } + + @Override + public void onLoadFinished(Loader cursorLoader, Cursor cursor) { + mAdapter.changeCursor(cursor); + } + + @Override + public void onLoaderReset(Loader cursorLoader) { + // Since the Loader is reset, this removes the cursor reference from the adapter. + mAdapter.changeCursor(null); + } } diff --git a/samples/NotePad/src/com/example/android/notepad/NotesLiveFolder.java b/samples/NotePad/src/com/example/android/notepad/NotesLiveFolder.java deleted file mode 100644 index 24afaa0d4..000000000 --- a/samples/NotePad/src/com/example/android/notepad/NotesLiveFolder.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (C) 2009 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 com.example.android.notepad.NotePad; - -import android.app.Activity; -import android.content.Intent; -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 { - - /** - * 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). - */ - @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)) { - - // Creates a new Intent. - final Intent liveFolderIntent = new Intent(); - - /* - * 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); - - /* - * 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 5abe97b39..e6f029b98 100644 --- a/samples/NotePad/src/com/example/android/notepad/TitleEditor.java +++ b/samples/NotePad/src/com/example/android/notepad/TitleEditor.java @@ -21,8 +21,10 @@ import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; +import android.text.TextUtils; import android.view.View; import android.widget.EditText; +import android.widget.Toast; /** * This Activity allows the user to edit a note's title. It displays a floating window @@ -49,15 +51,15 @@ public class TitleEditor extends Activity { // The position of the title column in a Cursor returned by the provider. private static final int COLUMN_INDEX_TITLE = 1; - // A Cursor object that will contain the results of querying the provider for a note. - private Cursor mCursor; - // An EditText object for preserving the edited title. private EditText mText; // A URI object for the note whose title is being edited. private Uri mUri; + // The title that was last saved. + private String mSavedTitle; + /** * 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. @@ -69,6 +71,9 @@ public class TitleEditor extends Activity { // Set the View for this Activity object's UI. setContentView(R.layout.title_editor); + // Gets the View ID for the EditText box + mText = (EditText) this.findViewById(R.id.title); + // 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(); @@ -82,7 +87,7 @@ public class TitleEditor extends Activity { * android.content.AsyncQueryHandler or android.os.AsyncTask. */ - mCursor = managedQuery( + Cursor cursor = getContentResolver().query( 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. @@ -90,8 +95,15 @@ public class TitleEditor extends Activity { null // No sort order is needed. ); - // Gets the View ID for the EditText box - mText = (EditText) this.findViewById(R.id.title); + if (cursor != 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. + cursor.moveToFirst(); + + // Displays the current title text in the EditText object. + mText.setText(cursor.getString(COLUMN_INDEX_TITLE)); + } } /** @@ -103,65 +115,83 @@ public class TitleEditor extends Activity { @Override protected void onResume() { super.onResume(); - - // 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. + * While there is no need to override this method in this app, it is shown here to highlight + * that we are not saving any state in onPause, but have moved app state saving to onStop + * callback. + * In earlier versions of this app and popular literature it had been shown that onPause is good + * place to persist any unsaved work, however, this is not really a good practice because of how + * application and process lifecycle behave. + * As a general guideline apps should have a way of saving their business logic that does not + * solely rely on Activity (or other component) lifecyle state transitions. + * As a backstop you should save any app state, not saved during lifetime of the Activity, in + * onStop(). + * For a more detailed explanation of this recommendation please read + * + * Processes and Application Life Cycle . + * + * Pausing and Resuming an Activity . */ @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) { - - // Creates a values map for updating the provider. - ContentValues values = new ContentValues(); - - // 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 Activity becomes invisible. + * + * For Activity objects that edit information, onStop() may be the one place where changes are + * saved. + * Updates the note with the text currently in the text box. + */ + @Override + protected void onStop() { + super.onStop(); + saveTitle(); } public void onClickOk(View v) { + saveTitle(); finish(); } + + // Saves the title if required + private void saveTitle() { + + if (!TextUtils.isEmpty(mText.getText())) { + + String newTitle = mText.getText().toString(); + + if (!newTitle.equals(mSavedTitle)) { + // Creates a values map for updating the provider. + ContentValues values = new ContentValues(); + + // In the values map, sets the title to the current contents of the edit box. + values.put(NotePad.Notes.COLUMN_NAME_TITLE, newTitle); + + /* + * 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. + ); + mSavedTitle = newTitle; + } + } else { + Toast.makeText(this, R.string.title_blank, Toast.LENGTH_SHORT).show(); + } + } }