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