| /* |
| * 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.example.android.notepad; |
| |
| import com.example.android.notepad.NotePad; |
| |
| import android.app.Activity; |
| import android.content.ClipboardManager; |
| import android.content.ClipData; |
| import android.content.ComponentName; |
| import android.content.ContentResolver; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.database.Cursor; |
| import android.graphics.Canvas; |
| import android.graphics.Paint; |
| import android.graphics.Rect; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| import android.view.Menu; |
| import android.view.MenuItem; |
| import android.widget.EditText; |
| |
| /** |
| * 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"; |
| |
| /* |
| * Creates a projection that returns the note ID and the note contents. |
| */ |
| 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 |
| private static final int COLUMN_INDEX_NOTE = 1; |
| |
| // The index of the title column |
| private static final int COLUMN_INDEX_TITLE = 2; |
| |
| // A label for the saved state of the activity |
| private static final String ORIGINAL_CONTENT = "origContent"; |
| |
| // 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; |
| |
| // 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; |
| private Cursor mCursor; |
| private EditText mText; |
| private String mOriginalContent; |
| |
| /** |
| * 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; |
| |
| // 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(); |
| |
| /* |
| * 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)) { |
| |
| // 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)) { |
| |
| // 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 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; |
| } |
| |
| // 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())); |
| |
| // For a paste, initializes the data from clipboard. |
| if (Intent.ACTION_PASTE.equals(action)) { |
| |
| // Does the paste |
| performPaste(); |
| // Switches the state to PASTE. The title can not be modified. |
| mState = STATE_PASTE; |
| } |
| |
| // If the action was other than EDIT or INSERT: |
| } else { |
| |
| // 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; |
| } |
| |
| // Sets the layout for this Activity. See res/layout/note_editor.xml |
| setContentView(R.layout.note_editor); |
| |
| // Gets a handle to the EditText in the the layout. |
| mText = (EditText) findViewById(R.id.note); |
| |
| /* |
| * 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(); |
| |
| /* |
| * 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) { |
| |
| /* 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) { |
| |
| // 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)); |
| } |
| |
| /* |
| * 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); |
| |
| // 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 |
| // 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(); |
| |
| /* |
| * 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 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(); |
| |
| /* |
| * 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); |
| |
| // 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); |
| } |
| |
| // 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); |
| } |
| |
| /* |
| * 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); |
| |
| /* |
| * 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) { |
| |
| // 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; |
| } |
| |
| // 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) |
| /** |
| * 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 |
| ClipData clip = clipboard.getPrimaryClip(); |
| if (clip != null) { |
| |
| String text=null; |
| String title=null; |
| |
| // Gets the first item from the clipboard data |
| ClipData.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 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 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) |
| |
| /** |
| * 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(); |
| |
| // If updateTitle is true, sets the modification date/time stamp to now. |
| if (updateTitle) { |
| values.put(NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE, System.currentTimeMillis()); |
| |
| // 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) { |
| title = title.substring(0, lastSpace); |
| } |
| } |
| } |
| |
| // In the values map, sets the value of the title |
| values.put(NotePad.Notes.COLUMN_NAME_TITLE, title); |
| } |
| } |
| |
| // 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. |
| ); |
| |
| |
| } |
| |
| /** |
| * 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(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(); |
| } |
| |
| /** |
| * 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(""); |
| } |
| } |
| } |