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