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