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