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