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.NoteColumns; |
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.content.res.Resources; |
27 | import android.database.Cursor; |
28 | import android.graphics.Canvas; |
29 | import android.graphics.Paint; |
30 | import android.graphics.Rect; |
31 | import android.net.Uri; |
32 | import android.os.Bundle; |
33 | import android.util.AttributeSet; |
34 | import android.util.Log; |
35 | import android.view.Menu; |
36 | import android.view.MenuInflater; |
37 | import android.view.MenuItem; |
38 | import android.widget.EditText; |
39 | import android.widget.Toast; |
40 | |
41 | /** |
42 | * A generic activity for editing a note in a database. This can be used |
43 | * either to simply view a note {@link Intent#ACTION_VIEW}, view and edit a note |
44 | * {@link Intent#ACTION_EDIT}, or create a new note {@link Intent#ACTION_INSERT}. |
45 | */ |
46 | public class NoteEditor extends Activity { |
47 | private static final String TAG = "NoteEditor"; |
48 | |
49 | /** |
50 | * Standard projection for the interesting columns of a normal note. |
51 | */ |
52 | private static final String[] PROJECTION = new String[] { |
53 | NoteColumns._ID, // 0 |
54 | NoteColumns.NOTE, // 1 |
55 | NoteColumns.TITLE, // 2 |
56 | }; |
57 | /** The index of the note column */ |
58 | private static final int COLUMN_INDEX_NOTE = 1; |
59 | /** The index of the title column */ |
60 | private static final int COLUMN_INDEX_TITLE = 2; |
61 | |
62 | // This is our state data that is stored when freezing. |
63 | private static final String ORIGINAL_CONTENT = "origContent"; |
64 | |
65 | // The different distinct states the activity can be run in. |
66 | private static final int STATE_EDIT = 0; |
67 | private static final int STATE_INSERT = 1; |
68 | |
69 | private int mState; |
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 |
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 |
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 |
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 | final String action = intent.getAction(); |
116 | if (Intent.ACTION_EDIT.equals(action)) { |
117 | // Requested to edit: set that state, and the data being edited. |
118 | mState = STATE_EDIT; |
119 | mUri = intent.getData(); |
120 | } else if (Intent.ACTION_INSERT.equals(action)) { |
121 | // Requested to insert: set that state, and create a new entry |
122 | // in the container. |
123 | mState = STATE_INSERT; |
124 | mUri = getContentResolver().insert(intent.getData(), null); |
125 | |
126 | // If we were unable to create a new note, then just finish |
127 | // this activity. A RESULT_CANCELED will be sent back to the |
128 | // original activity if they requested a result. |
129 | if (mUri == null) { |
130 | Log.e(TAG, "Failed to insert new note into " + getIntent().getData()); |
131 | finish(); |
132 | return; |
133 | } |
134 | |
135 | // The new entry was created, so assume all will end well and |
136 | // set the result to be returned. |
137 | setResult(RESULT_OK, (new Intent()).setAction(mUri.toString())); |
138 | |
139 | } else { |
140 | // Whoops, unknown action! Bail. |
141 | Log.e(TAG, "Unknown action, exiting"); |
142 | finish(); |
143 | return; |
144 | } |
145 | |
146 | // Set the layout for this activity. You can find it in res/layout/note_editor.xml |
147 | setContentView(R.layout.note_editor); |
148 | |
149 | // The text view for our note, identified by its ID in the XML file. |
150 | mText = (EditText) findViewById(R.id.note); |
151 | |
152 | // Get the note! |
153 | mCursor = managedQuery(mUri, PROJECTION, null, null, null); |
154 | |
155 | // If an instance of this activity had previously stopped, we can |
156 | // get the original text it started with. |
157 | if (savedInstanceState != null) { |
158 | mOriginalContent = savedInstanceState.getString(ORIGINAL_CONTENT); |
159 | } |
160 | } |
161 | |
162 | @Override |
163 | protected void onResume() { |
164 | super.onResume(); |
165 | // If we didn't have any trouble retrieving the data, it is now |
166 | // time to get at the stuff. |
167 | if (mCursor != null) { |
168 | // Requery in case something changed while paused (such as the title) |
169 | mCursor.requery(); |
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 | // Set the title of the Activity to include the note title |
176 | String title = mCursor.getString(COLUMN_INDEX_TITLE); |
177 | Resources res = getResources(); |
178 | String text = String.format(res.getString(R.string.title_edit), title); |
179 | setTitle(text); |
180 | } else if (mState == STATE_INSERT) { |
181 | setTitle(getText(R.string.title_create)); |
182 | } |
183 | |
184 | // This is a little tricky: we may be resumed after previously being |
185 | // paused/stopped. We want to put the new text in the text view, |
186 | // but leave the user where they were (retain the cursor position |
187 | // etc). This version of setText does that for us. |
188 | String note = mCursor.getString(COLUMN_INDEX_NOTE); |
189 | mText.setTextKeepState(note); |
190 | |
191 | // If we hadn't previously retrieved the original text, do so |
192 | // now. This allows the user to revert their changes. |
193 | if (mOriginalContent == null) { |
194 | mOriginalContent = note; |
195 | } |
196 | |
197 | } else { |
198 | setTitle(getText(R.string.error_title)); |
199 | mText.setText(getText(R.string.error_message)); |
200 | } |
201 | } |
202 | |
203 | @Override |
204 | protected void onSaveInstanceState(Bundle outState) { |
205 | // Save away the original text, so we still have it if the activity |
206 | // needs to be killed while paused. |
207 | outState.putString(ORIGINAL_CONTENT, mOriginalContent); |
208 | } |
209 | |
210 | @Override |
211 | protected void onPause() { |
212 | super.onPause(); |
213 | // The user is going somewhere, so make sure changes are saved |
214 | |
215 | String text = mText.getText().toString(); |
216 | int length = text.length(); |
217 | |
218 | // If this activity is finished, and there is no text, then we |
219 | // simply delete the note entry. |
220 | // Note that we do this both for editing and inserting... it |
221 | // would be reasonable to only do it when inserting. |
222 | if (isFinishing() && (length == 0) && mCursor != null) { |
223 | setResult(RESULT_CANCELED); |
224 | deleteNote(); |
225 | } else { |
226 | saveNote(); |
227 | } |
228 | } |
229 | |
230 | @Override |
231 | public boolean onCreateOptionsMenu(Menu menu) { |
232 | // Inflate menu from XML resource |
233 | MenuInflater inflater = getMenuInflater(); |
234 | inflater.inflate(R.menu.editor_options_menu, menu); |
235 | |
236 | // Append to the |
237 | // menu items for any other activities that can do stuff with it |
238 | // as well. This does a query on the system for any activities that |
239 | // implement the ALTERNATIVE_ACTION for our data, adding a menu item |
240 | // for each one that is found. |
241 | Intent intent = new Intent(null, getIntent().getData()); |
242 | intent.addCategory(Intent.CATEGORY_ALTERNATIVE); |
243 | menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0, |
244 | new ComponentName(this, NoteEditor.class), null, intent, 0, null); |
245 | |
246 | return super.onCreateOptionsMenu(menu); |
247 | } |
248 | |
249 | |
250 | |
251 | @Override |
252 | public boolean onPrepareOptionsMenu(Menu menu) { |
253 | if (mState == STATE_EDIT) { |
254 | menu.setGroupVisible(R.id.menu_group_edit, true); |
255 | menu.setGroupVisible(R.id.menu_group_insert, false); |
256 | |
257 | // Check if note has changed and enable/disable the revert option |
258 | String savedNote = mCursor.getString(COLUMN_INDEX_NOTE); |
259 | String currentNote = mText.getText().toString(); |
260 | if (savedNote.equals(currentNote)) { |
261 | menu.findItem(R.id.menu_revert).setEnabled(false); |
262 | } else { |
263 | menu.findItem(R.id.menu_revert).setEnabled(true); |
264 | } |
265 | } else { |
266 | menu.setGroupVisible(R.id.menu_group_edit, false); |
267 | menu.setGroupVisible(R.id.menu_group_insert, true); |
268 | } |
269 | return super.onPrepareOptionsMenu(menu); |
270 | } |
271 | |
272 | @Override |
273 | public boolean onOptionsItemSelected(MenuItem item) { |
274 | // Handle all of the possible menu actions. |
275 | switch (item.getItemId()) { |
276 | case R.id.menu_save: |
277 | saveNote(); |
278 | finish(); |
279 | break; |
280 | case R.id.menu_delete: |
281 | deleteNote(); |
282 | finish(); |
283 | break; |
284 | case R.id.menu_revert: |
285 | case R.id.menu_discard: |
286 | cancelNote(); |
287 | break; |
288 | } |
289 | return super.onOptionsItemSelected(item); |
290 | |
291 | } |
292 | |
293 | private final void saveNote() { |
294 | // Make sure their current |
295 | // changes are safely saved away in the provider. We don't need |
296 | // to do this if only editing. |
297 | if (mCursor != null) { |
298 | // Get out updates into the provider. |
299 | ContentValues values = new ContentValues(); |
300 | |
301 | // Bump the modification time to now. |
302 | values.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); |
303 | |
304 | String text = mText.getText().toString(); |
305 | int length = text.length(); |
306 | // If we are creating a new note, then we want to also create |
307 | // an initial title for it. |
308 | if (mState == STATE_INSERT) { |
309 | if (length == 0) { |
310 | Toast.makeText(this, R.string.nothing_to_save, Toast.LENGTH_SHORT).show(); |
311 | return; |
312 | } |
313 | String title = text.substring(0, Math.min(30, length)); |
314 | if (length > 30) { |
315 | int lastSpace = title.lastIndexOf(' '); |
316 | if (lastSpace > 0) { |
317 | title = title.substring(0, lastSpace); |
318 | } |
319 | } |
320 | values.put(NoteColumns.TITLE, title); |
321 | } |
322 | |
323 | // Write our text back into the provider. |
324 | values.put(NoteColumns.NOTE, text); |
325 | |
326 | // Commit all of our changes to persistent storage. When the update completes |
327 | // the content provider will notify the cursor of the change, which will |
328 | // cause the UI to be updated. |
329 | try { |
330 | getContentResolver().update(mUri, values, null, null); |
331 | } catch (NullPointerException e) { |
332 | Log.e(TAG, e.getMessage()); |
333 | } |
334 | |
335 | } |
336 | } |
337 | |
338 | /** |
339 | * Take care of canceling work on a note. Deletes the note if we |
340 | * had created it, otherwise reverts to the original text. |
341 | */ |
342 | private final void cancelNote() { |
343 | if (mCursor != null) { |
344 | if (mState == STATE_EDIT) { |
345 | // Put the original note text back into the database |
346 | mCursor.close(); |
347 | mCursor = null; |
348 | ContentValues values = new ContentValues(); |
349 | values.put(NoteColumns.NOTE, mOriginalContent); |
350 | getContentResolver().update(mUri, values, null, null); |
351 | } else if (mState == STATE_INSERT) { |
352 | // We inserted an empty note, make sure to delete it |
353 | deleteNote(); |
354 | } |
355 | } |
356 | setResult(RESULT_CANCELED); |
357 | finish(); |
358 | } |
359 | |
360 | /** |
361 | * Take care of deleting a note. Simply deletes the entry. |
362 | */ |
363 | private final void deleteNote() { |
364 | if (mCursor != null) { |
365 | mCursor.close(); |
366 | mCursor = null; |
367 | getContentResolver().delete(mUri, null, null); |
368 | mText.setText(""); |
369 | } |
370 | } |
371 | } |