1/*
2 * Copyright (C) 2011 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.ContactPhotoManager;
19import com.android.contacts.ContactTileLoaderFactory;
20import com.android.contacts.R;
21import com.android.contacts.preference.ContactsPreferences;
22import com.android.contacts.util.AccountFilterUtil;
23
24import android.app.Activity;
25import android.app.Fragment;
26import android.app.LoaderManager;
27import android.content.Context;
28import android.content.CursorLoader;
29import android.content.Intent;
30import android.content.Loader;
31import android.database.Cursor;
32import android.graphics.Rect;
33import android.net.Uri;
34import android.os.Bundle;
35import android.provider.ContactsContract.Directory;
36import android.util.Log;
37import android.view.LayoutInflater;
38import android.view.View;
39import android.view.View.OnClickListener;
40import android.view.ViewGroup;
41import android.widget.AbsListView;
42import android.widget.AdapterView;
43import android.widget.AdapterView.OnItemClickListener;
44import android.widget.FrameLayout;
45import android.widget.ListView;
46import android.widget.TextView;
47
48/**
49 * Fragment for Phone UI's favorite screen.
50 *
51 * This fragment contains three kinds of contacts in one screen: "starred", "frequent", and "all"
52 * contacts. To show them at once, this merges results from {@link ContactTileAdapter} and
53 * {@link PhoneNumberListAdapter} into one unified list using {@link PhoneFavoriteMergedAdapter}.
54 * A contact filter header is also inserted between those adapters' results.
55 */
56public class PhoneFavoriteFragment extends Fragment implements OnItemClickListener {
57    private static final String TAG = PhoneFavoriteFragment.class.getSimpleName();
58    private static final boolean DEBUG = false;
59
60    /**
61     * Used with LoaderManager.
62     */
63    private static int LOADER_ID_CONTACT_TILE = 1;
64    private static int LOADER_ID_ALL_CONTACTS = 2;
65
66    private static final String KEY_FILTER = "filter";
67
68    private static final int REQUEST_CODE_ACCOUNT_FILTER = 1;
69
70    public interface Listener {
71        public void onContactSelected(Uri contactUri);
72    }
73
74    private class ContactTileLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> {
75        @Override
76        public CursorLoader onCreateLoader(int id, Bundle args) {
77            if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onCreateLoader.");
78            return ContactTileLoaderFactory.createStrequentPhoneOnlyLoader(getActivity());
79        }
80
81        @Override
82        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
83            if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onLoadFinished");
84            mContactTileAdapter.setContactCursor(data);
85
86            if (mAllContactsForceReload) {
87                mAllContactsAdapter.onDataReload();
88                // Use restartLoader() to make LoaderManager to load the section again.
89                getLoaderManager().restartLoader(
90                        LOADER_ID_ALL_CONTACTS, null, mAllContactsLoaderListener);
91            } else if (!mAllContactsLoaderStarted) {
92                // Load "all" contacts if not loaded yet.
93                getLoaderManager().initLoader(
94                        LOADER_ID_ALL_CONTACTS, null, mAllContactsLoaderListener);
95            }
96            mAllContactsForceReload = false;
97            mAllContactsLoaderStarted = true;
98
99            // Show the filter header with "loading" state.
100            updateFilterHeaderView();
101            mAccountFilterHeader.setVisibility(View.VISIBLE);
102        }
103
104        @Override
105        public void onLoaderReset(Loader<Cursor> loader) {
106            if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onLoaderReset. ");
107        }
108    }
109
110    private class AllContactsLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> {
111        @Override
112        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
113            if (DEBUG) Log.d(TAG, "AllContactsLoaderListener#onCreateLoader");
114            CursorLoader loader = new CursorLoader(getActivity(), null, null, null, null, null);
115            mAllContactsAdapter.configureLoader(loader, Directory.DEFAULT);
116            return loader;
117        }
118
119        @Override
120        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
121            if (DEBUG) Log.d(TAG, "AllContactsLoaderListener#onLoadFinished");
122            mAllContactsAdapter.changeCursor(0, data);
123            updateFilterHeaderView();
124            mAccountFilterHeaderContainer.setVisibility(View.VISIBLE);
125        }
126
127        @Override
128        public void onLoaderReset(Loader<Cursor> loader) {
129            if (DEBUG) Log.d(TAG, "AllContactsLoaderListener#onLoaderReset. ");
130        }
131    }
132
133    private class ContactTileAdapterListener implements ContactTileAdapter.Listener {
134        @Override
135        public void onContactSelected(Uri contactUri, Rect targetRect) {
136            if (mListener != null) {
137                mListener.onContactSelected(contactUri);
138            }
139        }
140    }
141
142    private class FilterHeaderClickListener implements OnClickListener {
143        @Override
144        public void onClick(View view) {
145            AccountFilterUtil.startAccountFilterActivityForResult(
146                    PhoneFavoriteFragment.this, REQUEST_CODE_ACCOUNT_FILTER);
147        }
148    }
149
150    private class ContactsPreferenceChangeListener
151            implements ContactsPreferences.ChangeListener {
152        @Override
153        public void onChange() {
154            if (loadContactsPreferences()) {
155                requestReloadAllContacts();
156            }
157        }
158    }
159
160    private class ScrollListener implements ListView.OnScrollListener {
161        private boolean mShouldShowFastScroller;
162        @Override
163        public void onScroll(AbsListView view,
164                int firstVisibleItem, int visibleItemCount, int totalItemCount) {
165            // FastScroller should be visible only when the user is seeing "all" contacts section.
166            final boolean shouldShow = mAdapter.shouldShowFirstScroller(firstVisibleItem);
167            if (shouldShow != mShouldShowFastScroller) {
168                mListView.setVerticalScrollBarEnabled(shouldShow);
169                mListView.setFastScrollEnabled(shouldShow);
170                mListView.setFastScrollAlwaysVisible(shouldShow);
171                mShouldShowFastScroller = shouldShow;
172            }
173        }
174
175        @Override
176        public void onScrollStateChanged(AbsListView view, int scrollState) {
177        }
178    }
179
180    private Listener mListener;
181    private PhoneFavoriteMergedAdapter mAdapter;
182    private ContactTileAdapter mContactTileAdapter;
183    private PhoneNumberListAdapter mAllContactsAdapter;
184
185    /**
186     * true when the loader for {@link PhoneNumberListAdapter} has started already.
187     */
188    private boolean mAllContactsLoaderStarted;
189    /**
190     * true when the loader for {@link PhoneNumberListAdapter} must reload "all" contacts again.
191     * It typically happens when {@link ContactsPreferences} has changed its settings
192     * (display order and sort order)
193     */
194    private boolean mAllContactsForceReload;
195
196    private ContactsPreferences mContactsPrefs;
197    private ContactListFilter mFilter;
198
199    private TextView mEmptyView;
200    private ListView mListView;
201    /**
202     * Layout containing {@link #mAccountFilterHeader}. Used to limit area being "pressed".
203     */
204    private FrameLayout mAccountFilterHeaderContainer;
205    private View mAccountFilterHeader;
206
207    private final ContactTileAdapter.Listener mContactTileAdapterListener =
208            new ContactTileAdapterListener();
209    private final LoaderManager.LoaderCallbacks<Cursor> mContactTileLoaderListener =
210            new ContactTileLoaderListener();
211    private final LoaderManager.LoaderCallbacks<Cursor> mAllContactsLoaderListener =
212            new AllContactsLoaderListener();
213    private final OnClickListener mFilterHeaderClickListener = new FilterHeaderClickListener();
214    private final ContactsPreferenceChangeListener mContactsPreferenceChangeListener =
215            new ContactsPreferenceChangeListener();
216    private final ScrollListener mScrollListener = new ScrollListener();
217
218    @Override
219    public void onCreate(Bundle savedState) {
220        super.onCreate(savedState);
221        if (savedState != null) {
222            mFilter = savedState.getParcelable(KEY_FILTER);
223        }
224    }
225
226    @Override
227    public void onSaveInstanceState(Bundle outState) {
228        super.onSaveInstanceState(outState);
229        outState.putParcelable(KEY_FILTER, mFilter);
230    }
231
232    @Override
233    public void onAttach(Activity activity) {
234        super.onAttach(activity);
235
236        mContactsPrefs = new ContactsPreferences(activity);
237    }
238
239    @Override
240    public View onCreateView(LayoutInflater inflater, ViewGroup container,
241            Bundle savedInstanceState) {
242        final View listLayout = inflater.inflate(
243                R.layout.phone_contact_tile_list, container, false);
244
245        mListView = (ListView) listLayout.findViewById(R.id.contact_tile_list);
246        mListView.setItemsCanFocus(true);
247        mListView.setOnItemClickListener(this);
248        mListView.setVerticalScrollBarEnabled(false);
249        mListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_RIGHT);
250        mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
251
252        initAdapters(getActivity(), inflater);
253
254        mListView.setAdapter(mAdapter);
255
256        mListView.setOnScrollListener(mScrollListener);
257        mListView.setFastScrollEnabled(false);
258        mListView.setFastScrollAlwaysVisible(false);
259
260        mEmptyView = (TextView) listLayout.findViewById(R.id.contact_tile_list_empty);
261        mEmptyView.setText(getString(R.string.listTotalAllContactsZero));
262        mListView.setEmptyView(mEmptyView);
263
264        updateFilterHeaderView();
265
266        return listLayout;
267    }
268
269    /**
270     * Constructs and initializes {@link #mContactTileAdapter}, {@link #mAllContactsAdapter}, and
271     * {@link #mAllContactsAdapter}.
272     *
273     * TODO: Move all the code here to {@link PhoneFavoriteMergedAdapter} if possible.
274     * There are two problems: account header (whose content changes depending on filter settings)
275     * and OnClickListener (which initiates {@link Activity#startActivityForResult(Intent, int)}).
276     * See also issue 5429203, 5269692, and 5432286. If we are able to have a singleton for filter,
277     * this work will become easier.
278     */
279    private void initAdapters(Context context, LayoutInflater inflater) {
280        mContactTileAdapter = new ContactTileAdapter(context, mContactTileAdapterListener,
281                getResources().getInteger(R.integer.contact_tile_column_count),
282                ContactTileAdapter.DisplayType.STREQUENT_PHONE_ONLY);
283        mContactTileAdapter.setPhotoLoader(ContactPhotoManager.getInstance(context));
284
285        // Setup the "all" adapter manually. See also the setup logic in ContactEntryListFragment.
286        mAllContactsAdapter = new PhoneNumberListAdapter(context);
287        mAllContactsAdapter.setDisplayPhotos(true);
288        mAllContactsAdapter.setQuickContactEnabled(true);
289        mAllContactsAdapter.setSearchMode(false);
290        mAllContactsAdapter.setIncludeProfile(false);
291        mAllContactsAdapter.setSelectionVisible(false);
292        mAllContactsAdapter.setDarkTheme(true);
293        mAllContactsAdapter.setPhotoLoader(ContactPhotoManager.getInstance(context));
294        // Disable directory header.
295        mAllContactsAdapter.setHasHeader(0, false);
296        // Show A-Z section index.
297        mAllContactsAdapter.setSectionHeaderDisplayEnabled(true);
298        // Disable pinned header. It doesn't work with this fragment.
299        mAllContactsAdapter.setPinnedPartitionHeadersEnabled(false);
300        // Put photos on left for consistency with "frequent" contacts section.
301        mAllContactsAdapter.setPhotoPosition(ContactListItemView.PhotoPosition.LEFT);
302
303        if (mFilter != null) {
304            mAllContactsAdapter.setFilter(mFilter);
305        }
306
307        // Create the account filter header but keep it hidden until "all" contacts are loaded.
308        mAccountFilterHeaderContainer = new FrameLayout(context, null);
309        mAccountFilterHeader = inflater.inflate(R.layout.account_filter_header_for_phone_favorite,
310                mListView, false);
311        mAccountFilterHeader.setOnClickListener(mFilterHeaderClickListener);
312        mAccountFilterHeaderContainer.addView(mAccountFilterHeader);
313        mAccountFilterHeaderContainer.setVisibility(View.GONE);
314
315        mAdapter = new PhoneFavoriteMergedAdapter(context,
316                mContactTileAdapter, mAccountFilterHeaderContainer, mAllContactsAdapter);
317
318    }
319
320    @Override
321    public void onStart() {
322        super.onStart();
323
324        mContactsPrefs.registerChangeListener(mContactsPreferenceChangeListener);
325
326        // If ContactsPreferences has changed, we need to reload "all" contacts with the new
327        // settings. If mAllContactsFoarceReload is already true, it should be kept.
328        if (loadContactsPreferences()) {
329            mAllContactsForceReload = true;
330        }
331
332        // Use initLoader() instead of reloadLoader() to refraing unnecessary reload.
333        // This method call implicitly assures ContactTileLoaderListener's onLoadFinished() will
334        // be called, on which we'll check if "all" contacts should be reloaded again or not.
335        getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null, mContactTileLoaderListener);
336    }
337
338    @Override
339    public void onStop() {
340        super.onStop();
341        mContactsPrefs.unregisterChangeListener();
342    }
343
344    /**
345     * {@inheritDoc}
346     *
347     * This is only effective for elements provided by {@link #mContactTileAdapter}.
348     * {@link #mContactTileAdapter} has its own logic for click events.
349     */
350    @Override
351    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
352        final int contactTileAdapterCount = mContactTileAdapter.getCount();
353        if (position <= contactTileAdapterCount) {
354            Log.e(TAG, "onItemClick() event for unexpected position. "
355                    + "The position " + position + " is before \"all\" section. Ignored.");
356        } else {
357            final int localPosition = position - mContactTileAdapter.getCount() - 1;
358            if (mListener != null) {
359                mListener.onContactSelected(mAllContactsAdapter.getDataUri(localPosition));
360            }
361        }
362    }
363
364    @Override
365    public void onActivityResult(int requestCode, int resultCode, Intent data) {
366        if (requestCode == REQUEST_CODE_ACCOUNT_FILTER) {
367            if (getActivity() != null) {
368                AccountFilterUtil.handleAccountFilterResult(
369                        ContactListFilterController.getInstance(getActivity()), resultCode, data);
370            } else {
371                Log.e(TAG, "getActivity() returns null during Fragment#onActivityResult()");
372            }
373        }
374    }
375
376    private boolean loadContactsPreferences() {
377        if (mContactsPrefs == null || mAllContactsAdapter == null) {
378            return false;
379        }
380
381        boolean changed = false;
382        if (mAllContactsAdapter.getContactNameDisplayOrder() != mContactsPrefs.getDisplayOrder()) {
383            mAllContactsAdapter.setContactNameDisplayOrder(mContactsPrefs.getDisplayOrder());
384            changed = true;
385        }
386
387        if (mAllContactsAdapter.getSortOrder() != mContactsPrefs.getSortOrder()) {
388            mAllContactsAdapter.setSortOrder(mContactsPrefs.getSortOrder());
389            changed = true;
390        }
391
392        return changed;
393    }
394
395    /**
396     * Requests to reload "all" contacts. If the section is already loaded, this method will
397     * force reloading it now. If the section isn't loaded yet, the actual load may be done later
398     * (on {@link #onStart()}.
399     */
400    private void requestReloadAllContacts() {
401        if (DEBUG) {
402            Log.d(TAG, "requestReloadAllContacts()"
403                    + " mAllContactsAdapter: " + mAllContactsAdapter
404                    + ", mAllContactsLoaderStarted: " + mAllContactsLoaderStarted);
405        }
406
407        if (mAllContactsAdapter == null || !mAllContactsLoaderStarted) {
408            // Remember this request until next load on onStart().
409            mAllContactsForceReload = true;
410            return;
411        }
412
413        if (DEBUG) Log.d(TAG, "Reload \"all\" contacts now.");
414
415        mAllContactsAdapter.onDataReload();
416        // Use restartLoader() to make LoaderManager to load the section again.
417        getLoaderManager().restartLoader(LOADER_ID_ALL_CONTACTS, null, mAllContactsLoaderListener);
418    }
419
420    private void updateFilterHeaderView() {
421        final ContactListFilter filter = getFilter();
422        if (mAccountFilterHeader == null || mAllContactsAdapter == null || filter == null) {
423            return;
424        }
425        AccountFilterUtil.updateAccountFilterTitleForPhone(
426                mAccountFilterHeader, filter, mAllContactsAdapter.isLoading(), true);
427    }
428
429    public ContactListFilter getFilter() {
430        return mFilter;
431    }
432
433    public void setFilter(ContactListFilter filter) {
434        if ((mFilter == null && filter == null) || (mFilter != null && mFilter.equals(filter))) {
435            return;
436        }
437
438        if (DEBUG) {
439            Log.d(TAG, "setFilter(). old filter (" + mFilter
440                    + ") will be replaced with new filter (" + filter + ")");
441        }
442
443        mFilter = filter;
444
445        if (mAllContactsAdapter != null) {
446            mAllContactsAdapter.setFilter(mFilter);
447            requestReloadAllContacts();
448            updateFilterHeaderView();
449        }
450    }
451
452    public void setListener(Listener listener) {
453        mListener = listener;
454    }
455}