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}