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