• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
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         /*
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
onResume()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
onSaveInstanceState(Bundle outState)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
onPause()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
onCreateOptionsMenu(Menu menu)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
onPrepareOptionsMenu(Menu menu)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
onOptionsItemSelected(MenuItem item)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      */
performPaste()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      */
updateNote(String text, String title)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      */
cancelNote()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      */
deleteNote()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