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