• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 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 package com.android.contacts.list;
17 
18 import android.app.Activity;
19 import android.content.ContentResolver;
20 import android.content.ContentUris;
21 import android.content.Loader;
22 import android.content.SharedPreferences;
23 import android.content.SharedPreferences.Editor;
24 import android.database.Cursor;
25 import android.net.Uri;
26 import android.os.AsyncTask;
27 import android.os.Bundle;
28 import android.os.Handler;
29 import android.os.Message;
30 import android.preference.PreferenceManager;
31 import android.provider.ContactsContract;
32 import android.provider.ContactsContract.Contacts;
33 import android.provider.ContactsContract.Directory;
34 import android.text.TextUtils;
35 import android.util.Log;
36 
37 import com.android.common.widget.CompositeCursorAdapter.Partition;
38 import com.android.contacts.common.list.AutoScrollListView;
39 import com.android.contacts.common.list.ContactEntryListFragment;
40 import com.android.contacts.common.list.ContactListAdapter;
41 import com.android.contacts.common.list.ContactListFilter;
42 import com.android.contacts.common.list.DirectoryPartition;
43 import com.android.contacts.common.util.ContactLoaderUtils;
44 
45 import java.util.List;
46 
47 /**
48  * Fragment containing a contact list used for browsing (as compared to
49  * picking a contact with one of the PICK intents).
50  */
51 public abstract class ContactBrowseListFragment extends
52         ContactEntryListFragment<ContactListAdapter> {
53 
54     private static final String TAG = "ContactList";
55 
56     private static final String KEY_SELECTED_URI = "selectedUri";
57     private static final String KEY_SELECTION_VERIFIED = "selectionVerified";
58     private static final String KEY_FILTER = "filter";
59     private static final String KEY_LAST_SELECTED_POSITION = "lastSelected";
60 
61     private static final String PERSISTENT_SELECTION_PREFIX = "defaultContactBrowserSelection";
62 
63     /**
64      * The id for a delayed message that triggers automatic selection of the first
65      * found contact in search mode.
66      */
67     private static final int MESSAGE_AUTOSELECT_FIRST_FOUND_CONTACT = 1;
68 
69     /**
70      * The delay that is used for automatically selecting the first found contact.
71      */
72     private static final int DELAY_AUTOSELECT_FIRST_FOUND_CONTACT_MILLIS = 500;
73 
74     /**
75      * The minimum number of characters in the search query that is required
76      * before we automatically select the first found contact.
77      */
78     private static final int AUTOSELECT_FIRST_FOUND_CONTACT_MIN_QUERY_LENGTH = 2;
79 
80     private SharedPreferences mPrefs;
81     private Handler mHandler;
82 
83     private boolean mStartedLoading;
84     private boolean mSelectionRequired;
85     private boolean mSelectionToScreenRequested;
86     private boolean mSmoothScrollRequested;
87     private boolean mSelectionPersistenceRequested;
88     private Uri mSelectedContactUri;
89     private long mSelectedContactDirectoryId;
90     private String mSelectedContactLookupKey;
91     private long mSelectedContactId;
92     private boolean mSelectionVerified;
93     private int mLastSelectedPosition = -1;
94     private boolean mRefreshingContactUri;
95     private ContactListFilter mFilter;
96     private String mPersistentSelectionPrefix = PERSISTENT_SELECTION_PREFIX;
97 
98     protected OnContactBrowserActionListener mListener;
99     private ContactLookupTask mContactLookupTask;
100 
101     private final class ContactLookupTask extends AsyncTask<Void, Void, Uri> {
102 
103         private final Uri mUri;
104         private boolean mIsCancelled;
105 
ContactLookupTask(Uri uri)106         public ContactLookupTask(Uri uri) {
107             mUri = uri;
108         }
109 
110         @Override
doInBackground(Void... args)111         protected Uri doInBackground(Void... args) {
112             Cursor cursor = null;
113             try {
114                 final ContentResolver resolver = getContext().getContentResolver();
115                 final Uri uriCurrentFormat = ContactLoaderUtils.ensureIsContactUri(resolver, mUri);
116                 cursor = resolver.query(uriCurrentFormat,
117                         new String[] { Contacts._ID, Contacts.LOOKUP_KEY }, null, null, null);
118 
119                 if (cursor != null && cursor.moveToFirst()) {
120                     final long contactId = cursor.getLong(0);
121                     final String lookupKey = cursor.getString(1);
122                     if (contactId != 0 && !TextUtils.isEmpty(lookupKey)) {
123                         return Contacts.getLookupUri(contactId, lookupKey);
124                     }
125                 }
126 
127                 Log.e(TAG, "Error: No contact ID or lookup key for contact " + mUri);
128                 return null;
129             } catch (Exception e) {
130                 Log.e(TAG, "Error loading the contact: " + mUri, e);
131                 return null;
132             } finally {
133                 if (cursor != null) {
134                     cursor.close();
135                 }
136             }
137         }
138 
cancel()139         public void cancel() {
140             super.cancel(true);
141             // Use a flag to keep track of whether the {@link AsyncTask} was cancelled or not in
142             // order to ensure onPostExecute() is not executed after the cancel request. The flag is
143             // necessary because {@link AsyncTask} still calls onPostExecute() if the cancel request
144             // came after the worker thread was finished.
145             mIsCancelled = true;
146         }
147 
148         @Override
onPostExecute(Uri uri)149         protected void onPostExecute(Uri uri) {
150             // Make sure the {@link Fragment} is at least still attached to the {@link Activity}
151             // before continuing. Null URIs should still be allowed so that the list can be
152             // refreshed and a default contact can be selected (i.e. the case of deleted
153             // contacts).
154             if (mIsCancelled || !isAdded()) {
155                 return;
156             }
157             onContactUriQueryFinished(uri);
158         }
159     }
160 
161     private boolean mDelaySelection;
162 
getHandler()163     private Handler getHandler() {
164         if (mHandler == null) {
165             mHandler = new Handler() {
166                 @Override
167                 public void handleMessage(Message msg) {
168                     switch (msg.what) {
169                         case MESSAGE_AUTOSELECT_FIRST_FOUND_CONTACT:
170                             selectDefaultContact();
171                             break;
172                     }
173                 }
174             };
175         }
176         return mHandler;
177     }
178 
179     @Override
onAttach(Activity activity)180     public void onAttach(Activity activity) {
181         super.onAttach(activity);
182         mPrefs = PreferenceManager.getDefaultSharedPreferences(activity);
183         restoreFilter();
184         restoreSelectedUri(false);
185     }
186 
187     @Override
setSearchMode(boolean flag)188     protected void setSearchMode(boolean flag) {
189         if (isSearchMode() != flag) {
190             if (!flag) {
191                 restoreSelectedUri(true);
192             }
193             super.setSearchMode(flag);
194         }
195     }
196 
setFilter(ContactListFilter filter)197     public void setFilter(ContactListFilter filter) {
198         setFilter(filter, true);
199     }
200 
setFilter(ContactListFilter filter, boolean restoreSelectedUri)201     public void setFilter(ContactListFilter filter, boolean restoreSelectedUri) {
202         if (mFilter == null && filter == null) {
203             return;
204         }
205 
206         if (mFilter != null && mFilter.equals(filter)) {
207             return;
208         }
209 
210         Log.v(TAG, "New filter: " + filter);
211 
212         mFilter = filter;
213         mLastSelectedPosition = -1;
214         saveFilter();
215         if (restoreSelectedUri) {
216             mSelectedContactUri = null;
217             restoreSelectedUri(true);
218         }
219         reloadData();
220     }
221 
getFilter()222     public ContactListFilter getFilter() {
223         return mFilter;
224     }
225 
226     @Override
restoreSavedState(Bundle savedState)227     public void restoreSavedState(Bundle savedState) {
228         super.restoreSavedState(savedState);
229 
230         if (savedState == null) {
231             return;
232         }
233 
234         mFilter = savedState.getParcelable(KEY_FILTER);
235         mSelectedContactUri = savedState.getParcelable(KEY_SELECTED_URI);
236         mSelectionVerified = savedState.getBoolean(KEY_SELECTION_VERIFIED);
237         mLastSelectedPosition = savedState.getInt(KEY_LAST_SELECTED_POSITION);
238         parseSelectedContactUri();
239     }
240 
241     @Override
onSaveInstanceState(Bundle outState)242     public void onSaveInstanceState(Bundle outState) {
243         super.onSaveInstanceState(outState);
244         outState.putParcelable(KEY_FILTER, mFilter);
245         outState.putParcelable(KEY_SELECTED_URI, mSelectedContactUri);
246         outState.putBoolean(KEY_SELECTION_VERIFIED, mSelectionVerified);
247         outState.putInt(KEY_LAST_SELECTED_POSITION, mLastSelectedPosition);
248     }
249 
refreshSelectedContactUri()250     protected void refreshSelectedContactUri() {
251         if (mContactLookupTask != null) {
252             mContactLookupTask.cancel();
253         }
254 
255         if (!isSelectionVisible()) {
256             return;
257         }
258 
259         mRefreshingContactUri = true;
260 
261         if (mSelectedContactUri == null) {
262             onContactUriQueryFinished(null);
263             return;
264         }
265 
266         if (mSelectedContactDirectoryId != Directory.DEFAULT
267                 && mSelectedContactDirectoryId != Directory.LOCAL_INVISIBLE) {
268             onContactUriQueryFinished(mSelectedContactUri);
269         } else {
270             mContactLookupTask = new ContactLookupTask(mSelectedContactUri);
271             mContactLookupTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[])null);
272         }
273     }
274 
onContactUriQueryFinished(Uri uri)275     protected void onContactUriQueryFinished(Uri uri) {
276         mRefreshingContactUri = false;
277         mSelectedContactUri = uri;
278         parseSelectedContactUri();
279         checkSelection();
280     }
281 
getSelectedContactUri()282     public Uri getSelectedContactUri() {
283         return mSelectedContactUri;
284     }
285 
286     /**
287      * Sets the new selection for the list.
288      */
setSelectedContactUri(Uri uri)289     public void setSelectedContactUri(Uri uri) {
290         setSelectedContactUri(uri, true, false /* no smooth scroll */, true, false);
291     }
292 
293     @Override
setQueryString(String queryString, boolean delaySelection)294     public void setQueryString(String queryString, boolean delaySelection) {
295         mDelaySelection = delaySelection;
296         super.setQueryString(queryString, delaySelection);
297     }
298 
299     /**
300      * Sets whether or not a contact selection must be made.
301      * @param required if true, we need to check if the selection is present in
302      *            the list and if not notify the listener so that it can load a
303      *            different list.
304      * TODO: Figure out how to reconcile this with {@link #setSelectedContactUri},
305      * without causing unnecessary loading of the list if the selected contact URI is
306      * the same as before.
307      */
setSelectionRequired(boolean required)308     public void setSelectionRequired(boolean required) {
309         mSelectionRequired = required;
310     }
311 
312     /**
313      * Sets the new contact selection.
314      *
315      * @param uri the new selection
316      * @param required if true, we need to check if the selection is present in
317      *            the list and if not notify the listener so that it can load a
318      *            different list
319      * @param smoothScroll if true, the UI will roll smoothly to the new
320      *            selection
321      * @param persistent if true, the selection will be stored in shared
322      *            preferences.
323      * @param willReloadData if true, the selection will be remembered but not
324      *            actually shown, because we are expecting that the data will be
325      *            reloaded momentarily
326      */
setSelectedContactUri(Uri uri, boolean required, boolean smoothScroll, boolean persistent, boolean willReloadData)327     private void setSelectedContactUri(Uri uri, boolean required, boolean smoothScroll,
328             boolean persistent, boolean willReloadData) {
329         mSmoothScrollRequested = smoothScroll;
330         mSelectionToScreenRequested = true;
331 
332         if ((mSelectedContactUri == null && uri != null)
333                 || (mSelectedContactUri != null && !mSelectedContactUri.equals(uri))) {
334             mSelectionVerified = false;
335             mSelectionRequired = required;
336             mSelectionPersistenceRequested = persistent;
337             mSelectedContactUri = uri;
338             parseSelectedContactUri();
339 
340             if (!willReloadData) {
341                 // Configure the adapter to show the selection based on the
342                 // lookup key extracted from the URI
343                 ContactListAdapter adapter = getAdapter();
344                 if (adapter != null) {
345                     adapter.setSelectedContact(mSelectedContactDirectoryId,
346                             mSelectedContactLookupKey, mSelectedContactId);
347                     getListView().invalidateViews();
348                 }
349             }
350 
351             // Also, launch a loader to pick up a new lookup URI in case it has changed
352             refreshSelectedContactUri();
353         }
354     }
355 
parseSelectedContactUri()356     private void parseSelectedContactUri() {
357         if (mSelectedContactUri != null) {
358             String directoryParam =
359                     mSelectedContactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
360             mSelectedContactDirectoryId = TextUtils.isEmpty(directoryParam) ? Directory.DEFAULT
361                     : Long.parseLong(directoryParam);
362             if (mSelectedContactUri.toString().startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) {
363                 List<String> pathSegments = mSelectedContactUri.getPathSegments();
364                 mSelectedContactLookupKey = Uri.encode(pathSegments.get(2));
365                 if (pathSegments.size() == 4) {
366                     mSelectedContactId = ContentUris.parseId(mSelectedContactUri);
367                 }
368             } else if (mSelectedContactUri.toString().startsWith(Contacts.CONTENT_URI.toString()) &&
369                     mSelectedContactUri.getPathSegments().size() >= 2) {
370                 mSelectedContactLookupKey = null;
371                 mSelectedContactId = ContentUris.parseId(mSelectedContactUri);
372             } else {
373                 Log.e(TAG, "Unsupported contact URI: " + mSelectedContactUri);
374                 mSelectedContactLookupKey = null;
375                 mSelectedContactId = 0;
376             }
377 
378         } else {
379             mSelectedContactDirectoryId = Directory.DEFAULT;
380             mSelectedContactLookupKey = null;
381             mSelectedContactId = 0;
382         }
383     }
384 
385     @Override
configureAdapter()386     protected void configureAdapter() {
387         super.configureAdapter();
388 
389         ContactListAdapter adapter = getAdapter();
390         if (adapter == null) {
391             return;
392         }
393 
394         boolean searchMode = isSearchMode();
395         if (!searchMode && mFilter != null) {
396             adapter.setFilter(mFilter);
397             if (mSelectionRequired
398                     || mFilter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) {
399                 adapter.setSelectedContact(
400                         mSelectedContactDirectoryId, mSelectedContactLookupKey, mSelectedContactId);
401             }
402         }
403 
404         // Display the user's profile if not in search mode
405         adapter.setIncludeProfile(!searchMode);
406     }
407 
408     @Override
onLoadFinished(Loader<Cursor> loader, Cursor data)409     public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
410         super.onLoadFinished(loader, data);
411         mSelectionVerified = false;
412 
413         // Refresh the currently selected lookup in case it changed while we were sleeping
414         refreshSelectedContactUri();
415     }
416 
417     @Override
onLoaderReset(Loader<Cursor> loader)418     public void onLoaderReset(Loader<Cursor> loader) {
419     }
420 
checkSelection()421     private void checkSelection() {
422         if (mSelectionVerified) {
423             return;
424         }
425 
426         if (mRefreshingContactUri) {
427             return;
428         }
429 
430         if (isLoadingDirectoryList()) {
431             return;
432         }
433 
434         ContactListAdapter adapter = getAdapter();
435         if (adapter == null) {
436             return;
437         }
438 
439         boolean directoryLoading = true;
440         int count = adapter.getPartitionCount();
441         for (int i = 0; i < count; i++) {
442             Partition partition = adapter.getPartition(i);
443             if (partition instanceof DirectoryPartition) {
444                 DirectoryPartition directory = (DirectoryPartition) partition;
445                 if (directory.getDirectoryId() == mSelectedContactDirectoryId) {
446                     directoryLoading = directory.isLoading();
447                     break;
448                 }
449             }
450         }
451 
452         if (directoryLoading) {
453             return;
454         }
455 
456         adapter.setSelectedContact(
457                 mSelectedContactDirectoryId, mSelectedContactLookupKey, mSelectedContactId);
458 
459         final int selectedPosition = adapter.getSelectedContactPosition();
460         if (selectedPosition != -1) {
461             mLastSelectedPosition = selectedPosition;
462         } else {
463             if (isSearchMode()) {
464                 if (mDelaySelection) {
465                     selectFirstFoundContactAfterDelay();
466                     if (mListener != null) {
467                         mListener.onSelectionChange();
468                     }
469                     return;
470                 }
471             } else if (mSelectionRequired) {
472                 // A specific contact was requested, but it's not in the loaded list.
473 
474                 // Try reconfiguring and reloading the list that will hopefully contain
475                 // the requested contact. Only take one attempt to avoid an infinite loop
476                 // in case the contact cannot be found at all.
477                 mSelectionRequired = false;
478 
479                 // If we were looking at a different specific contact, just reload
480                 // FILTER_TYPE_ALL_ACCOUNTS is needed for the case where a new contact is added
481                 // on a tablet and the loader is returning a stale list.  In this case, the contact
482                 // will not be found until the next load. b/7621855 This will only fix the most
483                 // common case where all accounts are shown. It will not fix the one account case.
484                 // TODO: we may want to add more FILTER_TYPEs or relax this check to fix all other
485                 // FILTER_TYPE cases.
486                 if (mFilter != null
487                         && (mFilter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT
488                         || mFilter.filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS)) {
489                     reloadData();
490                 } else {
491                     // Otherwise, call the listener, which will adjust the filter.
492                     notifyInvalidSelection();
493                 }
494                 return;
495             } else if (mFilter != null
496                     && mFilter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) {
497                 // If we were trying to load a specific contact, but that contact no longer
498                 // exists, call the listener, which will adjust the filter.
499                 notifyInvalidSelection();
500                 return;
501             }
502 
503             saveSelectedUri(null);
504             selectDefaultContact();
505         }
506 
507         mSelectionRequired = false;
508         mSelectionVerified = true;
509 
510         if (mSelectionPersistenceRequested) {
511             saveSelectedUri(mSelectedContactUri);
512             mSelectionPersistenceRequested = false;
513         }
514 
515         if (mSelectionToScreenRequested) {
516             requestSelectionToScreen(selectedPosition);
517         }
518 
519         getListView().invalidateViews();
520 
521         if (mListener != null) {
522             mListener.onSelectionChange();
523         }
524     }
525 
526     /**
527      * Automatically selects the first found contact in search mode.  The selection
528      * is updated after a delay to allow the user to type without to much UI churn
529      * and to save bandwidth on directory queries.
530      */
selectFirstFoundContactAfterDelay()531     public void selectFirstFoundContactAfterDelay() {
532         Handler handler = getHandler();
533         handler.removeMessages(MESSAGE_AUTOSELECT_FIRST_FOUND_CONTACT);
534 
535         String queryString = getQueryString();
536         if (queryString != null
537                 && queryString.length() >= AUTOSELECT_FIRST_FOUND_CONTACT_MIN_QUERY_LENGTH) {
538             handler.sendEmptyMessageDelayed(MESSAGE_AUTOSELECT_FIRST_FOUND_CONTACT,
539                     DELAY_AUTOSELECT_FIRST_FOUND_CONTACT_MILLIS);
540         } else {
541             setSelectedContactUri(null, false, false, false, false);
542         }
543     }
544 
selectDefaultContact()545     protected void selectDefaultContact() {
546         Uri contactUri = null;
547         ContactListAdapter adapter = getAdapter();
548         if (mLastSelectedPosition != -1) {
549             int count = adapter.getCount();
550             int pos = mLastSelectedPosition;
551             if (pos >= count && count > 0) {
552                 pos = count - 1;
553             }
554             contactUri = adapter.getContactUri(pos);
555         }
556 
557         if (contactUri == null) {
558             contactUri = adapter.getFirstContactUri();
559         }
560 
561         setSelectedContactUri(contactUri, false, mSmoothScrollRequested, false, false);
562     }
563 
requestSelectionToScreen(int selectedPosition)564     protected void requestSelectionToScreen(int selectedPosition) {
565         if (selectedPosition != -1) {
566             AutoScrollListView listView = (AutoScrollListView)getListView();
567             listView.requestPositionToScreen(
568                     selectedPosition + listView.getHeaderViewsCount(), mSmoothScrollRequested);
569             mSelectionToScreenRequested = false;
570         }
571     }
572 
573     @Override
isLoading()574     public boolean isLoading() {
575         return mRefreshingContactUri || super.isLoading();
576     }
577 
578     @Override
startLoading()579     protected void startLoading() {
580         mStartedLoading = true;
581         mSelectionVerified = false;
582         super.startLoading();
583     }
584 
reloadDataAndSetSelectedUri(Uri uri)585     public void reloadDataAndSetSelectedUri(Uri uri) {
586         setSelectedContactUri(uri, true, true, true, true);
587         reloadData();
588     }
589 
590     @Override
reloadData()591     public void reloadData() {
592         if (mStartedLoading) {
593             mSelectionVerified = false;
594             mLastSelectedPosition = -1;
595             super.reloadData();
596         }
597     }
598 
setOnContactListActionListener(OnContactBrowserActionListener listener)599     public void setOnContactListActionListener(OnContactBrowserActionListener listener) {
600         mListener = listener;
601     }
602 
viewContact(Uri contactUri)603     public void viewContact(Uri contactUri) {
604         setSelectedContactUri(contactUri, false, false, true, false);
605         if (mListener != null) mListener.onViewContactAction(contactUri);
606     }
607 
deleteContact(Uri contactUri)608     public void deleteContact(Uri contactUri) {
609         if (mListener != null) mListener.onDeleteContactAction(contactUri);
610     }
611 
notifyInvalidSelection()612     private void notifyInvalidSelection() {
613         if (mListener != null) mListener.onInvalidSelection();
614     }
615 
616     @Override
finish()617     protected void finish() {
618         super.finish();
619         if (mListener != null) mListener.onFinishAction();
620     }
621 
saveSelectedUri(Uri contactUri)622     private void saveSelectedUri(Uri contactUri) {
623         if (isSearchMode()) {
624             return;
625         }
626 
627         ContactListFilter.storeToPreferences(mPrefs, mFilter);
628 
629         Editor editor = mPrefs.edit();
630         if (contactUri == null) {
631             editor.remove(getPersistentSelectionKey());
632         } else {
633             editor.putString(getPersistentSelectionKey(), contactUri.toString());
634         }
635         editor.apply();
636     }
637 
restoreSelectedUri(boolean willReloadData)638     private void restoreSelectedUri(boolean willReloadData) {
639         // The meaning of mSelectionRequired is that we need to show some
640         // selection other than the previous selection saved in shared preferences
641         if (mSelectionRequired) {
642             return;
643         }
644 
645         String selectedUri = mPrefs.getString(getPersistentSelectionKey(), null);
646         if (selectedUri == null) {
647             setSelectedContactUri(null, false, false, false, willReloadData);
648         } else {
649             setSelectedContactUri(Uri.parse(selectedUri), false, false, false, willReloadData);
650         }
651     }
652 
saveFilter()653     private void saveFilter() {
654         ContactListFilter.storeToPreferences(mPrefs, mFilter);
655     }
656 
restoreFilter()657     private void restoreFilter() {
658         mFilter = ContactListFilter.restoreDefaultPreferences(mPrefs);
659     }
660 
getPersistentSelectionKey()661     private String getPersistentSelectionKey() {
662         if (mFilter == null) {
663             return mPersistentSelectionPrefix;
664         } else {
665             return mPersistentSelectionPrefix + "-" + mFilter.getId();
666         }
667     }
668 
isOptionsMenuChanged()669     public boolean isOptionsMenuChanged() {
670         // This fragment does not have an option menu of its own
671         return false;
672     }
673 }
674