1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.example.android.notepad; 18 19 import android.app.Activity; 20 import android.content.ClipData; 21 import android.content.ClipboardManager; 22 import android.content.ComponentName; 23 import android.content.ContentResolver; 24 import android.content.ContentValues; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.res.Resources; 28 import android.database.Cursor; 29 import android.graphics.Canvas; 30 import android.graphics.Paint; 31 import android.graphics.Rect; 32 import android.net.Uri; 33 import android.os.Bundle; 34 import android.util.AttributeSet; 35 import android.util.Log; 36 import android.view.Menu; 37 import android.view.MenuInflater; 38 import android.view.MenuItem; 39 import android.widget.EditText; 40 41 /** 42 * This Activity handles "editing" a note, where editing is responding to 43 * {@link Intent#ACTION_VIEW} (request to view data), edit a note 44 * {@link Intent#ACTION_EDIT}, create a note {@link Intent#ACTION_INSERT}, or 45 * create a new note from the current contents of the clipboard {@link Intent#ACTION_PASTE}. 46 * 47 * NOTE: Notice that the provider operations in this Activity are taking place on the UI thread. 48 * This is not a good practice. It is only done here to make the code more readable. A real 49 * application should use the {@link android.content.AsyncQueryHandler} 50 * or {@link android.os.AsyncTask} object to perform operations asynchronously on a separate thread. 51 */ 52 public class NoteEditor extends Activity { 53 // For logging and debugging purposes 54 private static final String TAG = "NoteEditor"; 55 56 /* 57 * Creates a projection that returns the note ID and the note contents. 58 */ 59 private static final String[] PROJECTION = 60 new String[] { 61 NotePad.Notes._ID, 62 NotePad.Notes.COLUMN_NAME_TITLE, 63 NotePad.Notes.COLUMN_NAME_NOTE 64 }; 65 66 // A label for the saved state of the activity 67 private static final String ORIGINAL_CONTENT = "origContent"; 68 69 // This Activity can be started by more than one action. Each action is represented 70 // as a "state" constant 71 private static final int STATE_EDIT = 0; 72 private static final int STATE_INSERT = 1; 73 74 // Global mutable variables 75 private int mState; 76 private Uri mUri; 77 private Cursor mCursor; 78 private EditText mText; 79 private String mOriginalContent; 80 81 /** 82 * Defines a custom EditText View that draws lines between each line of text that is displayed. 83 */ 84 public static class LinedEditText extends EditText { 85 private Rect mRect; 86 private Paint mPaint; 87 88 // This constructor is used by LayoutInflater 89 public LinedEditText(Context context, AttributeSet attrs) { 90 super(context, attrs); 91 92 // Creates a Rect and a Paint object, and sets the style and color of the Paint object. 93 mRect = new Rect(); 94 mPaint = new Paint(); 95 mPaint.setStyle(Paint.Style.STROKE); 96 mPaint.setColor(0x800000FF); 97 } 98 99 /** 100 * This is called to draw the LinedEditText object 101 * @param canvas The canvas on which the background is drawn. 102 */ 103 @Override 104 protected void onDraw(Canvas canvas) { 105 106 // Gets the number of lines of text in the View. 107 int count = getLineCount(); 108 109 // Gets the global Rect and Paint objects 110 Rect r = mRect; 111 Paint paint = mPaint; 112 113 /* 114 * Draws one line in the rectangle for every line of text in the EditText 115 */ 116 for (int i = 0; i < count; i++) { 117 118 // Gets the baseline coordinates for the current line of text 119 int baseline = getLineBounds(i, r); 120 121 /* 122 * Draws a line in the background from the left of the rectangle to the right, 123 * at a vertical position one dip below the baseline, using the "paint" object 124 * for details. 125 */ 126 canvas.drawLine(r.left, baseline + 1, r.right, baseline + 1, paint); 127 } 128 129 // Finishes up by calling the parent method 130 super.onDraw(canvas); 131 } 132 } 133 134 /** 135 * This method is called by Android when the Activity is first started. From the incoming 136 * Intent, it determines what kind of editing is desired, and then does it. 137 */ 138 @Override 139 protected void onCreate(Bundle savedInstanceState) { 140 super.onCreate(savedInstanceState); 141 142 /* 143 * Creates an Intent to use when the Activity object's result is sent back to the 144 * caller. 145 */ 146 final Intent intent = getIntent(); 147 148 /* 149 * Sets up for the edit, based on the action specified for the incoming Intent. 150 */ 151 152 // Gets the action that triggered the intent filter for this Activity 153 final String action = intent.getAction(); 154 155 // For an edit action: 156 if (Intent.ACTION_EDIT.equals(action)) { 157 158 // Sets the Activity state to EDIT, and gets the URI for the data to be edited. 159 mState = STATE_EDIT; 160 mUri = intent.getData(); 161 162 // For an insert or paste action: 163 } else if (Intent.ACTION_INSERT.equals(action) 164 || Intent.ACTION_PASTE.equals(action)) { 165 166 // Sets the Activity state to INSERT, gets the general note URI, and inserts an 167 // empty record in the provider 168 mState = STATE_INSERT; 169 mUri = getContentResolver().insert(intent.getData(), null); 170 171 /* 172 * If the attempt to insert the new note fails, shuts down this Activity. The 173 * originating Activity receives back RESULT_CANCELED if it requested a result. 174 * Logs that the insert failed. 175 */ 176 if (mUri == null) { 177 178 // Writes the log identifier, a message, and the URI that failed. 179 Log.e(TAG, "Failed to insert new note into " + getIntent().getData()); 180 181 // Closes the activity. 182 finish(); 183 return; 184 } 185 186 // Since the new entry was created, this sets the result to be returned 187 // set the result to be returned. 188 setResult(RESULT_OK, (new Intent()).setAction(mUri.toString())); 189 190 // If the action was other than EDIT or INSERT: 191 } else { 192 193 // Logs an error that the action was not understood, finishes the Activity, and 194 // returns RESULT_CANCELED to an originating Activity. 195 Log.e(TAG, "Unknown action, exiting"); 196 finish(); 197 return; 198 } 199 200 /* 201 * Using the URI passed in with the triggering Intent, gets the note or notes in 202 * the provider. 203 * Note: This is being done on the UI thread. It will block the thread until the query 204 * completes. In a sample app, going against a simple provider based on a local database, 205 * the block will be momentary, but in a real app you should use 206 * android.content.AsyncQueryHandler or android.os.AsyncTask. 207 */ 208 mCursor = managedQuery( 209 mUri, // The URI that gets multiple notes from the provider. 210 PROJECTION, // A projection that returns the note ID and note content for each note. 211 null, // No "where" clause selection criteria. 212 null, // No "where" clause selection values. 213 null // Use the default sort order (modification date, descending) 214 ); 215 216 // For a paste, initializes the data from clipboard. 217 // (Must be done after mCursor is initialized.) 218 if (Intent.ACTION_PASTE.equals(action)) { 219 // Does the paste 220 performPaste(); 221 // Switches the state to EDIT so the title can be modified. 222 mState = STATE_EDIT; 223 } 224 225 // Sets the layout for this Activity. See res/layout/note_editor.xml 226 setContentView(R.layout.note_editor); 227 228 // Gets a handle to the EditText in the the layout. 229 mText = (EditText) findViewById(R.id.note); 230 231 /* 232 * If this Activity had stopped previously, its state was written the ORIGINAL_CONTENT 233 * location in the saved Instance state. This gets the state. 234 */ 235 if (savedInstanceState != null) { 236 mOriginalContent = savedInstanceState.getString(ORIGINAL_CONTENT); 237 } 238 } 239 240 /** 241 * This method is called when the Activity is about to come to the foreground. This happens 242 * when the Activity comes to the top of the task stack, OR when it is first starting. 243 * 244 * Moves to the first note in the list, sets an appropriate title for the action chosen by 245 * the user, puts the note contents into the TextView, and saves the original text as a 246 * backup. 247 */ 248 @Override 249 protected void onResume() { 250 super.onResume(); 251 252 /* 253 * mCursor is initialized, since onCreate() always precedes onResume for any running 254 * process. This tests that it's not null, since it should always contain data. 255 */ 256 if (mCursor != null) { 257 // Requery in case something changed while paused (such as the title) 258 mCursor.requery(); 259 260 /* Moves to the first record. Always call moveToFirst() before accessing data in 261 * a Cursor for the first time. The semantics of using a Cursor are that when it is 262 * created, its internal index is pointing to a "place" immediately before the first 263 * record. 264 */ 265 mCursor.moveToFirst(); 266 267 // Modifies the window title for the Activity according to the current Activity state. 268 if (mState == STATE_EDIT) { 269 // Set the title of the Activity to include the note title 270 int colTitleIndex = mCursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_TITLE); 271 String title = mCursor.getString(colTitleIndex); 272 Resources res = getResources(); 273 String text = String.format(res.getString(R.string.title_edit), title); 274 setTitle(text); 275 // Sets the title to "create" for inserts 276 } else if (mState == STATE_INSERT) { 277 setTitle(getText(R.string.title_create)); 278 } 279 280 /* 281 * onResume() may have been called after the Activity lost focus (was paused). 282 * The user was either editing or creating a note when the Activity paused. 283 * The Activity should re-display the text that had been retrieved previously, but 284 * it should not move the cursor. This helps the user to continue editing or entering. 285 */ 286 287 // Gets the note text from the Cursor and puts it in the TextView, but doesn't change 288 // the text cursor's position. 289 int colNoteIndex = mCursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_NOTE); 290 String note = mCursor.getString(colNoteIndex); 291 mText.setTextKeepState(note); 292 293 // Stores the original note text, to allow the user to revert changes. 294 if (mOriginalContent == null) { 295 mOriginalContent = note; 296 } 297 298 /* 299 * Something is wrong. The Cursor should always contain data. Report an error in the 300 * note. 301 */ 302 } else { 303 setTitle(getText(R.string.error_title)); 304 mText.setText(getText(R.string.error_message)); 305 } 306 } 307 308 /** 309 * This method is called when an Activity loses focus during its normal operation, and is then 310 * later on killed. The Activity has a chance to save its state so that the system can restore 311 * it. 312 * 313 * Notice that this method isn't a normal part of the Activity lifecycle. It won't be called 314 * if the user simply navigates away from the Activity. 315 */ 316 @Override 317 protected void onSaveInstanceState(Bundle outState) { 318 // Save away the original text, so we still have it if the activity 319 // needs to be killed while paused. 320 outState.putString(ORIGINAL_CONTENT, mOriginalContent); 321 } 322 323 /** 324 * This method is called when the Activity loses focus. 325 * 326 * For Activity objects that edit information, onPause() may be the one place where changes are 327 * saved. The Android application model is predicated on the idea that "save" and "exit" aren't 328 * required actions. When users navigate away from an Activity, they shouldn't have to go back 329 * to it to complete their work. The act of going away should save everything and leave the 330 * Activity in a state where Android can destroy it if necessary. 331 * 332 * If the user hasn't done anything, then this deletes or clears out the note, otherwise it 333 * writes the user's work to the provider. 334 */ 335 @Override 336 protected void onPause() { 337 super.onPause(); 338 339 /* 340 * Tests to see that the query operation didn't fail (see onCreate()). The Cursor object 341 * will exist, even if no records were returned, unless the query failed because of some 342 * exception or error. 343 * 344 */ 345 if (mCursor != null) { 346 347 // Get the current note text. 348 String text = mText.getText().toString(); 349 int length = text.length(); 350 351 /* 352 * If the Activity is in the midst of finishing and there is no text in the current 353 * note, returns a result of CANCELED to the caller, and deletes the note. This is done 354 * even if the note was being edited, the assumption being that the user wanted to 355 * "clear out" (delete) the note. 356 */ 357 if (isFinishing() && (length == 0)) { 358 setResult(RESULT_CANCELED); 359 deleteNote(); 360 361 /* 362 * Writes the edits to the provider. The note has been edited if an existing note was 363 * retrieved into the editor *or* if a new note was inserted. In the latter case, 364 * onCreate() inserted a new empty note into the provider, and it is this new note 365 * that is being edited. 366 */ 367 } else if (mState == STATE_EDIT) { 368 // Creates a map to contain the new values for the columns 369 updateNote(text, null); 370 } else if (mState == STATE_INSERT) { 371 updateNote(text, text); 372 mState = STATE_EDIT; 373 } 374 } 375 } 376 377 /** 378 * This method is called when the user clicks the device's Menu button the first time for 379 * this Activity. Android passes in a Menu object that is populated with items. 380 * 381 * Builds the menus for editing and inserting, and adds in alternative actions that 382 * registered themselves to handle the MIME types for this application. 383 * 384 * @param menu A Menu object to which items should be added. 385 * @return True to display the menu. 386 */ 387 @Override 388 public boolean onCreateOptionsMenu(Menu menu) { 389 // Inflate menu from XML resource 390 MenuInflater inflater = getMenuInflater(); 391 inflater.inflate(R.menu.editor_options_menu, menu); 392 393 // Only add extra menu items for a saved note 394 if (mState == STATE_EDIT) { 395 // Append to the 396 // menu items for any other activities that can do stuff with it 397 // as well. This does a query on the system for any activities that 398 // implement the ALTERNATIVE_ACTION for our data, adding a menu item 399 // for each one that is found. 400 Intent intent = new Intent(null, mUri); 401 intent.addCategory(Intent.CATEGORY_ALTERNATIVE); 402 menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0, 403 new ComponentName(this, NoteEditor.class), null, intent, 0, null); 404 } 405 406 return super.onCreateOptionsMenu(menu); 407 } 408 409 @Override 410 public boolean onPrepareOptionsMenu(Menu menu) { 411 // Check if note has changed and enable/disable the revert option 412 int colNoteIndex = mCursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_NOTE); 413 String savedNote = mCursor.getString(colNoteIndex); 414 String currentNote = mText.getText().toString(); 415 if (savedNote.equals(currentNote)) { 416 menu.findItem(R.id.menu_revert).setVisible(false); 417 } else { 418 menu.findItem(R.id.menu_revert).setVisible(true); 419 } 420 return super.onPrepareOptionsMenu(menu); 421 } 422 423 /** 424 * This method is called when a menu item is selected. Android passes in the selected item. 425 * The switch statement in this method calls the appropriate method to perform the action the 426 * user chose. 427 * 428 * @param item The selected MenuItem 429 * @return True to indicate that the item was processed, and no further work is necessary. False 430 * to proceed to further processing as indicated in the MenuItem object. 431 */ 432 @Override 433 public boolean onOptionsItemSelected(MenuItem item) { 434 // Handle all of the possible menu actions. 435 switch (item.getItemId()) { 436 case R.id.menu_save: 437 String text = mText.getText().toString(); 438 updateNote(text, null); 439 finish(); 440 break; 441 case R.id.menu_delete: 442 deleteNote(); 443 finish(); 444 break; 445 case R.id.menu_revert: 446 cancelNote(); 447 break; 448 } 449 return super.onOptionsItemSelected(item); 450 } 451 452 //BEGIN_INCLUDE(paste) 453 /** 454 * A helper method that replaces the note's data with the contents of the clipboard. 455 */ 456 private final void performPaste() { 457 458 // Gets a handle to the Clipboard Manager 459 ClipboardManager clipboard = (ClipboardManager) 460 getSystemService(Context.CLIPBOARD_SERVICE); 461 462 // Gets a content resolver instance 463 ContentResolver cr = getContentResolver(); 464 465 // Gets the clipboard data from the clipboard 466 ClipData clip = clipboard.getPrimaryClip(); 467 if (clip != null) { 468 469 String text=null; 470 String title=null; 471 472 // Gets the first item from the clipboard data 473 ClipData.Item item = clip.getItemAt(0); 474 475 // Tries to get the item's contents as a URI pointing to a note 476 Uri uri = item.getUri(); 477 478 // Tests to see that the item actually is an URI, and that the URI 479 // is a content URI pointing to a provider whose MIME type is the same 480 // as the MIME type supported by the Note pad provider. 481 if (uri != null && NotePad.Notes.CONTENT_ITEM_TYPE.equals(cr.getType(uri))) { 482 483 // The clipboard holds a reference to data with a note MIME type. This copies it. 484 Cursor orig = cr.query( 485 uri, // URI for the content provider 486 PROJECTION, // Get the columns referred to in the projection 487 null, // No selection variables 488 null, // No selection variables, so no criteria are needed 489 null // Use the default sort order 490 ); 491 492 // If the Cursor is not null, and it contains at least one record 493 // (moveToFirst() returns true), then this gets the note data from it. 494 if (orig != null) { 495 if (orig.moveToFirst()) { 496 int colNoteIndex = mCursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_NOTE); 497 int colTitleIndex = mCursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_TITLE); 498 text = orig.getString(colNoteIndex); 499 title = orig.getString(colTitleIndex); 500 } 501 502 // Closes the cursor. 503 orig.close(); 504 } 505 } 506 507 // If the contents of the clipboard wasn't a reference to a note, then 508 // this converts whatever it is to text. 509 if (text == null) { 510 text = item.coerceToText(this).toString(); 511 } 512 513 // Updates the current note with the retrieved title and text. 514 updateNote(text, title); 515 } 516 } 517 //END_INCLUDE(paste) 518 519 /** 520 * Replaces the current note contents with the text and title provided as arguments. 521 * @param text The new note contents to use. 522 * @param title The new note title to use 523 */ 524 private final void updateNote(String text, String title) { 525 526 // Sets up a map to contain values to be updated in the provider. 527 ContentValues values = new ContentValues(); 528 values.put(NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE, System.currentTimeMillis()); 529 530 // If the action is to insert a new note, this creates an initial title for it. 531 if (mState == STATE_INSERT) { 532 533 // If no title was provided as an argument, create one from the note text. 534 if (title == null) { 535 536 // Get the note's length 537 int length = text.length(); 538 539 // Sets the title by getting a substring of the text that is 31 characters long 540 // or the number of characters in the note plus one, whichever is smaller. 541 title = text.substring(0, Math.min(30, length)); 542 543 // If the resulting length is more than 30 characters, chops off any 544 // trailing spaces 545 if (length > 30) { 546 int lastSpace = title.lastIndexOf(' '); 547 if (lastSpace > 0) { 548 title = title.substring(0, lastSpace); 549 } 550 } 551 } 552 // In the values map, sets the value of the title 553 values.put(NotePad.Notes.COLUMN_NAME_TITLE, title); 554 } else if (title != null) { 555 // In the values map, sets the value of the title 556 values.put(NotePad.Notes.COLUMN_NAME_TITLE, title); 557 } 558 559 // This puts the desired notes text into the map. 560 values.put(NotePad.Notes.COLUMN_NAME_NOTE, text); 561 562 /* 563 * Updates the provider with the new values in the map. The ListView is updated 564 * automatically. The provider sets this up by setting the notification URI for 565 * query Cursor objects to the incoming URI. The content resolver is thus 566 * automatically notified when the Cursor for the URI changes, and the UI is 567 * updated. 568 * Note: This is being done on the UI thread. It will block the thread until the 569 * update completes. In a sample app, going against a simple provider based on a 570 * local database, the block will be momentary, but in a real app you should use 571 * android.content.AsyncQueryHandler or android.os.AsyncTask. 572 */ 573 getContentResolver().update( 574 mUri, // The URI for the record to update. 575 values, // The map of column names and new values to apply to them. 576 null, // No selection criteria are used, so no where columns are necessary. 577 null // No where columns are used, so no where arguments are necessary. 578 ); 579 580 581 } 582 583 /** 584 * This helper method cancels the work done on a note. It deletes the note if it was 585 * newly created, or reverts to the original text of the note i 586 */ 587 private final void cancelNote() { 588 if (mCursor != null) { 589 if (mState == STATE_EDIT) { 590 // Put the original note text back into the database 591 mCursor.close(); 592 mCursor = null; 593 ContentValues values = new ContentValues(); 594 values.put(NotePad.Notes.COLUMN_NAME_NOTE, mOriginalContent); 595 getContentResolver().update(mUri, values, null, null); 596 } else if (mState == STATE_INSERT) { 597 // We inserted an empty note, make sure to delete it 598 deleteNote(); 599 } 600 } 601 setResult(RESULT_CANCELED); 602 finish(); 603 } 604 605 /** 606 * Take care of deleting a note. Simply deletes the entry. 607 */ 608 private final void deleteNote() { 609 if (mCursor != null) { 610 mCursor.close(); 611 mCursor = null; 612 getContentResolver().delete(mUri, null, null); 613 mText.setText(""); 614 } 615 } 616 } 617