Home | History | Annotate | Download | only in notepad
      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
     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         // 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
    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
    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
    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
    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
    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
    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      */
    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      */
    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      */
    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      */
    544     private final void deleteNote() {
    545         getContentResolver().delete(mUri, null, null);
    546         mText.setText("");
    547     }
    548 
    549     // LoaderManager callbacks
    550     @Override
    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
    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
    590     public void onLoaderReset(Loader<Cursor> cursorLoader) {}
    591 }
    592