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