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