ContactBrowseListFragment.java revision 682e152f65a14971d7df191ff849f9db9d50d617
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 (mSelectionRequired) {
439                mSelectionRequired = false;
440                notifyInvalidSelection();
441                return;
442            }
443
444            if (isSearchMode()) {
445                selectFirstFoundContactAfterDelay();
446                if (mListener != null) {
447                    mListener.onSelectionChange();
448                }
449                return;
450            }
451
452            saveSelectedUri(null);
453            selectDefaultContact();
454        }
455
456        mSelectionRequired = false;
457        mSelectionVerified = true;
458
459        if (mSelectionPersistenceRequested) {
460            saveSelectedUri(mSelectedContactUri);
461            mSelectionPersistenceRequested = false;
462        }
463
464        if (mSelectionToScreenRequested) {
465            requestSelectionToScreen();
466        }
467
468        getListView().invalidateViews();
469
470        if (mListener != null) {
471            mListener.onSelectionChange();
472        }
473    }
474
475    /**
476     * Automatically selects the first found contact in search mode.  The selection
477     * is updated after a delay to allow the user to type without to much UI churn
478     * and to save bandwidth on directory queries.
479     */
480    public void selectFirstFoundContactAfterDelay() {
481        Handler handler = getHandler();
482        handler.removeMessages(MESSAGE_AUTOSELECT_FIRST_FOUND_CONTACT);
483
484        String queryString = getQueryString();
485        if (queryString != null
486                && queryString.length() >= AUTOSELECT_FIRST_FOUND_CONTACT_MIN_QUERY_LENGTH) {
487            handler.sendEmptyMessageDelayed(MESSAGE_AUTOSELECT_FIRST_FOUND_CONTACT,
488                    DELAY_AUTOSELECT_FIRST_FOUND_CONTACT_MILLIS);
489        } else {
490            setSelectedContactUri(null, false, false, false, false);
491        }
492    }
493
494    protected void selectDefaultContact() {
495        Uri contactUri = null;
496        if (mLastSelectedPosition != -1) {
497            contactUri = getAdapter().getContactUri(mLastSelectedPosition);
498        }
499
500        if (contactUri == null) {
501            contactUri = getAdapter().getFirstContactUri();
502        }
503
504        setSelectedContactUri(contactUri, false, mSmoothScrollRequested, false, false);
505    }
506
507    protected void requestSelectionToScreen() {
508        int selectedPosition = getAdapter().getSelectedContactPosition();
509        if (selectedPosition != -1) {
510            AutoScrollListView listView = (AutoScrollListView)getListView();
511            listView.requestPositionToScreen(
512                    selectedPosition + listView.getHeaderViewsCount(), mSmoothScrollRequested);
513            mSelectionToScreenRequested = false;
514        }
515    }
516
517    @Override
518    public boolean isLoading() {
519        return mRefreshingContactUri || super.isLoading();
520    }
521
522    @Override
523    protected void startLoading() {
524        mStartedLoading = true;
525        mSelectionVerified = false;
526        super.startLoading();
527    }
528
529    public void reloadDataAndSetSelectedUri(Uri uri) {
530        setSelectedContactUri(uri, true, true, true, true);
531        reloadData();
532    }
533
534    @Override
535    public void reloadData() {
536        if (mStartedLoading) {
537            mSelectionVerified = false;
538            super.reloadData();
539        }
540    }
541
542    public void setOnContactListActionListener(OnContactBrowserActionListener listener) {
543        mListener = listener;
544    }
545
546    public void createNewContact() {
547        if (mListener != null) mListener.onCreateNewContactAction();
548    }
549
550    public void viewContact(Uri contactUri) {
551        setSelectedContactUri(contactUri, false, false, true, false);
552        if (mListener != null) { mListener.onViewContactAction(contactUri); }
553    }
554
555    public void editContact(Uri contactUri) {
556        if (mListener != null) mListener.onEditContactAction(contactUri);
557    }
558
559    public void deleteContact(Uri contactUri) {
560        if (mListener != null) mListener.onDeleteContactAction(contactUri);
561    }
562
563    public void addToFavorites(Uri contactUri) {
564        if (mListener != null) mListener.onAddToFavoritesAction(contactUri);
565    }
566
567    public void removeFromFavorites(Uri contactUri) {
568        if (mListener != null) mListener.onRemoveFromFavoritesAction(contactUri);
569    }
570
571    public void callContact(Uri contactUri) {
572        if (mListener != null) mListener.onCallContactAction(contactUri);
573    }
574
575    public void smsContact(Uri contactUri) {
576        if (mListener != null) mListener.onSmsContactAction(contactUri);
577    }
578
579    private void notifyInvalidSelection() {
580        if (mListener != null) mListener.onInvalidSelection();
581    }
582
583    @Override
584    protected void finish() {
585        super.finish();
586        if (mListener != null) mListener.onFinishAction();
587    }
588
589    private void saveSelectedUri(Uri contactUri) {
590        if (isSearchMode()) {
591            return;
592        }
593
594        ContactListFilter.storeToPreferences(mPrefs, mFilter);
595
596        Editor editor = mPrefs.edit();
597        if (contactUri == null) {
598            editor.remove(getPersistentSelectionKey());
599        } else {
600            editor.putString(getPersistentSelectionKey(), contactUri.toString());
601        }
602        editor.apply();
603    }
604
605    private void restoreSelectedUri(boolean willReloadData) {
606        // The meaning of mSelectionRequired is that we need to show some
607        // selection other than the previous selection saved in shared preferences
608        if (mSelectionRequired) {
609            return;
610        }
611
612        String selectedUri = mPrefs.getString(getPersistentSelectionKey(), null);
613        if (selectedUri == null) {
614            setSelectedContactUri(null, false, false, false, willReloadData);
615        } else {
616            setSelectedContactUri(Uri.parse(selectedUri), false, false, false, willReloadData);
617        }
618    }
619
620    private void saveFilter() {
621        ContactListFilter.storeToPreferences(mPrefs, mFilter);
622    }
623
624    private void restoreFilter() {
625        mFilter = ContactListFilter.restoreFromPreferences(mPrefs);
626        if (mFilter == null) {
627            mFilter = new ContactListFilter(ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS);
628        }
629    }
630
631    private String getPersistentSelectionKey() {
632        if (mFilter == null) {
633            return mPersistentSelectionPrefix;
634        } else {
635            return mPersistentSelectionPrefix + "-" + mFilter.getId();
636        }
637    }
638
639    public boolean isOptionsMenuChanged() {
640        // This fragment does not have an option menu of its own
641        return false;
642    }
643}
644