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.dialog.ClearFrequentsDialog; 22 import com.android.contacts.interactions.ImportExportDialogFragment; 23 import com.android.contacts.preference.ContactsPreferences; 24 import com.android.contacts.util.AccountFilterUtil; 25 26 import android.app.Activity; 27 import android.app.Fragment; 28 import android.app.LoaderManager; 29 import android.content.CursorLoader; 30 import android.content.Intent; 31 import android.content.Loader; 32 import android.database.Cursor; 33 import android.graphics.Rect; 34 import android.net.Uri; 35 import android.os.Bundle; 36 import android.os.Handler; 37 import android.os.Message; 38 import android.provider.ContactsContract; 39 import android.provider.ContactsContract.Directory; 40 import android.provider.Settings; 41 import android.util.Log; 42 import android.view.LayoutInflater; 43 import android.view.Menu; 44 import android.view.MenuInflater; 45 import android.view.MenuItem; 46 import android.view.View; 47 import android.view.View.OnClickListener; 48 import android.view.ViewGroup; 49 import android.widget.AbsListView; 50 import android.widget.AdapterView; 51 import android.widget.AdapterView.OnItemClickListener; 52 import android.widget.FrameLayout; 53 import android.widget.ListView; 54 import android.widget.TextView; 55 56 /** 57 * Fragment for Phone UI's favorite screen. 58 * 59 * This fragment contains three kinds of contacts in one screen: "starred", "frequent", and "all" 60 * contacts. To show them at once, this merges results from {@link ContactTileAdapter} and 61 * {@link PhoneNumberListAdapter} into one unified list using {@link PhoneFavoriteMergedAdapter}. 62 * A contact filter header is also inserted between those adapters' results. 63 */ 64 public class PhoneFavoriteFragment extends Fragment implements OnItemClickListener { 65 private static final String TAG = PhoneFavoriteFragment.class.getSimpleName(); 66 private static final boolean DEBUG = false; 67 68 /** 69 * Used with LoaderManager. 70 */ 71 private static int LOADER_ID_CONTACT_TILE = 1; 72 private static int LOADER_ID_ALL_CONTACTS = 2; 73 74 private static final String KEY_FILTER = "filter"; 75 76 private static final int REQUEST_CODE_ACCOUNT_FILTER = 1; 77 78 public interface Listener { 79 public void onContactSelected(Uri contactUri); 80 public void onCallNumberDirectly(String phoneNumber); 81 } 82 83 private class ContactTileLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> { 84 @Override 85 public CursorLoader onCreateLoader(int id, Bundle args) { 86 if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onCreateLoader."); 87 return ContactTileLoaderFactory.createStrequentPhoneOnlyLoader(getActivity()); 88 } 89 90 @Override 91 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 92 if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onLoadFinished"); 93 mContactTileAdapter.setContactCursor(data); 94 95 if (mAllContactsForceReload) { 96 mAllContactsAdapter.onDataReload(); 97 // Use restartLoader() to make LoaderManager to load the section again. 98 getLoaderManager().restartLoader( 99 LOADER_ID_ALL_CONTACTS, null, mAllContactsLoaderListener); 100 } else if (!mAllContactsLoaderStarted) { 101 // Load "all" contacts if not loaded yet. 102 getLoaderManager().initLoader( 103 LOADER_ID_ALL_CONTACTS, null, mAllContactsLoaderListener); 104 } 105 mAllContactsForceReload = false; 106 mAllContactsLoaderStarted = true; 107 108 // Show the filter header with "loading" state. 109 updateFilterHeaderView(); 110 mAccountFilterHeader.setVisibility(View.VISIBLE); 111 112 // invalidate the options menu if needed 113 invalidateOptionsMenuIfNeeded(); 114 } 115 116 @Override 117 public void onLoaderReset(Loader<Cursor> loader) { 118 if (DEBUG) Log.d(TAG, "ContactTileLoaderListener#onLoaderReset. "); 119 } 120 } 121 122 private class AllContactsLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> { 123 @Override 124 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 125 if (DEBUG) Log.d(TAG, "AllContactsLoaderListener#onCreateLoader"); 126 CursorLoader loader = new CursorLoader(getActivity(), null, null, null, null, null); 127 mAllContactsAdapter.configureLoader(loader, Directory.DEFAULT); 128 return loader; 129 } 130 131 @Override 132 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 133 if (DEBUG) Log.d(TAG, "AllContactsLoaderListener#onLoadFinished"); 134 mAllContactsAdapter.changeCursor(0, data); 135 updateFilterHeaderView(); 136 mHandler.removeMessages(MESSAGE_SHOW_LOADING_EFFECT); 137 mLoadingView.setVisibility(View.VISIBLE); 138 } 139 140 @Override 141 public void onLoaderReset(Loader<Cursor> loader) { 142 if (DEBUG) Log.d(TAG, "AllContactsLoaderListener#onLoaderReset. "); 143 } 144 } 145 146 private class ContactTileAdapterListener implements ContactTileView.Listener { 147 @Override 148 public void onContactSelected(Uri contactUri, Rect targetRect) { 149 if (mListener != null) { 150 mListener.onContactSelected(contactUri); 151 } 152 } 153 154 @Override 155 public void onCallNumberDirectly(String phoneNumber) { 156 if (mListener != null) { 157 mListener.onCallNumberDirectly(phoneNumber); 158 } 159 } 160 161 @Override 162 public int getApproximateTileWidth() { 163 return getView().getWidth() / mContactTileAdapter.getColumnCount(); 164 } 165 } 166 167 private class FilterHeaderClickListener implements OnClickListener { 168 @Override 169 public void onClick(View view) { 170 AccountFilterUtil.startAccountFilterActivityForResult( 171 PhoneFavoriteFragment.this, 172 REQUEST_CODE_ACCOUNT_FILTER, 173 mFilter); 174 } 175 } 176 177 private class ContactsPreferenceChangeListener 178 implements ContactsPreferences.ChangeListener { 179 @Override 180 public void onChange() { 181 if (loadContactsPreferences()) { 182 requestReloadAllContacts(); 183 } 184 } 185 } 186 187 private class ScrollListener implements ListView.OnScrollListener { 188 private boolean mShouldShowFastScroller; 189 @Override 190 public void onScroll(AbsListView view, 191 int firstVisibleItem, int visibleItemCount, int totalItemCount) { 192 // FastScroller should be visible only when the user is seeing "all" contacts section. 193 final boolean shouldShow = mAdapter.shouldShowFirstScroller(firstVisibleItem); 194 if (shouldShow != mShouldShowFastScroller) { 195 mListView.setVerticalScrollBarEnabled(shouldShow); 196 mListView.setFastScrollEnabled(shouldShow); 197 mListView.setFastScrollAlwaysVisible(shouldShow); 198 mShouldShowFastScroller = shouldShow; 199 } 200 } 201 202 @Override 203 public void onScrollStateChanged(AbsListView view, int scrollState) { 204 } 205 } 206 207 private static final int MESSAGE_SHOW_LOADING_EFFECT = 1; 208 private static final int LOADING_EFFECT_DELAY = 500; // ms 209 private final Handler mHandler = new Handler() { 210 @Override 211 public void handleMessage(Message msg) { 212 switch (msg.what) { 213 case MESSAGE_SHOW_LOADING_EFFECT: 214 mLoadingView.setVisibility(View.VISIBLE); 215 break; 216 } 217 } 218 }; 219 220 private Listener mListener; 221 private PhoneFavoriteMergedAdapter mAdapter; 222 private ContactTileAdapter mContactTileAdapter; 223 private PhoneNumberListAdapter mAllContactsAdapter; 224 225 /** 226 * true when the loader for {@link PhoneNumberListAdapter} has started already. 227 */ 228 private boolean mAllContactsLoaderStarted; 229 /** 230 * true when the loader for {@link PhoneNumberListAdapter} must reload "all" contacts again. 231 * It typically happens when {@link ContactsPreferences} has changed its settings 232 * (display order and sort order) 233 */ 234 private boolean mAllContactsForceReload; 235 236 private ContactsPreferences mContactsPrefs; 237 private ContactListFilter mFilter; 238 239 private TextView mEmptyView; 240 private ListView mListView; 241 /** 242 * Layout containing {@link #mAccountFilterHeader}. Used to limit area being "pressed". 243 */ 244 private FrameLayout mAccountFilterHeaderContainer; 245 private View mAccountFilterHeader; 246 247 /** 248 * Layout used when contacts load is slower than expected and thus "loading" view should be 249 * shown. 250 */ 251 private View mLoadingView; 252 253 private final ContactTileView.Listener mContactTileAdapterListener = 254 new ContactTileAdapterListener(); 255 private final LoaderManager.LoaderCallbacks<Cursor> mContactTileLoaderListener = 256 new ContactTileLoaderListener(); 257 private final LoaderManager.LoaderCallbacks<Cursor> mAllContactsLoaderListener = 258 new AllContactsLoaderListener(); 259 private final OnClickListener mFilterHeaderClickListener = new FilterHeaderClickListener(); 260 private final ContactsPreferenceChangeListener mContactsPreferenceChangeListener = 261 new ContactsPreferenceChangeListener(); 262 private final ScrollListener mScrollListener = new ScrollListener(); 263 264 private boolean mOptionsMenuHasFrequents; 265 266 @Override 267 public void onAttach(Activity activity) { 268 if (DEBUG) Log.d(TAG, "onAttach()"); 269 super.onAttach(activity); 270 271 mContactsPrefs = new ContactsPreferences(activity); 272 273 // Construct two base adapters which will become part of PhoneFavoriteMergedAdapter. 274 // We don't construct the resultant adapter at this moment since it requires LayoutInflater 275 // that will be available on onCreateView(). 276 277 mContactTileAdapter = new ContactTileAdapter(activity, mContactTileAdapterListener, 278 getResources().getInteger(R.integer.contact_tile_column_count_in_favorites), 279 ContactTileAdapter.DisplayType.STREQUENT_PHONE_ONLY); 280 mContactTileAdapter.setPhotoLoader(ContactPhotoManager.getInstance(activity)); 281 282 // Setup the "all" adapter manually. See also the setup logic in ContactEntryListFragment. 283 mAllContactsAdapter = new PhoneNumberListAdapter(activity); 284 mAllContactsAdapter.setDisplayPhotos(true); 285 mAllContactsAdapter.setQuickContactEnabled(true); 286 mAllContactsAdapter.setSearchMode(false); 287 mAllContactsAdapter.setIncludeProfile(false); 288 mAllContactsAdapter.setSelectionVisible(false); 289 mAllContactsAdapter.setDarkTheme(true); 290 mAllContactsAdapter.setPhotoLoader(ContactPhotoManager.getInstance(activity)); 291 // Disable directory header. 292 mAllContactsAdapter.setHasHeader(0, false); 293 // Show A-Z section index. 294 mAllContactsAdapter.setSectionHeaderDisplayEnabled(true); 295 // Disable pinned header. It doesn't work with this fragment. 296 mAllContactsAdapter.setPinnedPartitionHeadersEnabled(false); 297 // Put photos on left for consistency with "frequent" contacts section. 298 mAllContactsAdapter.setPhotoPosition(ContactListItemView.PhotoPosition.LEFT); 299 300 // Use Callable.CONTENT_URI which will include not only phone numbers but also SIP 301 // addresses. 302 mAllContactsAdapter.setUseCallableUri(true); 303 304 mAllContactsAdapter.setContactNameDisplayOrder(mContactsPrefs.getDisplayOrder()); 305 mAllContactsAdapter.setSortOrder(mContactsPrefs.getSortOrder()); 306 } 307 308 @Override 309 public void onCreate(Bundle savedState) { 310 if (DEBUG) Log.d(TAG, "onCreate()"); 311 super.onCreate(savedState); 312 if (savedState != null) { 313 mFilter = savedState.getParcelable(KEY_FILTER); 314 315 if (mFilter != null) { 316 mAllContactsAdapter.setFilter(mFilter); 317 } 318 } 319 setHasOptionsMenu(true); 320 } 321 322 @Override 323 public void onSaveInstanceState(Bundle outState) { 324 super.onSaveInstanceState(outState); 325 outState.putParcelable(KEY_FILTER, mFilter); 326 } 327 328 @Override 329 public View onCreateView(LayoutInflater inflater, ViewGroup container, 330 Bundle savedInstanceState) { 331 final View listLayout = inflater.inflate( 332 R.layout.phone_contact_tile_list, container, false); 333 334 mListView = (ListView) listLayout.findViewById(R.id.contact_tile_list); 335 mListView.setItemsCanFocus(true); 336 mListView.setOnItemClickListener(this); 337 mListView.setVerticalScrollBarEnabled(false); 338 mListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_RIGHT); 339 mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY); 340 341 // Create the account filter header but keep it hidden until "all" contacts are loaded. 342 mAccountFilterHeaderContainer = new FrameLayout(getActivity(), null); 343 mAccountFilterHeader = inflater.inflate(R.layout.account_filter_header_for_phone_favorite, 344 mListView, false); 345 mAccountFilterHeader.setOnClickListener(mFilterHeaderClickListener); 346 mAccountFilterHeaderContainer.addView(mAccountFilterHeader); 347 348 mLoadingView = inflater.inflate(R.layout.phone_loading_contacts, mListView, false); 349 350 mAdapter = new PhoneFavoriteMergedAdapter(getActivity(), 351 mContactTileAdapter, mAccountFilterHeaderContainer, mAllContactsAdapter, 352 mLoadingView); 353 354 mListView.setAdapter(mAdapter); 355 356 mListView.setOnScrollListener(mScrollListener); 357 mListView.setFastScrollEnabled(false); 358 mListView.setFastScrollAlwaysVisible(false); 359 360 mEmptyView = (TextView) listLayout.findViewById(R.id.contact_tile_list_empty); 361 mEmptyView.setText(getString(R.string.listTotalAllContactsZero)); 362 mListView.setEmptyView(mEmptyView); 363 364 updateFilterHeaderView(); 365 366 return listLayout; 367 } 368 369 private boolean isOptionsMenuChanged() { 370 return mOptionsMenuHasFrequents != hasFrequents(); 371 } 372 373 private void invalidateOptionsMenuIfNeeded() { 374 if (isOptionsMenuChanged()) { 375 getActivity().invalidateOptionsMenu(); 376 } 377 } 378 379 @Override 380 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 381 super.onCreateOptionsMenu(menu, inflater); 382 inflater.inflate(R.menu.phone_favorite_options, menu); 383 } 384 385 @Override 386 public void onPrepareOptionsMenu(Menu menu) { 387 final MenuItem clearFrequents = menu.findItem(R.id.menu_clear_frequents); 388 mOptionsMenuHasFrequents = hasFrequents(); 389 clearFrequents.setVisible(mOptionsMenuHasFrequents); 390 } 391 392 private boolean hasFrequents() { 393 return mContactTileAdapter.getNumFrequents() > 0; 394 } 395 396 @Override 397 public boolean onOptionsItemSelected(MenuItem item) { 398 switch (item.getItemId()) { 399 case R.id.menu_import_export: 400 // We hard-code the "contactsAreAvailable" argument because doing it properly would 401 // involve querying a {@link ProviderStatusLoader}, which we don't want to do right 402 // now in Dialtacts for (potential) performance reasons. Compare with how it is 403 // done in {@link PeopleActivity}. 404 ImportExportDialogFragment.show(getFragmentManager(), true); 405 return true; 406 case R.id.menu_accounts: 407 final Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS); 408 intent.putExtra(Settings.EXTRA_AUTHORITIES, new String[] { 409 ContactsContract.AUTHORITY 410 }); 411 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 412 startActivity(intent); 413 return true; 414 case R.id.menu_clear_frequents: 415 ClearFrequentsDialog.show(getFragmentManager()); 416 return true; 417 } 418 return false; 419 } 420 421 @Override 422 public void onStart() { 423 super.onStart(); 424 425 mContactsPrefs.registerChangeListener(mContactsPreferenceChangeListener); 426 427 // If ContactsPreferences has changed, we need to reload "all" contacts with the new 428 // settings. If mAllContactsFoarceReload is already true, it should be kept. 429 if (loadContactsPreferences()) { 430 mAllContactsForceReload = true; 431 } 432 433 // Use initLoader() instead of restartLoader() to refraining unnecessary reload. 434 // This method call implicitly assures ContactTileLoaderListener's onLoadFinished() will 435 // be called, on which we'll check if "all" contacts should be reloaded again or not. 436 getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null, mContactTileLoaderListener); 437 438 // Delay showing "loading" view until certain amount of time so that users won't see 439 // instant flash of the view when the contacts load is fast enough. 440 // This will be kept shown until both tile and all sections are loaded. 441 mLoadingView.setVisibility(View.INVISIBLE); 442 mHandler.sendEmptyMessageDelayed(MESSAGE_SHOW_LOADING_EFFECT, LOADING_EFFECT_DELAY); 443 } 444 445 @Override 446 public void onStop() { 447 super.onStop(); 448 mContactsPrefs.unregisterChangeListener(); 449 } 450 451 /** 452 * {@inheritDoc} 453 * 454 * This is only effective for elements provided by {@link #mContactTileAdapter}. 455 * {@link #mContactTileAdapter} has its own logic for click events. 456 */ 457 @Override 458 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 459 final int contactTileAdapterCount = mContactTileAdapter.getCount(); 460 if (position <= contactTileAdapterCount) { 461 Log.e(TAG, "onItemClick() event for unexpected position. " 462 + "The position " + position + " is before \"all\" section. Ignored."); 463 } else { 464 final int localPosition = position - mContactTileAdapter.getCount() - 1; 465 if (mListener != null) { 466 mListener.onContactSelected(mAllContactsAdapter.getDataUri(localPosition)); 467 } 468 } 469 } 470 471 @Override 472 public void onActivityResult(int requestCode, int resultCode, Intent data) { 473 if (requestCode == REQUEST_CODE_ACCOUNT_FILTER) { 474 if (getActivity() != null) { 475 AccountFilterUtil.handleAccountFilterResult( 476 ContactListFilterController.getInstance(getActivity()), resultCode, data); 477 } else { 478 Log.e(TAG, "getActivity() returns null during Fragment#onActivityResult()"); 479 } 480 } 481 } 482 483 private boolean loadContactsPreferences() { 484 if (mContactsPrefs == null || mAllContactsAdapter == null) { 485 return false; 486 } 487 488 boolean changed = false; 489 final int currentDisplayOrder = mContactsPrefs.getDisplayOrder(); 490 if (mAllContactsAdapter.getContactNameDisplayOrder() != currentDisplayOrder) { 491 mAllContactsAdapter.setContactNameDisplayOrder(currentDisplayOrder); 492 changed = true; 493 } 494 495 final int currentSortOrder = mContactsPrefs.getSortOrder(); 496 if (mAllContactsAdapter.getSortOrder() != currentSortOrder) { 497 mAllContactsAdapter.setSortOrder(currentSortOrder); 498 changed = true; 499 } 500 501 return changed; 502 } 503 504 /** 505 * Requests to reload "all" contacts. If the section is already loaded, this method will 506 * force reloading it now. If the section isn't loaded yet, the actual load may be done later 507 * (on {@link #onStart()}. 508 */ 509 private void requestReloadAllContacts() { 510 if (DEBUG) { 511 Log.d(TAG, "requestReloadAllContacts()" 512 + " mAllContactsAdapter: " + mAllContactsAdapter 513 + ", mAllContactsLoaderStarted: " + mAllContactsLoaderStarted); 514 } 515 516 if (mAllContactsAdapter == null || !mAllContactsLoaderStarted) { 517 // Remember this request until next load on onStart(). 518 mAllContactsForceReload = true; 519 return; 520 } 521 522 if (DEBUG) Log.d(TAG, "Reload \"all\" contacts now."); 523 524 mAllContactsAdapter.onDataReload(); 525 // Use restartLoader() to make LoaderManager to load the section again. 526 getLoaderManager().restartLoader(LOADER_ID_ALL_CONTACTS, null, mAllContactsLoaderListener); 527 } 528 529 private void updateFilterHeaderView() { 530 final ContactListFilter filter = getFilter(); 531 if (mAccountFilterHeader == null || mAllContactsAdapter == null || filter == null) { 532 return; 533 } 534 AccountFilterUtil.updateAccountFilterTitleForPhone(mAccountFilterHeader, filter, true); 535 } 536 537 public ContactListFilter getFilter() { 538 return mFilter; 539 } 540 541 public void setFilter(ContactListFilter filter) { 542 if ((mFilter == null && filter == null) || (mFilter != null && mFilter.equals(filter))) { 543 return; 544 } 545 546 if (DEBUG) { 547 Log.d(TAG, "setFilter(). old filter (" + mFilter 548 + ") will be replaced with new filter (" + filter + ")"); 549 } 550 551 mFilter = filter; 552 553 if (mAllContactsAdapter != null) { 554 mAllContactsAdapter.setFilter(mFilter); 555 requestReloadAllContacts(); 556 updateFilterHeaderView(); 557 } 558 } 559 560 public void setListener(Listener listener) { 561 mListener = listener; 562 } 563 } 564