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