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 com.example.android.notepad.NotePad.Notes; 20 21 import android.app.Activity; 22 import android.content.ComponentName; 23 import android.content.ContentValues; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.database.Cursor; 27 import android.graphics.Canvas; 28 import android.graphics.Paint; 29 import android.graphics.Rect; 30 import android.net.Uri; 31 import android.os.Bundle; 32 import android.util.AttributeSet; 33 import android.util.Log; 34 import android.view.Menu; 35 import android.view.MenuItem; 36 import android.widget.EditText; 37 38 /** 39 * A generic activity for editing a note in a database. This can be used 40 * either to simply view a note {@link Intent#ACTION_VIEW}, view and edit a note 41 * {@link Intent#ACTION_EDIT}, or create a new note {@link Intent#ACTION_INSERT}. 42 */ 43 public class NoteEditor extends Activity { 44 private static final String TAG = "Notes"; 45 46 /** 47 * Standard projection for the interesting columns of a normal note. 48 */ 49 private static final String[] PROJECTION = new String[] { 50 Notes._ID, // 0 51 Notes.NOTE, // 1 52 }; 53 /** The index of the note column */ 54 private static final int COLUMN_INDEX_NOTE = 1; 55 56 // This is our state data that is stored when freezing. 57 private static final String ORIGINAL_CONTENT = "origContent"; 58 59 // Identifiers for our menu items. 60 private static final int REVERT_ID = Menu.FIRST; 61 private static final int DISCARD_ID = Menu.FIRST + 1; 62 private static final int DELETE_ID = Menu.FIRST + 2; 63 64 // The different distinct states the activity can be run in. 65 private static final int STATE_EDIT = 0; 66 private static final int STATE_INSERT = 1; 67 68 private int mState; 69 private boolean mNoteOnly = false; 70 private Uri mUri; 71 private Cursor mCursor; 72 private EditText mText; 73 private String mOriginalContent; 74 75 /** 76 * A custom EditText that draws lines between each line of text that is displayed. 77 */ 78 public static class LinedEditText extends EditText { 79 private Rect mRect; 80 private Paint mPaint; 81 82 // we need this constructor for LayoutInflater LinedEditText(Context context, AttributeSet attrs)83 public LinedEditText(Context context, AttributeSet attrs) { 84 super(context, attrs); 85 86 mRect = new Rect(); 87 mPaint = new Paint(); 88 mPaint.setStyle(Paint.Style.STROKE); 89 mPaint.setColor(0x800000FF); 90 } 91 92 @Override onDraw(Canvas canvas)93 protected void onDraw(Canvas canvas) { 94 int count = getLineCount(); 95 Rect r = mRect; 96 Paint paint = mPaint; 97 98 for (int i = 0; i < count; i++) { 99 int baseline = getLineBounds(i, r); 100 101 canvas.drawLine(r.left, baseline + 1, r.right, baseline + 1, paint); 102 } 103 104 super.onDraw(canvas); 105 } 106 } 107 108 @Override onCreate(Bundle savedInstanceState)109 protected void onCreate(Bundle savedInstanceState) { 110 super.onCreate(savedInstanceState); 111 112 final Intent intent = getIntent(); 113 114 // Do some setup based on the action being performed. 115 116 final String action = intent.getAction(); 117 if (Intent.ACTION_EDIT.equals(action)) { 118 // Requested to edit: set that state, and the data being edited. 119 mState = STATE_EDIT; 120 mUri = intent.getData(); 121 } else if (Intent.ACTION_INSERT.equals(action)) { 122 // Requested to insert: set that state, and create a new entry 123 // in the container. 124 mState = STATE_INSERT; 125 mUri = getContentResolver().insert(intent.getData(), null); 126 127 // If we were unable to create a new note, then just finish 128 // this activity. A RESULT_CANCELED will be sent back to the 129 // original activity if they requested a result. 130 if (mUri == null) { 131 Log.e(TAG, "Failed to insert new note into " + getIntent().getData()); 132 finish(); 133 return; 134 } 135 136 // The new entry was created, so assume all will end well and 137 // set the result to be returned. 138 setResult(RESULT_OK, (new Intent()).setAction(mUri.toString())); 139 140 } else { 141 // Whoops, unknown action! Bail. 142 Log.e(TAG, "Unknown action, exiting"); 143 finish(); 144 return; 145 } 146 147 // Set the layout for this activity. You can find it in res/layout/note_editor.xml 148 setContentView(R.layout.note_editor); 149 150 // The text view for our note, identified by its ID in the XML file. 151 mText = (EditText) findViewById(R.id.note); 152 153 // Get the note! 154 mCursor = managedQuery(mUri, PROJECTION, null, null, null); 155 156 // If an instance of this activity had previously stopped, we can 157 // get the original text it started with. 158 if (savedInstanceState != null) { 159 mOriginalContent = savedInstanceState.getString(ORIGINAL_CONTENT); 160 } 161 } 162 163 @Override onResume()164 protected void onResume() { 165 super.onResume(); 166 167 // If we didn't have any trouble retrieving the data, it is now 168 // time to get at the stuff. 169 if (mCursor != null) { 170 // Make sure we are at the one and only row in the cursor. 171 mCursor.moveToFirst(); 172 173 // Modify our overall title depending on the mode we are running in. 174 if (mState == STATE_EDIT) { 175 setTitle(getText(R.string.title_edit)); 176 } else if (mState == STATE_INSERT) { 177 setTitle(getText(R.string.title_create)); 178 } 179 180 // This is a little tricky: we may be resumed after previously being 181 // paused/stopped. We want to put the new text in the text view, 182 // but leave the user where they were (retain the cursor position 183 // etc). This version of setText does that for us. 184 String note = mCursor.getString(COLUMN_INDEX_NOTE); 185 mText.setTextKeepState(note); 186 187 // If we hadn't previously retrieved the original text, do so 188 // now. This allows the user to revert their changes. 189 if (mOriginalContent == null) { 190 mOriginalContent = note; 191 } 192 193 } else { 194 setTitle(getText(R.string.error_title)); 195 mText.setText(getText(R.string.error_message)); 196 } 197 } 198 199 @Override onSaveInstanceState(Bundle outState)200 protected void onSaveInstanceState(Bundle outState) { 201 // Save away the original text, so we still have it if the activity 202 // needs to be killed while paused. 203 outState.putString(ORIGINAL_CONTENT, mOriginalContent); 204 } 205 206 @Override onPause()207 protected void onPause() { 208 super.onPause(); 209 210 // The user is going somewhere else, so make sure their current 211 // changes are safely saved away in the provider. We don't need 212 // to do this if only editing. 213 if (mCursor != null) { 214 String text = mText.getText().toString(); 215 int length = text.length(); 216 217 // If this activity is finished, and there is no text, then we 218 // do something a little special: simply delete the note entry. 219 // Note that we do this both for editing and inserting... it 220 // would be reasonable to only do it when inserting. 221 if (isFinishing() && (length == 0) && !mNoteOnly) { 222 setResult(RESULT_CANCELED); 223 deleteNote(); 224 225 // Get out updates into the provider. 226 } else { 227 ContentValues values = new ContentValues(); 228 229 // This stuff is only done when working with a full-fledged note. 230 if (!mNoteOnly) { 231 // Bump the modification time to now. 232 values.put(Notes.MODIFIED_DATE, System.currentTimeMillis()); 233 234 // If we are creating a new note, then we want to also create 235 // an initial title for it. 236 if (mState == STATE_INSERT) { 237 String title = text.substring(0, Math.min(30, length)); 238 if (length > 30) { 239 int lastSpace = title.lastIndexOf(' '); 240 if (lastSpace > 0) { 241 title = title.substring(0, lastSpace); 242 } 243 } 244 values.put(Notes.TITLE, title); 245 } 246 } 247 248 // Write our text back into the provider. 249 values.put(Notes.NOTE, text); 250 251 // Commit all of our changes to persistent storage. When the update completes 252 // the content provider will notify the cursor of the change, which will 253 // cause the UI to be updated. 254 getContentResolver().update(mUri, values, null, null); 255 } 256 } 257 } 258 259 @Override onCreateOptionsMenu(Menu menu)260 public boolean onCreateOptionsMenu(Menu menu) { 261 super.onCreateOptionsMenu(menu); 262 263 // Build the menus that are shown when editing. 264 if (mState == STATE_EDIT) { 265 menu.add(0, REVERT_ID, 0, R.string.menu_revert) 266 .setShortcut('0', 'r') 267 .setIcon(android.R.drawable.ic_menu_revert); 268 if (!mNoteOnly) { 269 menu.add(0, DELETE_ID, 0, R.string.menu_delete) 270 .setShortcut('1', 'd') 271 .setIcon(android.R.drawable.ic_menu_delete); 272 } 273 274 // Build the menus that are shown when inserting. 275 } else { 276 menu.add(0, DISCARD_ID, 0, R.string.menu_discard) 277 .setShortcut('0', 'd') 278 .setIcon(android.R.drawable.ic_menu_delete); 279 } 280 281 // If we are working on a full note, then append to the 282 // menu items for any other activities that can do stuff with it 283 // as well. This does a query on the system for any activities that 284 // implement the ALTERNATIVE_ACTION for our data, adding a menu item 285 // for each one that is found. 286 if (!mNoteOnly) { 287 Intent intent = new Intent(null, getIntent().getData()); 288 intent.addCategory(Intent.CATEGORY_ALTERNATIVE); 289 menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0, 290 new ComponentName(this, NoteEditor.class), null, intent, 0, null); 291 } 292 293 return true; 294 } 295 296 @Override onOptionsItemSelected(MenuItem item)297 public boolean onOptionsItemSelected(MenuItem item) { 298 // Handle all of the possible menu actions. 299 switch (item.getItemId()) { 300 case DELETE_ID: 301 deleteNote(); 302 finish(); 303 break; 304 case DISCARD_ID: 305 cancelNote(); 306 break; 307 case REVERT_ID: 308 cancelNote(); 309 break; 310 } 311 return super.onOptionsItemSelected(item); 312 } 313 314 /** 315 * Take care of canceling work on a note. Deletes the note if we 316 * had created it, otherwise reverts to the original text. 317 */ cancelNote()318 private final void cancelNote() { 319 if (mCursor != null) { 320 if (mState == STATE_EDIT) { 321 // Put the original note text back into the database 322 mCursor.close(); 323 mCursor = null; 324 ContentValues values = new ContentValues(); 325 values.put(Notes.NOTE, mOriginalContent); 326 getContentResolver().update(mUri, values, null, null); 327 } else if (mState == STATE_INSERT) { 328 // We inserted an empty note, make sure to delete it 329 deleteNote(); 330 } 331 } 332 setResult(RESULT_CANCELED); 333 finish(); 334 } 335 336 /** 337 * Take care of deleting a note. Simply deletes the entry. 338 */ deleteNote()339 private final void deleteNote() { 340 if (mCursor != null) { 341 mCursor.close(); 342 mCursor = null; 343 getContentResolver().delete(mUri, null, null); 344 mText.setText(""); 345 } 346 } 347 } 348