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 }