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.ListActivity; 20 import android.app.LoaderManager; 21 import android.content.ClipboardManager; 22 import android.content.ClipData; 23 import android.content.ComponentName; 24 import android.content.ContentUris; 25 import android.content.Context; 26 import android.content.CursorLoader; 27 import android.content.Intent; 28 import android.content.Loader; 29 import android.database.Cursor; 30 import android.net.Uri; 31 import android.os.Bundle; 32 import android.util.Log; 33 import android.view.ContextMenu; 34 import android.view.Menu; 35 import android.view.MenuInflater; 36 import android.view.MenuItem; 37 import android.view.View; 38 import android.view.ContextMenu.ContextMenuInfo; 39 import android.widget.AdapterView; 40 import android.widget.CursorAdapter; 41 import android.widget.ListView; 42 import android.widget.SimpleCursorAdapter; 43 44 /** 45 * Displays a list of notes. Will display notes from the {@link Uri} 46 * provided in the incoming Intent if there is one, otherwise it defaults to displaying the 47 * contents of the {@link NotePadProvider}. 48 */ 49 public class NotesList extends ListActivity implements LoaderManager.LoaderCallbacks<Cursor> { 50 51 // For logging and debugging 52 private static final String TAG = "NotesList"; 53 54 private static final int LOADER_ID = 0; 55 56 /** 57 * The columns needed by the cursor adapter 58 */ 59 private static final String[] PROJECTION = new String[] { 60 NotePad.Notes._ID, // 0 61 NotePad.Notes.COLUMN_NAME_TITLE, // 1 62 }; 63 64 /** The index of the title column */ 65 private static final int COLUMN_INDEX_TITLE = 1; 66 67 private SimpleCursorAdapter mAdapter; 68 69 /** 70 * onCreate is called when Android starts this Activity from scratch. 71 */ 72 @Override onCreate(Bundle savedInstanceState)73 protected void onCreate(Bundle savedInstanceState) { 74 super.onCreate(savedInstanceState); 75 76 // The user does not need to hold down the key to use menu shortcuts. 77 setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT); 78 79 /* If no data is given in the Intent that started this Activity, then this Activity 80 * was started when the intent filter matched a MAIN action. We should use the default 81 * provider URI. 82 */ 83 // Gets the intent that started this Activity. 84 Intent intent = getIntent(); 85 86 // If there is no data associated with the Intent, sets the data to the default URI, which 87 // accesses a list of notes. 88 if (intent.getData() == null) { 89 intent.setData(NotePad.Notes.CONTENT_URI); 90 } 91 92 /* 93 * Sets the callback for context menu activation for the ListView. The listener is set 94 * to be this Activity. The effect is that context menus are enabled for items in the 95 * ListView, and the context menu is handled by a method in NotesList. 96 */ 97 getListView().setOnCreateContextMenuListener(this); 98 99 100 /* 101 * The following two arrays create a "map" between columns in the cursor and view IDs 102 * for items in the ListView. Each element in the dataColumns array represents 103 * a column name; each element in the viewID array represents the ID of a View. 104 * The SimpleCursorAdapter maps them in ascending order to determine where each column 105 * value will appear in the ListView. 106 */ 107 108 // The names of the cursor columns to display in the view, initialized to the title column 109 String[] dataColumns = { NotePad.Notes.COLUMN_NAME_TITLE } ; 110 111 // The view IDs that will display the cursor columns, initialized to the TextView in 112 // noteslist_item.xml 113 int[] viewIDs = { android.R.id.text1 }; 114 115 // Creates the backing adapter for the ListView. 116 mAdapter = new SimpleCursorAdapter( 117 this, // The Context for the ListView 118 R.layout.noteslist_item, // Points to the XML for a list item 119 null, // The cursor is set by CursorLoader when loaded 120 dataColumns, 121 viewIDs, 122 CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER 123 ); 124 125 // Sets the ListView's adapter to be the cursor adapter that was just created. 126 setListAdapter(mAdapter); 127 // Initialize the LoaderManager and start the query 128 getLoaderManager().initLoader(LOADER_ID, null, this); 129 } 130 131 /** 132 * Called when the user clicks the device's Menu button the first time for 133 * this Activity. Android passes in a Menu object that is populated with items. 134 * 135 * Sets up a menu that provides the Insert option plus a list of alternative actions for 136 * this Activity. Other applications that want to handle notes can "register" themselves in 137 * Android by providing an intent filter that includes the category ALTERNATIVE and the 138 * mimeTYpe NotePad.Notes.CONTENT_TYPE. If they do this, the code in onCreateOptionsMenu() 139 * will add the Activity that contains the intent filter to its list of options. In effect, 140 * the menu will offer the user other applications that can handle notes. 141 * @param menu A Menu object, to which menu items should be added. 142 * @return True, always. The menu should be displayed. 143 */ 144 @Override onCreateOptionsMenu(Menu menu)145 public boolean onCreateOptionsMenu(Menu menu) { 146 // Inflate menu from XML resource 147 MenuInflater inflater = getMenuInflater(); 148 inflater.inflate(R.menu.list_options_menu, menu); 149 150 // Generate any additional actions that can be performed on the 151 // overall list. In a normal install, there are no additional 152 // actions found here, but this allows other applications to extend 153 // our menu with their own actions. 154 Intent intent = new Intent(null, getIntent().getData()); 155 intent.addCategory(Intent.CATEGORY_ALTERNATIVE); 156 menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0, 157 new ComponentName(this, NotesList.class), null, intent, 0, null); 158 159 return super.onCreateOptionsMenu(menu); 160 } 161 162 @Override onPrepareOptionsMenu(Menu menu)163 public boolean onPrepareOptionsMenu(Menu menu) { 164 super.onPrepareOptionsMenu(menu); 165 166 // The paste menu item is enabled if there is data on the clipboard. 167 ClipboardManager clipboard = (ClipboardManager) 168 getSystemService(Context.CLIPBOARD_SERVICE); 169 170 171 MenuItem mPasteItem = menu.findItem(R.id.menu_paste); 172 173 // If the clipboard contains an item, enables the Paste option on the menu. 174 if (clipboard.hasPrimaryClip()) { 175 mPasteItem.setEnabled(true); 176 } else { 177 // If the clipboard is empty, disables the menu's Paste option. 178 mPasteItem.setEnabled(false); 179 } 180 181 // Gets the number of notes currently being displayed. 182 final boolean haveItems = getListAdapter().getCount() > 0; 183 184 // If there are any notes in the list (which implies that one of 185 // them is selected), then we need to generate the actions that 186 // can be performed on the current selection. This will be a combination 187 // of our own specific actions along with any extensions that can be 188 // found. 189 if (haveItems) { 190 191 // This is the selected item. 192 Uri uri = ContentUris.withAppendedId(getIntent().getData(), getSelectedItemId()); 193 194 // Creates an array of Intents with one element. This will be used to send an Intent 195 // based on the selected menu item. 196 Intent[] specifics = new Intent[1]; 197 198 // Sets the Intent in the array to be an EDIT action on the URI of the selected note. 199 specifics[0] = new Intent(Intent.ACTION_EDIT, uri); 200 201 // Creates an array of menu items with one element. This will contain the EDIT option. 202 MenuItem[] items = new MenuItem[1]; 203 204 // Creates an Intent with no specific action, using the URI of the selected note. 205 Intent intent = new Intent(null, uri); 206 207 /* Adds the category ALTERNATIVE to the Intent, with the note ID URI as its 208 * data. This prepares the Intent as a place to group alternative options in the 209 * menu. 210 */ 211 intent.addCategory(Intent.CATEGORY_ALTERNATIVE); 212 213 /* 214 * Add alternatives to the menu 215 */ 216 menu.addIntentOptions( 217 Menu.CATEGORY_ALTERNATIVE, // Add the Intents as options in the alternatives group. 218 Menu.NONE, // A unique item ID is not required. 219 Menu.NONE, // The alternatives don't need to be in order. 220 null, // The caller's name is not excluded from the group. 221 specifics, // These specific options must appear first. 222 intent, // These Intent objects map to the options in specifics. 223 Menu.NONE, // No flags are required. 224 items // The menu items generated from the specifics-to- 225 // Intents mapping 226 ); 227 // If the Edit menu item exists, adds shortcuts for it. 228 if (items[0] != null) { 229 230 // Sets the Edit menu item shortcut to numeric "1", letter "e" 231 items[0].setShortcut('1', 'e'); 232 } 233 } else { 234 // If the list is empty, removes any existing alternative actions from the menu 235 menu.removeGroup(Menu.CATEGORY_ALTERNATIVE); 236 } 237 238 // Displays the menu 239 return true; 240 } 241 242 /** 243 * This method is called when the user selects an option from the menu, but no item 244 * in the list is selected. If the option was INSERT, then a new Intent is sent out with action 245 * ACTION_INSERT. The data from the incoming Intent is put into the new Intent. In effect, 246 * this triggers the NoteEditor activity in the NotePad application. 247 * 248 * If the item was not INSERT, then most likely it was an alternative option from another 249 * application. The parent method is called to process the item. 250 * @param item The menu item that was selected by the user 251 * @return True, if the INSERT menu item was selected; otherwise, the result of calling 252 * the parent method. 253 */ 254 @Override onOptionsItemSelected(MenuItem item)255 public boolean onOptionsItemSelected(MenuItem item) { 256 switch (item.getItemId()) { 257 case R.id.menu_add: 258 /* 259 * Launches a new Activity using an Intent. The intent filter for the Activity 260 * has to have action ACTION_INSERT. No category is set, so DEFAULT is assumed. 261 * In effect, this starts the NoteEditor Activity in NotePad. 262 */ 263 startActivity(new Intent(Intent.ACTION_INSERT, getIntent().getData())); 264 return true; 265 case R.id.menu_paste: 266 /* 267 * Launches a new Activity using an Intent. The intent filter for the Activity 268 * has to have action ACTION_PASTE. No category is set, so DEFAULT is assumed. 269 * In effect, this starts the NoteEditor Activity in NotePad. 270 */ 271 startActivity(new Intent(Intent.ACTION_PASTE, getIntent().getData())); 272 return true; 273 default: 274 return super.onOptionsItemSelected(item); 275 } 276 } 277 278 /** 279 * This method is called when the user context-clicks a note in the list. NotesList registers 280 * itself as the handler for context menus in its ListView (this is done in onCreate()). 281 * 282 * The only available options are COPY and DELETE. 283 * 284 * Context-click is equivalent to long-press. 285 * 286 * @param menu A ContexMenu object to which items should be added. 287 * @param view The View for which the context menu is being constructed. 288 * @param menuInfo Data associated with view. 289 * @throws ClassCastException 290 */ 291 @Override onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo)292 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { 293 294 // The data from the menu item. 295 AdapterView.AdapterContextMenuInfo info; 296 297 // Tries to get the position of the item in the ListView that was long-pressed. 298 try { 299 // Casts the incoming data object into the type for AdapterView objects. 300 info = (AdapterView.AdapterContextMenuInfo) menuInfo; 301 } catch (ClassCastException e) { 302 // If the menu object can't be cast, logs an error. 303 Log.e(TAG, "bad menuInfo", e); 304 return; 305 } 306 307 /* 308 * Gets the data associated with the item at the selected position. getItem() returns 309 * whatever the backing adapter of the ListView has associated with the item. In NotesList, 310 * the adapter associated all of the data for a note with its list item. As a result, 311 * getItem() returns that data as a Cursor. 312 */ 313 Cursor cursor = (Cursor) getListAdapter().getItem(info.position); 314 315 // If the cursor is empty, then for some reason the adapter can't get the data from the 316 // provider, so returns null to the caller. 317 if (cursor == null) { 318 // For some reason the requested item isn't available, do nothing 319 return; 320 } 321 322 // Inflate menu from XML resource 323 MenuInflater inflater = getMenuInflater(); 324 inflater.inflate(R.menu.list_context_menu, menu); 325 326 // Sets the menu header to be the title of the selected note. 327 menu.setHeaderTitle(cursor.getString(COLUMN_INDEX_TITLE)); 328 329 // Append to the 330 // menu items for any other activities that can do stuff with it 331 // as well. This does a query on the system for any activities that 332 // implement the ALTERNATIVE_ACTION for our data, adding a menu item 333 // for each one that is found. 334 Intent intent = new Intent(null, Uri.withAppendedPath(getIntent().getData(), 335 Integer.toString((int) info.id) )); 336 intent.addCategory(Intent.CATEGORY_ALTERNATIVE); 337 menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 0, 0, 338 new ComponentName(this, NotesList.class), null, intent, 0, null); 339 } 340 341 /** 342 * This method is called when the user selects an item from the context menu 343 * (see onCreateContextMenu()). The only menu items that are actually handled are DELETE and 344 * COPY. Anything else is an alternative option, for which default handling should be done. 345 * 346 * @param item The selected menu item 347 * @return True if the menu item was DELETE, and no default processing is need, otherwise false, 348 * which triggers the default handling of the item. 349 * @throws ClassCastException 350 */ 351 @Override onContextItemSelected(MenuItem item)352 public boolean onContextItemSelected(MenuItem item) { 353 // The data from the menu item. 354 AdapterView.AdapterContextMenuInfo info; 355 356 /* 357 * Gets the extra info from the menu item. When an note in the Notes list is long-pressed, a 358 * context menu appears. The menu items for the menu automatically get the data 359 * associated with the note that was long-pressed. The data comes from the provider that 360 * backs the list. 361 * 362 * The note's data is passed to the context menu creation routine in a ContextMenuInfo 363 * object. 364 * 365 * When one of the context menu items is clicked, the same data is passed, along with the 366 * note ID, to onContextItemSelected() via the item parameter. 367 */ 368 try { 369 // Casts the data object in the item into the type for AdapterView objects. 370 info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); 371 } catch (ClassCastException e) { 372 373 // If the object can't be cast, logs an error 374 Log.e(TAG, "bad menuInfo", e); 375 376 // Triggers default processing of the menu item. 377 return false; 378 } 379 // Appends the selected note's ID to the URI sent with the incoming Intent. 380 Uri noteUri = ContentUris.withAppendedId(getIntent().getData(), info.id); 381 382 /* 383 * Gets the menu item's ID and compares it to known actions. 384 */ 385 switch (item.getItemId()) { 386 case R.id.context_open: 387 // Launch activity to view/edit the currently selected item 388 startActivity(new Intent(Intent.ACTION_EDIT, noteUri)); 389 return true; 390 //BEGIN_INCLUDE(copy) 391 case R.id.context_copy: 392 // Gets a handle to the clipboard service. 393 ClipboardManager clipboard = (ClipboardManager) 394 getSystemService(Context.CLIPBOARD_SERVICE); 395 396 // Copies the notes URI to the clipboard. In effect, this copies the note itself 397 clipboard.setPrimaryClip(ClipData.newUri( // new clipboard item holding a URI 398 getContentResolver(), // resolver to retrieve URI info 399 "Note", // label for the clip 400 noteUri) // the URI 401 ); 402 403 // Returns to the caller and skips further processing. 404 return true; 405 //END_INCLUDE(copy) 406 case R.id.context_delete: 407 408 // Deletes the note from the provider by passing in a URI in note ID format. 409 // Please see the introductory note about performing provider operations on the 410 // UI thread. 411 getContentResolver().delete( 412 noteUri, // The URI of the provider 413 null, // No where clause is needed, since only a single note ID is being 414 // passed in. 415 null // No where clause is used, so no where arguments are needed. 416 ); 417 418 // Returns to the caller and skips further processing. 419 return true; 420 default: 421 return super.onContextItemSelected(item); 422 } 423 } 424 425 /** 426 * This method is called when the user clicks a note in the displayed list. 427 * 428 * This method handles incoming actions of either PICK (get data from the provider) or 429 * GET_CONTENT (get or create data). If the incoming action is EDIT, this method sends a 430 * new Intent to start NoteEditor. 431 * @param l The ListView that contains the clicked item 432 * @param v The View of the individual item 433 * @param position The position of v in the displayed list 434 * @param id The row ID of the clicked item 435 */ 436 @Override onListItemClick(ListView l, View v, int position, long id)437 protected void onListItemClick(ListView l, View v, int position, long id) { 438 439 // Constructs a new URI from the incoming URI and the row ID 440 Uri uri = ContentUris.withAppendedId(getIntent().getData(), id); 441 442 // Gets the action from the incoming Intent 443 String action = getIntent().getAction(); 444 445 // Handles requests for note data 446 if (Intent.ACTION_PICK.equals(action) || Intent.ACTION_GET_CONTENT.equals(action)) { 447 448 // Sets the result to return to the component that called this Activity. The 449 // result contains the new URI 450 setResult(RESULT_OK, new Intent().setData(uri)); 451 } else { 452 453 // Sends out an Intent to start an Activity that can handle ACTION_EDIT. The 454 // Intent's data is the note ID URI. The effect is to call NoteEdit. 455 startActivity(new Intent(Intent.ACTION_EDIT, uri)); 456 } 457 } 458 459 // LoaderManager callbacks 460 @Override onCreateLoader(int i, Bundle bundle)461 public Loader<Cursor> onCreateLoader(int i, Bundle bundle) { 462 return new CursorLoader( 463 this, 464 getIntent().getData(), // Use the default content URI for the provider. 465 PROJECTION, // Return the note ID and title for each note. 466 null, // No where clause, return all records. 467 null, // No where clause, therefore no where column values. 468 NotePad.Notes.DEFAULT_SORT_ORDER // Use the default sort order. 469 ); 470 } 471 472 @Override onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor)473 public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) { 474 mAdapter.changeCursor(cursor); 475 } 476 477 @Override onLoaderReset(Loader<Cursor> cursorLoader)478 public void onLoaderReset(Loader<Cursor> cursorLoader) { 479 // Since the Loader is reset, this removes the cursor reference from the adapter. 480 mAdapter.changeCursor(null); 481 } 482 } 483