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