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