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 android.app.Activity;
19import android.content.ContentResolver;
20import android.content.ContentUris;
21import android.content.Loader;
22import android.content.SharedPreferences;
23import android.content.SharedPreferences.Editor;
24import android.database.Cursor;
25import android.net.Uri;
26import android.os.AsyncTask;
27import android.os.Bundle;
28import android.os.Handler;
29import android.os.Message;
30import android.preference.PreferenceManager;
31import android.provider.ContactsContract;
32import android.provider.ContactsContract.Contacts;
33import android.provider.ContactsContract.Directory;
34import android.text.TextUtils;
35import android.util.Log;
36
37import com.android.common.widget.CompositeCursorAdapter.Partition;
38import com.android.contacts.R;
39import com.android.contacts.util.ContactLoaderUtils;
40import com.android.contacts.widget.AutoScrollListView;
41
42import java.util.List;
43
44/**
45 * Fragment containing a contact list used for browsing (as compared to
46 * picking a contact with one of the PICK intents).
47 */
48public abstract class ContactBrowseListFragment extends
49        ContactEntryListFragment<ContactListAdapter> {
50
51    private static final String TAG = "ContactList";
52
53    private static final String KEY_SELECTED_URI = "selectedUri";
54    private static final String KEY_SELECTION_VERIFIED = "selectionVerified";
55    private static final String KEY_FILTER = "filter";
56    private static final String KEY_LAST_SELECTED_POSITION = "lastSelected";
57
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 int mLastSelectedPosition = -1;
91    private boolean mRefreshingContactUri;
92    private ContactListFilter mFilter;
93    private String mPersistentSelectionPrefix = PERSISTENT_SELECTION_PREFIX;
94
95    protected OnContactBrowserActionListener mListener;
96    private ContactLookupTask mContactLookupTask;
97
98    private final class ContactLookupTask extends AsyncTask<Void, Void, Uri> {
99
100        private final Uri mUri;
101        private boolean mIsCancelled;
102
103        public ContactLookupTask(Uri uri) {
104            mUri = uri;
105        }
106
107        @Override
108        protected Uri doInBackground(Void... args) {
109            Cursor cursor = null;
110            try {
111                final ContentResolver resolver = getContext().getContentResolver();
112                final Uri uriCurrentFormat = ContactLoaderUtils.ensureIsContactUri(resolver, mUri);
113                cursor = resolver.query(uriCurrentFormat,
114                        new String[] { Contacts._ID, Contacts.LOOKUP_KEY }, null, null, null);
115
116                if (cursor != null && cursor.moveToFirst()) {
117                    final long contactId = cursor.getLong(0);
118                    final String lookupKey = cursor.getString(1);
119                    if (contactId != 0 && !TextUtils.isEmpty(lookupKey)) {
120                        return Contacts.getLookupUri(contactId, lookupKey);
121                    }
122                }
123
124                Log.e(TAG, "Error: No contact ID or lookup key for contact " + mUri);
125                return null;
126            } finally {
127                if (cursor != null) {
128                    cursor.close();
129                }
130            }
131        }
132
133        public void cancel() {
134            super.cancel(true);
135            // Use a flag to keep track of whether the {@link AsyncTask} was cancelled or not in
136            // order to ensure onPostExecute() is not executed after the cancel request. The flag is
137            // necessary because {@link AsyncTask} still calls onPostExecute() if the cancel request
138            // came after the worker thread was finished.
139            mIsCancelled = true;
140        }
141
142        @Override
143        protected void onPostExecute(Uri uri) {
144            // Make sure the {@link Fragment} is at least still attached to the {@link Activity}
145            // before continuing. Null URIs should still be allowed so that the list can be
146            // refreshed and a default contact can be selected (i.e. the case of deleted
147            // contacts).
148            if (mIsCancelled || !isAdded()) {
149                return;
150            }
151            onContactUriQueryFinished(uri);
152        }
153    }
154
155    private boolean mDelaySelection;
156
157    private Handler getHandler() {
158        if (mHandler == null) {
159            mHandler = new Handler() {
160                @Override
161                public void handleMessage(Message msg) {
162                    switch (msg.what) {
163                        case MESSAGE_AUTOSELECT_FIRST_FOUND_CONTACT:
164                            selectDefaultContact();
165                            break;
166                    }
167                }
168            };
169        }
170        return mHandler;
171    }
172
173    @Override
174    public void onAttach(Activity activity) {
175        super.onAttach(activity);
176        mPrefs = PreferenceManager.getDefaultSharedPreferences(activity);
177        restoreFilter();
178        restoreSelectedUri(false);
179    }
180
181    @Override
182    protected void setSearchMode(boolean flag) {
183        if (isSearchMode() != flag) {
184            if (!flag) {
185                restoreSelectedUri(true);
186            }
187            super.setSearchMode(flag);
188        }
189    }
190
191    public void setFilter(ContactListFilter filter) {
192        setFilter(filter, true);
193    }
194
195    public void setFilter(ContactListFilter filter, boolean restoreSelectedUri) {
196        if (mFilter == null && filter == null) {
197            return;
198        }
199
200        if (mFilter != null && mFilter.equals(filter)) {
201            return;
202        }
203
204        Log.v(TAG, "New filter: " + filter);
205
206        mFilter = filter;
207        mLastSelectedPosition = -1;
208        saveFilter();
209        if (restoreSelectedUri) {
210            mSelectedContactUri = null;
211            restoreSelectedUri(true);
212        }
213        reloadData();
214    }
215
216    public ContactListFilter getFilter() {
217        return mFilter;
218    }
219
220    @Override
221    public void restoreSavedState(Bundle savedState) {
222        super.restoreSavedState(savedState);
223
224        if (savedState == null) {
225            return;
226        }
227
228        mFilter = savedState.getParcelable(KEY_FILTER);
229        mSelectedContactUri = savedState.getParcelable(KEY_SELECTED_URI);
230        mSelectionVerified = savedState.getBoolean(KEY_SELECTION_VERIFIED);
231        mLastSelectedPosition = savedState.getInt(KEY_LAST_SELECTED_POSITION);
232        parseSelectedContactUri();
233    }
234
235    @Override
236    public void onSaveInstanceState(Bundle outState) {
237        super.onSaveInstanceState(outState);
238        outState.putParcelable(KEY_FILTER, mFilter);
239        outState.putParcelable(KEY_SELECTED_URI, mSelectedContactUri);
240        outState.putBoolean(KEY_SELECTION_VERIFIED, mSelectionVerified);
241        outState.putInt(KEY_LAST_SELECTED_POSITION, mLastSelectedPosition);
242    }
243
244    protected void refreshSelectedContactUri() {
245        if (mContactLookupTask != null) {
246            mContactLookupTask.cancel();
247        }
248
249        if (!isSelectionVisible()) {
250            return;
251        }
252
253        mRefreshingContactUri = true;
254
255        if (mSelectedContactUri == null) {
256            onContactUriQueryFinished(null);
257            return;
258        }
259
260        if (mSelectedContactDirectoryId != Directory.DEFAULT
261                && mSelectedContactDirectoryId != Directory.LOCAL_INVISIBLE) {
262            onContactUriQueryFinished(mSelectedContactUri);
263        } else {
264            mContactLookupTask = new ContactLookupTask(mSelectedContactUri);
265            mContactLookupTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[])null);
266        }
267    }
268
269    protected void onContactUriQueryFinished(Uri uri) {
270        mRefreshingContactUri = false;
271        mSelectedContactUri = uri;
272        parseSelectedContactUri();
273        checkSelection();
274    }
275
276    @Override
277    protected void prepareEmptyView() {
278        if (isSearchMode()) {
279            return;
280        } else if (isSyncActive()) {
281            if (hasIccCard()) {
282                setEmptyText(R.string.noContactsHelpTextWithSync);
283            } else {
284                setEmptyText(R.string.noContactsNoSimHelpTextWithSync);
285            }
286        } else {
287            if (hasIccCard()) {
288                setEmptyText(R.string.noContactsHelpText);
289            } else {
290                setEmptyText(R.string.noContactsNoSimHelpText);
291            }
292        }
293    }
294
295    public Uri getSelectedContactUri() {
296        return mSelectedContactUri;
297    }
298
299    /**
300     * Sets the new selection for the list.
301     */
302    public void setSelectedContactUri(Uri uri) {
303        setSelectedContactUri(uri, true, false /* no smooth scroll */, true, false);
304    }
305
306    @Override
307    public void setQueryString(String queryString, boolean delaySelection) {
308        mDelaySelection = delaySelection;
309        super.setQueryString(queryString, delaySelection);
310    }
311
312    /**
313     * Sets whether or not a contact selection must be made.
314     * @param required if true, we need to check if the selection is present in
315     *            the list and if not notify the listener so that it can load a
316     *            different list.
317     * TODO: Figure out how to reconcile this with {@link #setSelectedContactUri},
318     * without causing unnecessary loading of the list if the selected contact URI is
319     * the same as before.
320     */
321    public void setSelectionRequired(boolean required) {
322        mSelectionRequired = required;
323    }
324
325    /**
326     * Sets the new contact selection.
327     *
328     * @param uri the new selection
329     * @param required if true, we need to check if the selection is present in
330     *            the list and if not notify the listener so that it can load a
331     *            different list
332     * @param smoothScroll if true, the UI will roll smoothly to the new
333     *            selection
334     * @param persistent if true, the selection will be stored in shared
335     *            preferences.
336     * @param willReloadData if true, the selection will be remembered but not
337     *            actually shown, because we are expecting that the data will be
338     *            reloaded momentarily
339     */
340    private void setSelectedContactUri(Uri uri, boolean required, boolean smoothScroll,
341            boolean persistent, boolean willReloadData) {
342        mSmoothScrollRequested = smoothScroll;
343        mSelectionToScreenRequested = true;
344
345        if ((mSelectedContactUri == null && uri != null)
346                || (mSelectedContactUri != null && !mSelectedContactUri.equals(uri))) {
347            mSelectionVerified = false;
348            mSelectionRequired = required;
349            mSelectionPersistenceRequested = persistent;
350            mSelectedContactUri = uri;
351            parseSelectedContactUri();
352
353            if (!willReloadData) {
354                // Configure the adapter to show the selection based on the
355                // lookup key extracted from the URI
356                ContactListAdapter adapter = getAdapter();
357                if (adapter != null) {
358                    adapter.setSelectedContact(mSelectedContactDirectoryId,
359                            mSelectedContactLookupKey, mSelectedContactId);
360                    getListView().invalidateViews();
361                }
362            }
363
364            // Also, launch a loader to pick up a new lookup URI in case it has changed
365            refreshSelectedContactUri();
366        }
367    }
368
369    private void parseSelectedContactUri() {
370        if (mSelectedContactUri != null) {
371            String directoryParam =
372                    mSelectedContactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
373            mSelectedContactDirectoryId = TextUtils.isEmpty(directoryParam) ? Directory.DEFAULT
374                    : Long.parseLong(directoryParam);
375            if (mSelectedContactUri.toString().startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) {
376                List<String> pathSegments = mSelectedContactUri.getPathSegments();
377                mSelectedContactLookupKey = Uri.encode(pathSegments.get(2));
378                if (pathSegments.size() == 4) {
379                    mSelectedContactId = ContentUris.parseId(mSelectedContactUri);
380                }
381            } else if (mSelectedContactUri.toString().startsWith(Contacts.CONTENT_URI.toString()) &&
382                    mSelectedContactUri.getPathSegments().size() >= 2) {
383                mSelectedContactLookupKey = null;
384                mSelectedContactId = ContentUris.parseId(mSelectedContactUri);
385            } else {
386                Log.e(TAG, "Unsupported contact URI: " + mSelectedContactUri);
387                mSelectedContactLookupKey = null;
388                mSelectedContactId = 0;
389            }
390
391        } else {
392            mSelectedContactDirectoryId = Directory.DEFAULT;
393            mSelectedContactLookupKey = null;
394            mSelectedContactId = 0;
395        }
396    }
397
398    @Override
399    protected void configureAdapter() {
400        super.configureAdapter();
401
402        ContactListAdapter adapter = getAdapter();
403        if (adapter == null) {
404            return;
405        }
406
407        boolean searchMode = isSearchMode();
408        if (!searchMode && mFilter != null) {
409            adapter.setFilter(mFilter);
410            if (mSelectionRequired
411                    || mFilter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) {
412                adapter.setSelectedContact(
413                        mSelectedContactDirectoryId, mSelectedContactLookupKey, mSelectedContactId);
414            }
415        }
416
417        // Display the user's profile if not in search mode
418        adapter.setIncludeProfile(!searchMode);
419    }
420
421    @Override
422    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
423        super.onLoadFinished(loader, data);
424        mSelectionVerified = false;
425
426        // Refresh the currently selected lookup in case it changed while we were sleeping
427        refreshSelectedContactUri();
428    }
429
430    @Override
431    public void onLoaderReset(Loader<Cursor> loader) {
432    }
433
434    private void checkSelection() {
435        if (mSelectionVerified) {
436            return;
437        }
438
439        if (mRefreshingContactUri) {
440            return;
441        }
442
443        if (isLoadingDirectoryList()) {
444            return;
445        }
446
447        ContactListAdapter adapter = getAdapter();
448        if (adapter == null) {
449            return;
450        }
451
452        boolean directoryLoading = true;
453        int count = adapter.getPartitionCount();
454        for (int i = 0; i < count; i++) {
455            Partition partition = adapter.getPartition(i);
456            if (partition instanceof DirectoryPartition) {
457                DirectoryPartition directory = (DirectoryPartition) partition;
458                if (directory.getDirectoryId() == mSelectedContactDirectoryId) {
459                    directoryLoading = directory.isLoading();
460                    break;
461                }
462            }
463        }
464
465        if (directoryLoading) {
466            return;
467        }
468
469        adapter.setSelectedContact(
470                mSelectedContactDirectoryId, mSelectedContactLookupKey, mSelectedContactId);
471
472        final int selectedPosition = adapter.getSelectedContactPosition();
473        if (selectedPosition != -1) {
474            mLastSelectedPosition = selectedPosition;
475        } else {
476            if (isSearchMode()) {
477                if (mDelaySelection) {
478                    selectFirstFoundContactAfterDelay();
479                    if (mListener != null) {
480                        mListener.onSelectionChange();
481                    }
482                    return;
483                }
484            } else if (mSelectionRequired) {
485                // A specific contact was requested, but it's not in the loaded list.
486
487                // Try reconfiguring and reloading the list that will hopefully contain
488                // the requested contact. Only take one attempt to avoid an infinite loop
489                // in case the contact cannot be found at all.
490                mSelectionRequired = false;
491
492                // If we were looking at a different specific contact, just reload
493                // FILTER_TYPE_ALL_ACCOUNTS is needed for the case where a new contact is added
494                // on a tablet and the loader is returning a stale list.  In this case, the contact
495                // will not be found until the next load. b/7621855 This will only fix the most
496                // common case where all accounts are shown. It will not fix the one account case.
497                // TODO: we may want to add more FILTER_TYPEs or relax this check to fix all other
498                // FILTER_TYPE cases.
499                if (mFilter != null
500                        && (mFilter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT
501                        || mFilter.filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS)) {
502                    reloadData();
503                } else {
504                    // Otherwise, call the listener, which will adjust the filter.
505                    notifyInvalidSelection();
506                }
507                return;
508            } else if (mFilter != null
509                    && mFilter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) {
510                // If we were trying to load a specific contact, but that contact no longer
511                // exists, call the listener, which will adjust the filter.
512                notifyInvalidSelection();
513                return;
514            }
515
516            saveSelectedUri(null);
517            selectDefaultContact();
518        }
519
520        mSelectionRequired = false;
521        mSelectionVerified = true;
522
523        if (mSelectionPersistenceRequested) {
524            saveSelectedUri(mSelectedContactUri);
525            mSelectionPersistenceRequested = false;
526        }
527
528        if (mSelectionToScreenRequested) {
529            requestSelectionToScreen(selectedPosition);
530        }
531
532        getListView().invalidateViews();
533
534        if (mListener != null) {
535            mListener.onSelectionChange();
536        }
537    }
538
539    /**
540     * Automatically selects the first found contact in search mode.  The selection
541     * is updated after a delay to allow the user to type without to much UI churn
542     * and to save bandwidth on directory queries.
543     */
544    public void selectFirstFoundContactAfterDelay() {
545        Handler handler = getHandler();
546        handler.removeMessages(MESSAGE_AUTOSELECT_FIRST_FOUND_CONTACT);
547
548        String queryString = getQueryString();
549        if (queryString != null
550                && queryString.length() >= AUTOSELECT_FIRST_FOUND_CONTACT_MIN_QUERY_LENGTH) {
551            handler.sendEmptyMessageDelayed(MESSAGE_AUTOSELECT_FIRST_FOUND_CONTACT,
552                    DELAY_AUTOSELECT_FIRST_FOUND_CONTACT_MILLIS);
553        } else {
554            setSelectedContactUri(null, false, false, false, false);
555        }
556    }
557
558    protected void selectDefaultContact() {
559        Uri contactUri = null;
560        ContactListAdapter adapter = getAdapter();
561        if (mLastSelectedPosition != -1) {
562            int count = adapter.getCount();
563            int pos = mLastSelectedPosition;
564            if (pos >= count && count > 0) {
565                pos = count - 1;
566            }
567            contactUri = adapter.getContactUri(pos);
568        }
569
570        if (contactUri == null) {
571            contactUri = adapter.getFirstContactUri();
572        }
573
574        setSelectedContactUri(contactUri, false, mSmoothScrollRequested, false, false);
575    }
576
577    protected void requestSelectionToScreen(int selectedPosition) {
578        if (selectedPosition != -1) {
579            AutoScrollListView listView = (AutoScrollListView)getListView();
580            listView.requestPositionToScreen(
581                    selectedPosition + listView.getHeaderViewsCount(), mSmoothScrollRequested);
582            mSelectionToScreenRequested = false;
583        }
584    }
585
586    @Override
587    public boolean isLoading() {
588        return mRefreshingContactUri || super.isLoading();
589    }
590
591    @Override
592    protected void startLoading() {
593        mStartedLoading = true;
594        mSelectionVerified = false;
595        super.startLoading();
596    }
597
598    public void reloadDataAndSetSelectedUri(Uri uri) {
599        setSelectedContactUri(uri, true, true, true, true);
600        reloadData();
601    }
602
603    @Override
604    public void reloadData() {
605        if (mStartedLoading) {
606            mSelectionVerified = false;
607            mLastSelectedPosition = -1;
608            super.reloadData();
609        }
610    }
611
612    public void setOnContactListActionListener(OnContactBrowserActionListener listener) {
613        mListener = listener;
614    }
615
616    public void createNewContact() {
617        if (mListener != null) mListener.onCreateNewContactAction();
618    }
619
620    public void viewContact(Uri contactUri) {
621        setSelectedContactUri(contactUri, false, false, true, false);
622        if (mListener != null) mListener.onViewContactAction(contactUri);
623    }
624
625    public void editContact(Uri contactUri) {
626        if (mListener != null) mListener.onEditContactAction(contactUri);
627    }
628
629    public void deleteContact(Uri contactUri) {
630        if (mListener != null) mListener.onDeleteContactAction(contactUri);
631    }
632
633    public void addToFavorites(Uri contactUri) {
634        if (mListener != null) mListener.onAddToFavoritesAction(contactUri);
635    }
636
637    public void removeFromFavorites(Uri contactUri) {
638        if (mListener != null) mListener.onRemoveFromFavoritesAction(contactUri);
639    }
640
641    public void callContact(Uri contactUri) {
642        if (mListener != null) mListener.onCallContactAction(contactUri);
643    }
644
645    public void smsContact(Uri contactUri) {
646        if (mListener != null) mListener.onSmsContactAction(contactUri);
647    }
648
649    private void notifyInvalidSelection() {
650        if (mListener != null) mListener.onInvalidSelection();
651    }
652
653    @Override
654    protected void finish() {
655        super.finish();
656        if (mListener != null) mListener.onFinishAction();
657    }
658
659    private void saveSelectedUri(Uri contactUri) {
660        if (isSearchMode()) {
661            return;
662        }
663
664        ContactListFilter.storeToPreferences(mPrefs, mFilter);
665
666        Editor editor = mPrefs.edit();
667        if (contactUri == null) {
668            editor.remove(getPersistentSelectionKey());
669        } else {
670            editor.putString(getPersistentSelectionKey(), contactUri.toString());
671        }
672        editor.apply();
673    }
674
675    private void restoreSelectedUri(boolean willReloadData) {
676        // The meaning of mSelectionRequired is that we need to show some
677        // selection other than the previous selection saved in shared preferences
678        if (mSelectionRequired) {
679            return;
680        }
681
682        String selectedUri = mPrefs.getString(getPersistentSelectionKey(), null);
683        if (selectedUri == null) {
684            setSelectedContactUri(null, false, false, false, willReloadData);
685        } else {
686            setSelectedContactUri(Uri.parse(selectedUri), false, false, false, willReloadData);
687        }
688    }
689
690    private void saveFilter() {
691        ContactListFilter.storeToPreferences(mPrefs, mFilter);
692    }
693
694    private void restoreFilter() {
695        mFilter = ContactListFilter.restoreDefaultPreferences(mPrefs);
696    }
697
698    private String getPersistentSelectionKey() {
699        if (mFilter == null) {
700            return mPersistentSelectionPrefix;
701        } else {
702            return mPersistentSelectionPrefix + "-" + mFilter.getId();
703        }
704    }
705
706    public boolean isOptionsMenuChanged() {
707        // This fragment does not have an option menu of its own
708        return false;
709    }
710}
711