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
17package com.example.android.notepad;
18
19import android.app.Activity;
20import android.content.ClipData;
21import android.content.ClipboardManager;
22import android.content.ComponentName;
23import android.content.ContentResolver;
24import android.content.ContentValues;
25import android.content.Context;
26import android.content.Intent;
27import android.content.res.Resources;
28import android.database.Cursor;
29import android.graphics.Canvas;
30import android.graphics.Paint;
31import android.graphics.Rect;
32import android.net.Uri;
33import android.os.Bundle;
34import android.util.AttributeSet;
35import android.util.Log;
36import android.view.Menu;
37import android.view.MenuInflater;
38import android.view.MenuItem;
39import 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 */
52public 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