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.app.LoaderManager; 21 import android.content.ClipData; 22 import android.content.ClipboardManager; 23 import android.content.ComponentName; 24 import android.content.ContentResolver; 25 import android.content.ContentValues; 26 import android.content.Context; 27 import android.content.CursorLoader; 28 import android.content.Intent; 29 import android.content.Loader; 30 import android.content.res.Resources; 31 import android.database.Cursor; 32 import android.graphics.Canvas; 33 import android.graphics.Paint; 34 import android.graphics.Rect; 35 import android.net.Uri; 36 import android.os.Bundle; 37 import android.util.AttributeSet; 38 import android.util.Log; 39 import android.view.Menu; 40 import android.view.MenuInflater; 41 import android.view.MenuItem; 42 import android.widget.EditText; 43 import com.example.android.notepad.NotePad.Notes; 44 45 /** 46 * This Activity handles "editing" a note, where editing is responding to 47 * {@link Intent#ACTION_VIEW} (request to view data), edit a note 48 * {@link Intent#ACTION_EDIT}, create a note {@link Intent#ACTION_INSERT}, or 49 * create a new note from the current contents of the clipboard {@link Intent#ACTION_PASTE}. 50 */ 51 public class NoteEditor extends Activity implements LoaderManager.LoaderCallbacks<Cursor> { 52 // For logging and debugging purposes 53 private static final String TAG = "NoteEditor"; 54 55 /* 56 * Creates a projection that returns the note ID and the note contents. 57 */ 58 private static final String[] PROJECTION = 59 new String[] { 60 NotePad.Notes._ID, 61 NotePad.Notes.COLUMN_NAME_TITLE, 62 NotePad.Notes.COLUMN_NAME_NOTE 63 }; 64 65 // A label for the saved state of the activity 66 private static final String ORIGINAL_CONTENT = "origContent"; 67 68 // This Activity can be started by more than one action. Each action is represented 69 // as a "state" constant 70 private static final int STATE_EDIT = 0; 71 private static final int STATE_INSERT = 1; 72 73 private static final int LOADER_ID = 1; 74 75 // Global mutable variables 76 private int mState; 77 private Uri mUri; 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 LinedEditText(Context context, AttributeSet attrs)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 onDraw(Canvas canvas)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 onCreate(Bundle savedInstanceState)139 protected void onCreate(Bundle savedInstanceState) { 140 super.onCreate(savedInstanceState); 141 142 // Recovering the instance state from a previously destroyed Activity instance 143 if (savedInstanceState != null) { 144 mOriginalContent = savedInstanceState.getString(ORIGINAL_CONTENT); 145 } 146 147 /* 148 * Creates an Intent to use when the Activity object's result is sent back to the 149 * caller. 150 */ 151 final Intent intent = getIntent(); 152 153 /* 154 * Sets up for the edit, based on the action specified for the incoming Intent. 155 */ 156 157 // Gets the action that triggered the intent filter for this Activity 158 final String action = intent.getAction(); 159 160 // For an edit action: 161 if (Intent.ACTION_EDIT.equals(action)) { 162 163 // Sets the Activity state to EDIT, and gets the URI for the data to be edited. 164 mState = STATE_EDIT; 165 mUri = intent.getData(); 166 167 // For an insert or paste action: 168 } else if (Intent.ACTION_INSERT.equals(action) 169 || Intent.ACTION_PASTE.equals(action)) { 170 171 // Sets the Activity state to INSERT, gets the general note URI, and inserts an 172 // empty record in the provider 173 mState = STATE_INSERT; 174 setTitle(getText(R.string.title_create)); 175 176 mUri = getContentResolver().insert(intent.getData(), null); 177 178 /* 179 * If the attempt to insert the new note fails, shuts down this Activity. The 180 * originating Activity receives back RESULT_CANCELED if it requested a result. 181 * Logs that the insert failed. 182 */ 183 if (mUri == null) { 184 185 // Writes the log identifier, a message, and the URI that failed. 186 Log.e(TAG, "Failed to insert new note into " + getIntent().getData()); 187 188 // Closes the activity. 189 finish(); 190 return; 191 } 192 193 // Since the new entry was created, this sets the result to be returned 194 // set the result to be returned. 195 setResult(RESULT_OK, (new Intent()).setAction(mUri.toString())); 196 197 // If the action was other than EDIT or INSERT: 198 } else { 199 200 // Logs an error that the action was not understood, finishes the Activity, and 201 // returns RESULT_CANCELED to an originating Activity. 202 Log.e(TAG, "Unknown action, exiting"); 203 finish(); 204 return; 205 } 206 207 // Initialize the LoaderManager and start the query 208 getLoaderManager().initLoader(LOADER_ID, null, this); 209 210 // For a paste, initializes the data from clipboard. 211 if (Intent.ACTION_PASTE.equals(action)) { 212 // Does the paste 213 performPaste(); 214 // Switches the state to EDIT so the title can be modified. 215 mState = STATE_EDIT; 216 } 217 218 // Sets the layout for this Activity. See res/layout/note_editor.xml 219 setContentView(R.layout.note_editor); 220 221 // Gets a handle to the EditText in the the layout. 222 mText = (EditText) findViewById(R.id.note); 223 } 224 225 226 /** 227 * This method is called when an Activity loses focus during its normal operation. 228 * The Activity has a chance to save its state so that the system can restore 229 * it. 230 * 231 * Notice that this method isn't a normal part of the Activity lifecycle. It won't be called 232 * if the user simply navigates away from the Activity. 233 */ 234 @Override onSaveInstanceState(Bundle outState)235 protected void onSaveInstanceState(Bundle outState) { 236 // Save away the original text, so we still have it if the activity 237 // needs to be re-created. 238 outState.putString(ORIGINAL_CONTENT, mOriginalContent); 239 // Call the superclass to save the any view hierarchy state 240 super.onSaveInstanceState(outState); 241 } 242 243 /** 244 * This method is called when the Activity loses focus. 245 * 246 * While there is no need to override this method in this app, it is shown here to highlight 247 * that we are not saving any state in onPause, but have moved app state saving to onStop 248 * callback. 249 * In earlier versions of this app and popular literature it had been shown that onPause is good 250 * place to persist any unsaved work, however, this is not really a good practice because of how 251 * application and process lifecycle behave. 252 * As a general guideline apps should have a way of saving their business logic that does not 253 * solely rely on Activity (or other component) lifecyle state transitions. 254 * As a backstop you should save any app state, not saved during lifetime of the Activity, in 255 * onStop(). 256 * For a more detailed explanation of this recommendation please read 257 * <a href = "https://developer.android.com/guide/topics/processes/process-lifecycle.html"> 258 * Processes and Application Life Cycle </a>. 259 * <a href="https://developer.android.com/training/basics/activity-lifecycle/pausing.html"> 260 * Pausing and Resuming an Activity </a>. 261 */ 262 @Override onPause()263 protected void onPause() { 264 super.onPause(); 265 } 266 267 /** 268 * This method is called when the Activity becomes invisible. 269 * 270 * For Activity objects that edit information, onStop() may be the one place where changes maybe 271 * saved. 272 * 273 * If the user hasn't done anything, then this deletes or clears out the note, otherwise it 274 * writes the user's work to the provider. 275 */ 276 @Override onStop()277 protected void onStop() { 278 super.onStop(); 279 280 // Get the current note text. 281 String text = mText.getText().toString(); 282 int length = text.length(); 283 284 /* 285 * If the Activity is in the midst of finishing and there is no text in the current 286 * note, returns a result of CANCELED to the caller, and deletes the note. This is done 287 * even if the note was being edited, the assumption being that the user wanted to 288 * "clear out" (delete) the note. 289 */ 290 if (isFinishing() && (length == 0)) { 291 setResult(RESULT_CANCELED); 292 deleteNote(); 293 294 /* 295 * Writes the edits to the provider. The note has been edited if an existing note 296 * was retrieved into the editor *or* if a new note was inserted. 297 * In the latter case, onCreate() inserted a new empty note into the provider, 298 * and it is this new note that is being edited. 299 */ 300 } else if (mState == STATE_EDIT) { 301 // Creates a map to contain the new values for the columns 302 updateNote(text, null); 303 } else if (mState == STATE_INSERT) { 304 updateNote(text, text); 305 mState = STATE_EDIT; 306 } 307 } 308 309 /** 310 * This method is called when the user clicks the device's Menu button the first time for 311 * this Activity. Android passes in a Menu object that is populated with items. 312 * 313 * Builds the menus for editing and inserting, and adds in alternative actions that 314 * registered themselves to handle the MIME types for this application. 315 * 316 * @param menu A Menu object to which items should be added. 317 * @return True to display the menu. 318 */ 319 @Override onCreateOptionsMenu(Menu menu)320 public boolean onCreateOptionsMenu(Menu menu) { 321 // Inflate menu from XML resource 322 MenuInflater inflater = getMenuInflater(); 323 inflater.inflate(R.menu.editor_options_menu, menu); 324 325 // Only add extra menu items for a saved note 326 if (mState == STATE_EDIT) { 327 // Append to the 328 // menu items for any other activities that can do stuff with it 329 // as well. This does a query on the system for any activities that 330 // implement the ALTERNATIVE_ACTION for our data, adding a menu item 331 // for each one that is found. 332 Intent intent = new Intent(null, mUri); 333 intent.addCategory(Intent.CATEGORY_ALTERNATIVE); 334 menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0, 335 new ComponentName(this, NoteEditor.class), null, intent, 0, null); 336 } 337 338 return super.onCreateOptionsMenu(menu); 339 } 340 341 @Override onPrepareOptionsMenu(Menu menu)342 public boolean onPrepareOptionsMenu(Menu menu) { 343 // Check if note has changed and enable/disable the revert option 344 Cursor cursor = getContentResolver().query( 345 mUri, // The URI for the note that is to be retrieved. 346 PROJECTION, // The columns to retrieve 347 null, // No selection criteria are used, so no where columns are needed. 348 null, // No where columns are used, so no where values are needed. 349 null // No sort order is needed. 350 ); 351 cursor.moveToFirst(); 352 int colNoteIndex = cursor.getColumnIndex(Notes.COLUMN_NAME_NOTE); 353 String savedNote = cursor.getString(colNoteIndex); 354 String currentNote = mText.getText().toString(); 355 if (savedNote.equals(currentNote)) { 356 menu.findItem(R.id.menu_revert).setVisible(false); 357 } else { 358 menu.findItem(R.id.menu_revert).setVisible(true); 359 } 360 return super.onPrepareOptionsMenu(menu); 361 } 362 363 /** 364 * This method is called when a menu item is selected. Android passes in the selected item. 365 * The switch statement in this method calls the appropriate method to perform the action the 366 * user chose. 367 * 368 * @param item The selected MenuItem 369 * @return True to indicate that the item was processed, and no further work is necessary. False 370 * to proceed to further processing as indicated in the MenuItem object. 371 */ 372 @Override onOptionsItemSelected(MenuItem item)373 public boolean onOptionsItemSelected(MenuItem item) { 374 // Handle all of the possible menu actions. 375 switch (item.getItemId()) { 376 case R.id.menu_save: 377 String text = mText.getText().toString(); 378 updateNote(text, null); 379 finish(); 380 break; 381 case R.id.menu_delete: 382 deleteNote(); 383 finish(); 384 break; 385 case R.id.menu_revert: 386 cancelNote(); 387 break; 388 } 389 return super.onOptionsItemSelected(item); 390 } 391 392 //BEGIN_INCLUDE(paste) 393 /** 394 * A helper method that replaces the note's data with the contents of the clipboard. 395 */ performPaste()396 private final void performPaste() { 397 398 // Gets a handle to the Clipboard Manager 399 ClipboardManager clipboard = (ClipboardManager) 400 getSystemService(Context.CLIPBOARD_SERVICE); 401 402 // Gets a content resolver instance 403 ContentResolver cr = getContentResolver(); 404 405 // Gets the clipboard data from the clipboard 406 ClipData clip = clipboard.getPrimaryClip(); 407 if (clip != null) { 408 409 String text=null; 410 String title=null; 411 412 // Gets the first item from the clipboard data 413 ClipData.Item item = clip.getItemAt(0); 414 415 // Tries to get the item's contents as a URI pointing to a note 416 Uri uri = item.getUri(); 417 418 // Tests to see that the item actually is an URI, and that the URI 419 // is a content URI pointing to a provider whose MIME type is the same 420 // as the MIME type supported by the Note pad provider. 421 if (uri != null && NotePad.Notes.CONTENT_ITEM_TYPE.equals(cr.getType(uri))) { 422 423 // The clipboard holds a reference to data with a note MIME type. This copies it. 424 Cursor orig = cr.query( 425 uri, // URI for the content provider 426 PROJECTION, // Get the columns referred to in the projection 427 null, // No selection variables 428 null, // No selection variables, so no criteria are needed 429 null // Use the default sort order 430 ); 431 432 // If the Cursor is not null, and it contains at least one record 433 // (moveToFirst() returns true), then this gets the note data from it. 434 if (orig != null) { 435 if (orig.moveToFirst()) { 436 int colNoteIndex = orig.getColumnIndex(NotePad.Notes.COLUMN_NAME_NOTE); 437 int colTitleIndex = orig.getColumnIndex(NotePad.Notes.COLUMN_NAME_TITLE); 438 text = orig.getString(colNoteIndex); 439 title = orig.getString(colTitleIndex); 440 } 441 442 // Closes the cursor. 443 orig.close(); 444 } 445 } 446 447 // If the contents of the clipboard wasn't a reference to a note, then 448 // this converts whatever it is to text. 449 if (text == null) { 450 text = item.coerceToText(this).toString(); 451 } 452 453 // Updates the current note with the retrieved title and text. 454 updateNote(text, title); 455 } 456 } 457 //END_INCLUDE(paste) 458 459 /** 460 * Replaces the current note contents with the text and title provided as arguments. 461 * @param text The new note contents to use. 462 * @param title The new note title to use 463 */ updateNote(String text, String title)464 private final void updateNote(String text, String title) { 465 466 // Sets up a map to contain values to be updated in the provider. 467 ContentValues values = new ContentValues(); 468 values.put(NotePad.Notes.COLUMN_NAME_MODIFICATION_DATE, System.currentTimeMillis()); 469 470 // If the action is to insert a new note, this creates an initial title for it. 471 if (mState == STATE_INSERT) { 472 473 // If no title was provided as an argument, create one from the note text. 474 if (title == null) { 475 476 // Get the note's length 477 int length = text.length(); 478 479 // Sets the title by getting a substring of the text that is 31 characters long 480 // or the number of characters in the note plus one, whichever is smaller. 481 title = text.substring(0, Math.min(30, length)); 482 483 // If the resulting length is more than 30 characters, chops off any 484 // trailing spaces 485 if (length > 30) { 486 int lastSpace = title.lastIndexOf(' '); 487 if (lastSpace > 0) { 488 title = title.substring(0, lastSpace); 489 } 490 } 491 } 492 // In the values map, sets the value of the title 493 values.put(NotePad.Notes.COLUMN_NAME_TITLE, title); 494 } else if (title != null) { 495 // In the values map, sets the value of the title 496 values.put(NotePad.Notes.COLUMN_NAME_TITLE, title); 497 } 498 499 // This puts the desired notes text into the map. 500 values.put(NotePad.Notes.COLUMN_NAME_NOTE, text); 501 502 /* 503 * Updates the provider with the new values in the map. The ListView is updated 504 * automatically. The provider sets this up by setting the notification URI for 505 * query Cursor objects to the incoming URI. The content resolver is thus 506 * automatically notified when the Cursor for the URI changes, and the UI is 507 * updated. 508 * Note: This is being done on the UI thread. It will block the thread until the 509 * update completes. In a sample app, going against a simple provider based on a 510 * local database, the block will be momentary, but in a real app you should use 511 * android.content.AsyncQueryHandler or android.os.AsyncTask. 512 */ 513 getContentResolver().update( 514 mUri, // The URI for the record to update. 515 values, // The map of column names and new values to apply to them. 516 null, // No selection criteria are used, so no where columns are necessary. 517 null // No where columns are used, so no where arguments are necessary. 518 ); 519 } 520 521 /** 522 * This helper method cancels the work done on a note. It deletes the note if it was 523 * newly created, or reverts to the original text of the note i 524 */ cancelNote()525 private final void cancelNote() { 526 527 if (mState == STATE_EDIT) { 528 // Put the original note text back into the database 529 ContentValues values = new ContentValues(); 530 values.put(NotePad.Notes.COLUMN_NAME_NOTE, mOriginalContent); 531 getContentResolver().update(mUri, values, null, null); 532 } else if (mState == STATE_INSERT) { 533 // We inserted an empty note, make sure to delete it 534 deleteNote(); 535 } 536 537 setResult(RESULT_CANCELED); 538 finish(); 539 } 540 541 /** 542 * Take care of deleting a note. Simply deletes the entry. 543 */ deleteNote()544 private final void deleteNote() { 545 getContentResolver().delete(mUri, null, null); 546 mText.setText(""); 547 } 548 549 // LoaderManager callbacks 550 @Override onCreateLoader(int i, Bundle bundle)551 public Loader<Cursor> onCreateLoader(int i, Bundle bundle) { 552 return new CursorLoader( 553 this, 554 mUri, // The URI for the note that is to be retrieved. 555 PROJECTION, // The columns to retrieve 556 null, // No selection criteria are used, so no where columns are needed. 557 null, // No where columns are used, so no where values are needed. 558 null // No sort order is needed. 559 ); 560 } 561 562 @Override onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor)563 public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) { 564 565 // Modifies the window title for the Activity according to the current Activity state. 566 if (cursor != null && cursor.moveToFirst() && mState == STATE_EDIT) { 567 // Set the title of the Activity to include the note title 568 int colTitleIndex = cursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_TITLE); 569 int colNoteIndex = cursor.getColumnIndex(NotePad.Notes.COLUMN_NAME_NOTE); 570 571 // Gets the title and sets it 572 String title = cursor.getString(colTitleIndex); 573 Resources res = getResources(); 574 String text = String.format(res.getString(R.string.title_edit), title); 575 setTitle(text); 576 577 // Gets the note text from the Cursor and puts it in the TextView, but doesn't change 578 // the text cursor's position. 579 580 String note = cursor.getString(colNoteIndex); 581 mText.setTextKeepState(note); 582 // Stores the original note text, to allow the user to revert changes. 583 if (mOriginalContent == null) { 584 mOriginalContent = note; 585 } 586 } 587 } 588 589 @Override onLoaderReset(Loader<Cursor> cursorLoader)590 public void onLoaderReset(Loader<Cursor> cursorLoader) {} 591 } 592