1 /* 2 * Copyright (C) 2013 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 17 package com.example.android.contactslist.ui; 18 19 import android.annotation.SuppressLint; 20 import android.annotation.TargetApi; 21 import android.app.Activity; 22 import android.app.SearchManager; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.res.AssetFileDescriptor; 26 import android.database.Cursor; 27 import android.graphics.Bitmap; 28 import android.net.Uri; 29 import android.os.Build; 30 import android.os.Bundle; 31 import android.provider.ContactsContract.Contacts; 32 import android.provider.ContactsContract.Contacts.Photo; 33 import android.support.v4.app.ListFragment; 34 import android.support.v4.app.LoaderManager; 35 import android.support.v4.content.CursorLoader; 36 import android.support.v4.content.Loader; 37 import android.support.v4.widget.CursorAdapter; 38 import android.text.SpannableString; 39 import android.text.TextUtils; 40 import android.text.style.TextAppearanceSpan; 41 import android.util.DisplayMetrics; 42 import android.util.Log; 43 import android.util.TypedValue; 44 import android.view.LayoutInflater; 45 import android.view.Menu; 46 import android.view.MenuInflater; 47 import android.view.MenuItem; 48 import android.view.View; 49 import android.view.ViewGroup; 50 import android.widget.AbsListView; 51 import android.widget.AdapterView; 52 import android.widget.AlphabetIndexer; 53 import android.widget.ListView; 54 import android.widget.QuickContactBadge; 55 import android.widget.SearchView; 56 import android.widget.SectionIndexer; 57 import android.widget.TextView; 58 59 import com.example.android.contactslist.BuildConfig; 60 import com.example.android.contactslist.R; 61 import com.example.android.contactslist.util.ImageLoader; 62 import com.example.android.contactslist.util.Utils; 63 64 import java.io.FileDescriptor; 65 import java.io.FileNotFoundException; 66 import java.io.IOException; 67 import java.util.Locale; 68 69 /** 70 * This fragment displays a list of contacts stored in the Contacts Provider. Each item in the list 71 * shows the contact's thumbnail photo and display name. On devices with large screens, this 72 * fragment's UI appears as part of a two-pane layout, along with the UI of 73 * {@link ContactDetailFragment}. On smaller screens, this fragment's UI appears as a single pane. 74 * 75 * This Fragment retrieves contacts based on a search string. If the user doesn't enter a search 76 * string, then the list contains all the contacts in the Contacts Provider. If the user enters a 77 * search string, then the list contains only those contacts whose data matches the string. The 78 * Contacts Provider itself controls the matching algorithm, which is a "substring" search: if the 79 * search string is a substring of any of the contacts data, then there is a match. 80 * 81 * On newer API platforms, the search is implemented in a SearchView in the ActionBar; as the user 82 * types the search string, the list automatically refreshes to display results ("type to filter"). 83 * On older platforms, the user must enter the full string and trigger the search. In response, the 84 * trigger starts a new Activity which loads a fresh instance of this fragment. The resulting UI 85 * displays the filtered list and disables the search feature to prevent furthering searching. 86 */ 87 public class ContactsListFragment extends ListFragment implements 88 AdapterView.OnItemClickListener, LoaderManager.LoaderCallbacks<Cursor> { 89 90 // Defines a tag for identifying log entries 91 private static final String TAG = "ContactsListFragment"; 92 93 // Bundle key for saving previously selected search result item 94 private static final String STATE_PREVIOUSLY_SELECTED_KEY = 95 "com.example.android.contactslist.ui.SELECTED_ITEM"; 96 97 private ContactsAdapter mAdapter; // The main query adapter 98 private ImageLoader mImageLoader; // Handles loading the contact image in a background thread 99 private String mSearchTerm; // Stores the current search query term 100 101 // Contact selected listener that allows the activity holding this fragment to be notified of 102 // a contact being selected 103 private OnContactsInteractionListener mOnContactSelectedListener; 104 105 // Stores the previously selected search item so that on a configuration change the same item 106 // can be reselected again 107 private int mPreviouslySelectedSearchItem = 0; 108 109 // Whether or not the search query has changed since the last time the loader was refreshed 110 private boolean mSearchQueryChanged; 111 112 // Whether or not this fragment is showing in a two-pane layout 113 private boolean mIsTwoPaneLayout; 114 115 // Whether or not this is a search result view of this fragment, only used on pre-honeycomb 116 // OS versions as search results are shown in-line via Action Bar search from honeycomb onward 117 private boolean mIsSearchResultView = false; 118 119 /** 120 * Fragments require an empty constructor. 121 */ ContactsListFragment()122 public ContactsListFragment() {} 123 124 /** 125 * In platform versions prior to Android 3.0, the ActionBar and SearchView are not supported, 126 * and the UI gets the search string from an EditText. However, the fragment doesn't allow 127 * another search when search results are already showing. This would confuse the user, because 128 * the resulting search would re-query the Contacts Provider instead of searching the listed 129 * results. This method sets the search query and also a boolean that tracks if this Fragment 130 * should be displayed as a search result view or not. 131 * 132 * @param query The contacts search query. 133 */ setSearchQuery(String query)134 public void setSearchQuery(String query) { 135 if (TextUtils.isEmpty(query)) { 136 mIsSearchResultView = false; 137 } else { 138 mSearchTerm = query; 139 mIsSearchResultView = true; 140 } 141 } 142 143 @Override onCreate(Bundle savedInstanceState)144 public void onCreate(Bundle savedInstanceState) { 145 super.onCreate(savedInstanceState); 146 147 // Check if this fragment is part of a two-pane set up or a single pane by reading a 148 // boolean from the application resource directories. This lets allows us to easily specify 149 // which screen sizes should use a two-pane layout by setting this boolean in the 150 // corresponding resource size-qualified directory. 151 mIsTwoPaneLayout = getResources().getBoolean(R.bool.has_two_panes); 152 153 // Let this fragment contribute menu items 154 setHasOptionsMenu(true); 155 156 // Create the main contacts adapter 157 mAdapter = new ContactsAdapter(getActivity()); 158 159 if (savedInstanceState != null) { 160 // If we're restoring state after this fragment was recreated then 161 // retrieve previous search term and previously selected search 162 // result. 163 mSearchTerm = savedInstanceState.getString(SearchManager.QUERY); 164 mPreviouslySelectedSearchItem = 165 savedInstanceState.getInt(STATE_PREVIOUSLY_SELECTED_KEY, 0); 166 } 167 168 /* 169 * An ImageLoader object loads and resizes an image in the background and binds it to the 170 * QuickContactBadge in each item layout of the ListView. ImageLoader implements memory 171 * caching for each image, which substantially improves refreshes of the ListView as the 172 * user scrolls through it. 173 * 174 * To learn more about downloading images asynchronously and caching the results, read the 175 * Android training class Displaying Bitmaps Efficiently. 176 * 177 * http://developer.android.com/training/displaying-bitmaps/ 178 */ 179 mImageLoader = new ImageLoader(getActivity(), getListPreferredItemHeight()) { 180 @Override 181 protected Bitmap processBitmap(Object data) { 182 // This gets called in a background thread and passed the data from 183 // ImageLoader.loadImage(). 184 return loadContactPhotoThumbnail((String) data, getImageSize()); 185 } 186 }; 187 188 // Set a placeholder loading image for the image loader 189 mImageLoader.setLoadingImage(R.drawable.ic_contact_picture_holo_light); 190 191 // Add a cache to the image loader 192 mImageLoader.addImageCache(getActivity().getSupportFragmentManager(), 0.1f); 193 } 194 195 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)196 public View onCreateView(LayoutInflater inflater, ViewGroup container, 197 Bundle savedInstanceState) { 198 // Inflate the list fragment layout 199 return inflater.inflate(R.layout.contact_list_fragment, container, false); 200 } 201 202 @Override onActivityCreated(Bundle savedInstanceState)203 public void onActivityCreated(Bundle savedInstanceState) { 204 super.onActivityCreated(savedInstanceState); 205 206 // Set up ListView, assign adapter and set some listeners. The adapter was previously 207 // created in onCreate(). 208 setListAdapter(mAdapter); 209 getListView().setOnItemClickListener(this); 210 getListView().setOnScrollListener(new AbsListView.OnScrollListener() { 211 @Override 212 public void onScrollStateChanged(AbsListView absListView, int scrollState) { 213 // Pause image loader to ensure smoother scrolling when flinging 214 if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) { 215 mImageLoader.setPauseWork(true); 216 } else { 217 mImageLoader.setPauseWork(false); 218 } 219 } 220 221 @Override 222 public void onScroll(AbsListView absListView, int i, int i1, int i2) {} 223 }); 224 225 if (mIsTwoPaneLayout) { 226 // In a two-pane layout, set choice mode to single as there will be two panes 227 // when an item in the ListView is selected it should remain highlighted while 228 // the content shows in the second pane. 229 getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE); 230 } 231 232 // If there's a previously selected search item from a saved state then don't bother 233 // initializing the loader as it will be restarted later when the query is populated into 234 // the action bar search view (see onQueryTextChange() in onCreateOptionsMenu()). 235 if (mPreviouslySelectedSearchItem == 0) { 236 // Initialize the loader, and create a loader identified by ContactsQuery.QUERY_ID 237 getLoaderManager().initLoader(ContactsQuery.QUERY_ID, null, this); 238 } 239 } 240 241 @Override onAttach(Activity activity)242 public void onAttach(Activity activity) { 243 super.onAttach(activity); 244 245 try { 246 // Assign callback listener which the holding activity must implement. This is used 247 // so that when a contact item is interacted with (selected by the user) the holding 248 // activity will be notified and can take further action such as populating the contact 249 // detail pane (if in multi-pane layout) or starting a new activity with the contact 250 // details (single pane layout). 251 mOnContactSelectedListener = (OnContactsInteractionListener) activity; 252 } catch (ClassCastException e) { 253 throw new ClassCastException(activity.toString() 254 + " must implement OnContactsInteractionListener"); 255 } 256 } 257 258 @Override onPause()259 public void onPause() { 260 super.onPause(); 261 262 // In the case onPause() is called during a fling the image loader is 263 // un-paused to let any remaining background work complete. 264 mImageLoader.setPauseWork(false); 265 } 266 267 @Override onItemClick(AdapterView<?> parent, View v, int position, long id)268 public void onItemClick(AdapterView<?> parent, View v, int position, long id) { 269 // Gets the Cursor object currently bound to the ListView 270 final Cursor cursor = mAdapter.getCursor(); 271 272 // Moves to the Cursor row corresponding to the ListView item that was clicked 273 cursor.moveToPosition(position); 274 275 // Creates a contact lookup Uri from contact ID and lookup_key 276 final Uri uri = Contacts.getLookupUri( 277 cursor.getLong(ContactsQuery.ID), 278 cursor.getString(ContactsQuery.LOOKUP_KEY)); 279 280 // Notifies the parent activity that the user selected a contact. In a two-pane layout, the 281 // parent activity loads a ContactDetailFragment that displays the details for the selected 282 // contact. In a single-pane layout, the parent activity starts a new activity that 283 // displays contact details in its own Fragment. 284 mOnContactSelectedListener.onContactSelected(uri); 285 286 // If two-pane layout sets the selected item to checked so it remains highlighted. In a 287 // single-pane layout a new activity is started so this is not needed. 288 if (mIsTwoPaneLayout) { 289 getListView().setItemChecked(position, true); 290 } 291 } 292 293 /** 294 * Called when ListView selection is cleared, for example 295 * when search mode is finished and the currently selected 296 * contact should no longer be selected. 297 */ onSelectionCleared()298 private void onSelectionCleared() { 299 // Uses callback to notify activity this contains this fragment 300 mOnContactSelectedListener.onSelectionCleared(); 301 302 // Clears currently checked item 303 getListView().clearChoices(); 304 } 305 306 // This method uses APIs from newer OS versions than the minimum that this app supports. This 307 // annotation tells Android lint that they are properly guarded so they won't run on older OS 308 // versions and can be ignored by lint. 309 @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 310 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)311 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 312 313 // Inflate the menu items 314 inflater.inflate(R.menu.contact_list_menu, menu); 315 // Locate the search item 316 MenuItem searchItem = menu.findItem(R.id.menu_search); 317 318 // In versions prior to Android 3.0, hides the search item to prevent additional 319 // searches. In Android 3.0 and later, searching is done via a SearchView in the ActionBar. 320 // Since the search doesn't create a new Activity to do the searching, the menu item 321 // doesn't need to be turned off. 322 if (mIsSearchResultView) { 323 searchItem.setVisible(false); 324 } 325 326 // In version 3.0 and later, sets up and configures the ActionBar SearchView 327 if (Utils.hasHoneycomb()) { 328 329 // Retrieves the system search manager service 330 final SearchManager searchManager = 331 (SearchManager) getActivity().getSystemService(Context.SEARCH_SERVICE); 332 333 // Retrieves the SearchView from the search menu item 334 final SearchView searchView = (SearchView) searchItem.getActionView(); 335 336 // Assign searchable info to SearchView 337 searchView.setSearchableInfo( 338 searchManager.getSearchableInfo(getActivity().getComponentName())); 339 340 // Set listeners for SearchView 341 searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { 342 @Override 343 public boolean onQueryTextSubmit(String queryText) { 344 // Nothing needs to happen when the user submits the search string 345 return true; 346 } 347 348 @Override 349 public boolean onQueryTextChange(String newText) { 350 // Called when the action bar search text has changed. Updates 351 // the search filter, and restarts the loader to do a new query 352 // using the new search string. 353 String newFilter = !TextUtils.isEmpty(newText) ? newText : null; 354 355 // Don't do anything if the filter is empty 356 if (mSearchTerm == null && newFilter == null) { 357 return true; 358 } 359 360 // Don't do anything if the new filter is the same as the current filter 361 if (mSearchTerm != null && mSearchTerm.equals(newFilter)) { 362 return true; 363 } 364 365 // Updates current filter to new filter 366 mSearchTerm = newFilter; 367 368 // Restarts the loader. This triggers onCreateLoader(), which builds the 369 // necessary content Uri from mSearchTerm. 370 mSearchQueryChanged = true; 371 getLoaderManager().restartLoader( 372 ContactsQuery.QUERY_ID, null, ContactsListFragment.this); 373 return true; 374 } 375 }); 376 377 if (Utils.hasICS()) { 378 // This listener added in ICS 379 searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { 380 @Override 381 public boolean onMenuItemActionExpand(MenuItem menuItem) { 382 // Nothing to do when the action item is expanded 383 return true; 384 } 385 386 @Override 387 public boolean onMenuItemActionCollapse(MenuItem menuItem) { 388 // When the user collapses the SearchView the current search string is 389 // cleared and the loader restarted. 390 if (!TextUtils.isEmpty(mSearchTerm)) { 391 onSelectionCleared(); 392 } 393 mSearchTerm = null; 394 getLoaderManager().restartLoader( 395 ContactsQuery.QUERY_ID, null, ContactsListFragment.this); 396 return true; 397 } 398 }); 399 } 400 401 if (mSearchTerm != null) { 402 // If search term is already set here then this fragment is 403 // being restored from a saved state and the search menu item 404 // needs to be expanded and populated again. 405 406 // Stores the search term (as it will be wiped out by 407 // onQueryTextChange() when the menu item is expanded). 408 final String savedSearchTerm = mSearchTerm; 409 410 // Expands the search menu item 411 if (Utils.hasICS()) { 412 searchItem.expandActionView(); 413 } 414 415 // Sets the SearchView to the previous search string 416 searchView.setQuery(savedSearchTerm, false); 417 } 418 } 419 } 420 421 @Override onSaveInstanceState(Bundle outState)422 public void onSaveInstanceState(Bundle outState) { 423 super.onSaveInstanceState(outState); 424 if (!TextUtils.isEmpty(mSearchTerm)) { 425 // Saves the current search string 426 outState.putString(SearchManager.QUERY, mSearchTerm); 427 428 // Saves the currently selected contact 429 outState.putInt(STATE_PREVIOUSLY_SELECTED_KEY, getListView().getCheckedItemPosition()); 430 } 431 } 432 433 @Override onOptionsItemSelected(MenuItem item)434 public boolean onOptionsItemSelected(MenuItem item) { 435 switch (item.getItemId()) { 436 // Sends a request to the People app to display the create contact screen 437 case R.id.menu_add_contact: 438 final Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI); 439 startActivity(intent); 440 break; 441 // For platforms earlier than Android 3.0, triggers the search activity 442 case R.id.menu_search: 443 if (!Utils.hasHoneycomb()) { 444 getActivity().onSearchRequested(); 445 } 446 break; 447 } 448 return super.onOptionsItemSelected(item); 449 } 450 451 @Override onCreateLoader(int id, Bundle args)452 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 453 454 // If this is the loader for finding contacts in the Contacts Provider 455 // (the only one supported) 456 if (id == ContactsQuery.QUERY_ID) { 457 Uri contentUri; 458 459 // There are two types of searches, one which displays all contacts and 460 // one which filters contacts by a search query. If mSearchTerm is set 461 // then a search query has been entered and the latter should be used. 462 463 if (mSearchTerm == null) { 464 // Since there's no search string, use the content URI that searches the entire 465 // Contacts table 466 contentUri = ContactsQuery.CONTENT_URI; 467 } else { 468 // Since there's a search string, use the special content Uri that searches the 469 // Contacts table. The URI consists of a base Uri and the search string. 470 contentUri = 471 Uri.withAppendedPath(ContactsQuery.FILTER_URI, Uri.encode(mSearchTerm)); 472 } 473 474 // Returns a new CursorLoader for querying the Contacts table. No arguments are used 475 // for the selection clause. The search string is either encoded onto the content URI, 476 // or no contacts search string is used. The other search criteria are constants. See 477 // the ContactsQuery interface. 478 return new CursorLoader(getActivity(), 479 contentUri, 480 ContactsQuery.PROJECTION, 481 ContactsQuery.SELECTION, 482 null, 483 ContactsQuery.SORT_ORDER); 484 } 485 486 Log.e(TAG, "onCreateLoader - incorrect ID provided (" + id + ")"); 487 return null; 488 } 489 490 @Override onLoadFinished(Loader<Cursor> loader, Cursor data)491 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 492 // This swaps the new cursor into the adapter. 493 if (loader.getId() == ContactsQuery.QUERY_ID) { 494 mAdapter.swapCursor(data); 495 496 // If this is a two-pane layout and there is a search query then 497 // there is some additional work to do around default selected 498 // search item. 499 if (mIsTwoPaneLayout && !TextUtils.isEmpty(mSearchTerm) && mSearchQueryChanged) { 500 // Selects the first item in results, unless this fragment has 501 // been restored from a saved state (like orientation change) 502 // in which case it selects the previously selected search item. 503 if (data != null && data.moveToPosition(mPreviouslySelectedSearchItem)) { 504 // Creates the content Uri for the previously selected contact by appending the 505 // contact's ID to the Contacts table content Uri 506 final Uri uri = Uri.withAppendedPath( 507 Contacts.CONTENT_URI, String.valueOf(data.getLong(ContactsQuery.ID))); 508 mOnContactSelectedListener.onContactSelected(uri); 509 getListView().setItemChecked(mPreviouslySelectedSearchItem, true); 510 } else { 511 // No results, clear selection. 512 onSelectionCleared(); 513 } 514 // Only restore from saved state one time. Next time fall back 515 // to selecting first item. If the fragment state is saved again 516 // then the currently selected item will once again be saved. 517 mPreviouslySelectedSearchItem = 0; 518 mSearchQueryChanged = false; 519 } 520 } 521 } 522 523 @Override onLoaderReset(Loader<Cursor> loader)524 public void onLoaderReset(Loader<Cursor> loader) { 525 if (loader.getId() == ContactsQuery.QUERY_ID) { 526 // When the loader is being reset, clear the cursor from the adapter. This allows the 527 // cursor resources to be freed. 528 mAdapter.swapCursor(null); 529 } 530 } 531 532 /** 533 * Gets the preferred height for each item in the ListView, in pixels, after accounting for 534 * screen density. ImageLoader uses this value to resize thumbnail images to match the ListView 535 * item height. 536 * 537 * @return The preferred height in pixels, based on the current theme. 538 */ getListPreferredItemHeight()539 private int getListPreferredItemHeight() { 540 final TypedValue typedValue = new TypedValue(); 541 542 // Resolve list item preferred height theme attribute into typedValue 543 getActivity().getTheme().resolveAttribute( 544 android.R.attr.listPreferredItemHeight, typedValue, true); 545 546 // Create a new DisplayMetrics object 547 final DisplayMetrics metrics = new android.util.DisplayMetrics(); 548 549 // Populate the DisplayMetrics 550 getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics); 551 552 // Return theme value based on DisplayMetrics 553 return (int) typedValue.getDimension(metrics); 554 } 555 556 /** 557 * Decodes and scales a contact's image from a file pointed to by a Uri in the contact's data, 558 * and returns the result as a Bitmap. The column that contains the Uri varies according to the 559 * platform version. 560 * 561 * @param photoData For platforms prior to Android 3.0, provide the Contact._ID column value. 562 * For Android 3.0 and later, provide the Contact.PHOTO_THUMBNAIL_URI value. 563 * @param imageSize The desired target width and height of the output image in pixels. 564 * @return A Bitmap containing the contact's image, resized to fit the provided image size. If 565 * no thumbnail exists, returns null. 566 */ loadContactPhotoThumbnail(String photoData, int imageSize)567 private Bitmap loadContactPhotoThumbnail(String photoData, int imageSize) { 568 569 // Ensures the Fragment is still added to an activity. As this method is called in a 570 // background thread, there's the possibility the Fragment is no longer attached and 571 // added to an activity. If so, no need to spend resources loading the contact photo. 572 if (!isAdded() || getActivity() == null) { 573 return null; 574 } 575 576 // Instantiates an AssetFileDescriptor. Given a content Uri pointing to an image file, the 577 // ContentResolver can return an AssetFileDescriptor for the file. 578 AssetFileDescriptor afd = null; 579 580 // This "try" block catches an Exception if the file descriptor returned from the Contacts 581 // Provider doesn't point to an existing file. 582 try { 583 Uri thumbUri; 584 // If Android 3.0 or later, converts the Uri passed as a string to a Uri object. 585 if (Utils.hasHoneycomb()) { 586 thumbUri = Uri.parse(photoData); 587 } else { 588 // For versions prior to Android 3.0, appends the string argument to the content 589 // Uri for the Contacts table. 590 final Uri contactUri = Uri.withAppendedPath(Contacts.CONTENT_URI, photoData); 591 592 // Appends the content Uri for the Contacts.Photo table to the previously 593 // constructed contact Uri to yield a content URI for the thumbnail image 594 thumbUri = Uri.withAppendedPath(contactUri, Photo.CONTENT_DIRECTORY); 595 } 596 // Retrieves a file descriptor from the Contacts Provider. To learn more about this 597 // feature, read the reference documentation for 598 // ContentResolver#openAssetFileDescriptor. 599 afd = getActivity().getContentResolver().openAssetFileDescriptor(thumbUri, "r"); 600 601 // Gets a FileDescriptor from the AssetFileDescriptor. A BitmapFactory object can 602 // decode the contents of a file pointed to by a FileDescriptor into a Bitmap. 603 FileDescriptor fileDescriptor = afd.getFileDescriptor(); 604 605 if (fileDescriptor != null) { 606 // Decodes a Bitmap from the image pointed to by the FileDescriptor, and scales it 607 // to the specified width and height 608 return ImageLoader.decodeSampledBitmapFromDescriptor( 609 fileDescriptor, imageSize, imageSize); 610 } 611 } catch (FileNotFoundException e) { 612 // If the file pointed to by the thumbnail URI doesn't exist, or the file can't be 613 // opened in "read" mode, ContentResolver.openAssetFileDescriptor throws a 614 // FileNotFoundException. 615 if (BuildConfig.DEBUG) { 616 Log.d(TAG, "Contact photo thumbnail not found for contact " + photoData 617 + ": " + e.toString()); 618 } 619 } finally { 620 // If an AssetFileDescriptor was returned, try to close it 621 if (afd != null) { 622 try { 623 afd.close(); 624 } catch (IOException e) { 625 // Closing a file descriptor might cause an IOException if the file is 626 // already closed. Nothing extra is needed to handle this. 627 } 628 } 629 } 630 631 // If the decoding failed, returns null 632 return null; 633 } 634 635 /** 636 * This is a subclass of CursorAdapter that supports binding Cursor columns to a view layout. 637 * If those items are part of search results, the search string is marked by highlighting the 638 * query text. An {@link AlphabetIndexer} is used to allow quicker navigation up and down the 639 * ListView. 640 */ 641 private class ContactsAdapter extends CursorAdapter implements SectionIndexer { 642 private LayoutInflater mInflater; // Stores the layout inflater 643 private AlphabetIndexer mAlphabetIndexer; // Stores the AlphabetIndexer instance 644 private TextAppearanceSpan highlightTextSpan; // Stores the highlight text appearance style 645 646 /** 647 * Instantiates a new Contacts Adapter. 648 * @param context A context that has access to the app's layout. 649 */ ContactsAdapter(Context context)650 public ContactsAdapter(Context context) { 651 super(context, null, 0); 652 653 // Stores inflater for use later 654 mInflater = LayoutInflater.from(context); 655 656 // Loads a string containing the English alphabet. To fully localize the app, provide a 657 // strings.xml file in res/values-<x> directories, where <x> is a locale. In the file, 658 // define a string with android:name="alphabet" and contents set to all of the 659 // alphabetic characters in the language in their proper sort order, in upper case if 660 // applicable. 661 final String alphabet = context.getString(R.string.alphabet); 662 663 // Instantiates a new AlphabetIndexer bound to the column used to sort contact names. 664 // The cursor is left null, because it has not yet been retrieved. 665 mAlphabetIndexer = new AlphabetIndexer(null, ContactsQuery.SORT_KEY, alphabet); 666 667 // Defines a span for highlighting the part of a display name that matches the search 668 // string 669 highlightTextSpan = new TextAppearanceSpan(getActivity(), R.style.searchTextHiglight); 670 } 671 672 /** 673 * Identifies the start of the search string in the display name column of a Cursor row. 674 * E.g. If displayName was "Adam" and search query (mSearchTerm) was "da" this would 675 * return 1. 676 * 677 * @param displayName The contact display name. 678 * @return The starting position of the search string in the display name, 0-based. The 679 * method returns -1 if the string is not found in the display name, or if the search 680 * string is empty or null. 681 */ indexOfSearchQuery(String displayName)682 private int indexOfSearchQuery(String displayName) { 683 if (!TextUtils.isEmpty(mSearchTerm)) { 684 return displayName.toLowerCase(Locale.getDefault()).indexOf( 685 mSearchTerm.toLowerCase(Locale.getDefault())); 686 } 687 return -1; 688 } 689 690 /** 691 * Overrides newView() to inflate the list item views. 692 */ 693 @Override newView(Context context, Cursor cursor, ViewGroup viewGroup)694 public View newView(Context context, Cursor cursor, ViewGroup viewGroup) { 695 // Inflates the list item layout. 696 final View itemLayout = 697 mInflater.inflate(R.layout.contact_list_item, viewGroup, false); 698 699 // Creates a new ViewHolder in which to store handles to each view resource. This 700 // allows bindView() to retrieve stored references instead of calling findViewById for 701 // each instance of the layout. 702 final ViewHolder holder = new ViewHolder(); 703 holder.text1 = (TextView) itemLayout.findViewById(android.R.id.text1); 704 holder.text2 = (TextView) itemLayout.findViewById(android.R.id.text2); 705 holder.icon = (QuickContactBadge) itemLayout.findViewById(android.R.id.icon); 706 707 // Stores the resourceHolder instance in itemLayout. This makes resourceHolder 708 // available to bindView and other methods that receive a handle to the item view. 709 itemLayout.setTag(holder); 710 711 // Returns the item layout view 712 return itemLayout; 713 } 714 715 /** 716 * Binds data from the Cursor to the provided view. 717 */ 718 @Override bindView(View view, Context context, Cursor cursor)719 public void bindView(View view, Context context, Cursor cursor) { 720 // Gets handles to individual view resources 721 final ViewHolder holder = (ViewHolder) view.getTag(); 722 723 // For Android 3.0 and later, gets the thumbnail image Uri from the current Cursor row. 724 // For platforms earlier than 3.0, this isn't necessary, because the thumbnail is 725 // generated from the other fields in the row. 726 final String photoUri = cursor.getString(ContactsQuery.PHOTO_THUMBNAIL_DATA); 727 728 final String displayName = cursor.getString(ContactsQuery.DISPLAY_NAME); 729 730 final int startIndex = indexOfSearchQuery(displayName); 731 732 if (startIndex == -1) { 733 // If the user didn't do a search, or the search string didn't match a display 734 // name, show the display name without highlighting 735 holder.text1.setText(displayName); 736 737 if (TextUtils.isEmpty(mSearchTerm)) { 738 // If the search search is empty, hide the second line of text 739 holder.text2.setVisibility(View.GONE); 740 } else { 741 // Shows a second line of text that indicates the search string matched 742 // something other than the display name 743 holder.text2.setVisibility(View.VISIBLE); 744 } 745 } else { 746 // If the search string matched the display name, applies a SpannableString to 747 // highlight the search string with the displayed display name 748 749 // Wraps the display name in the SpannableString 750 final SpannableString highlightedName = new SpannableString(displayName); 751 752 // Sets the span to start at the starting point of the match and end at "length" 753 // characters beyond the starting point 754 highlightedName.setSpan(highlightTextSpan, startIndex, 755 startIndex + mSearchTerm.length(), 0); 756 757 // Binds the SpannableString to the display name View object 758 holder.text1.setText(highlightedName); 759 760 // Since the search string matched the name, this hides the secondary message 761 holder.text2.setVisibility(View.GONE); 762 } 763 764 // Processes the QuickContactBadge. A QuickContactBadge first appears as a contact's 765 // thumbnail image with styling that indicates it can be touched for additional 766 // information. When the user clicks the image, the badge expands into a dialog box 767 // containing the contact's details and icons for the built-in apps that can handle 768 // each detail type. 769 770 // Generates the contact lookup Uri 771 final Uri contactUri = Contacts.getLookupUri( 772 cursor.getLong(ContactsQuery.ID), 773 cursor.getString(ContactsQuery.LOOKUP_KEY)); 774 775 // Binds the contact's lookup Uri to the QuickContactBadge 776 holder.icon.assignContactUri(contactUri); 777 778 // Loads the thumbnail image pointed to by photoUri into the QuickContactBadge in a 779 // background worker thread 780 mImageLoader.loadImage(photoUri, holder.icon); 781 } 782 783 /** 784 * Overrides swapCursor to move the new Cursor into the AlphabetIndex as well as the 785 * CursorAdapter. 786 */ 787 @Override swapCursor(Cursor newCursor)788 public Cursor swapCursor(Cursor newCursor) { 789 // Update the AlphabetIndexer with new cursor as well 790 mAlphabetIndexer.setCursor(newCursor); 791 return super.swapCursor(newCursor); 792 } 793 794 /** 795 * An override of getCount that simplifies accessing the Cursor. If the Cursor is null, 796 * getCount returns zero. As a result, no test for Cursor == null is needed. 797 */ 798 @Override getCount()799 public int getCount() { 800 if (getCursor() == null) { 801 return 0; 802 } 803 return super.getCount(); 804 } 805 806 /** 807 * Defines the SectionIndexer.getSections() interface. 808 */ 809 @Override getSections()810 public Object[] getSections() { 811 return mAlphabetIndexer.getSections(); 812 } 813 814 /** 815 * Defines the SectionIndexer.getPositionForSection() interface. 816 */ 817 @Override getPositionForSection(int i)818 public int getPositionForSection(int i) { 819 if (getCursor() == null) { 820 return 0; 821 } 822 return mAlphabetIndexer.getPositionForSection(i); 823 } 824 825 /** 826 * Defines the SectionIndexer.getSectionForPosition() interface. 827 */ 828 @Override getSectionForPosition(int i)829 public int getSectionForPosition(int i) { 830 if (getCursor() == null) { 831 return 0; 832 } 833 return mAlphabetIndexer.getSectionForPosition(i); 834 } 835 836 /** 837 * A class that defines fields for each resource ID in the list item layout. This allows 838 * ContactsAdapter.newView() to store the IDs once, when it inflates the layout, instead of 839 * calling findViewById in each iteration of bindView. 840 */ 841 private class ViewHolder { 842 TextView text1; 843 TextView text2; 844 QuickContactBadge icon; 845 } 846 } 847 848 /** 849 * This interface must be implemented by any activity that loads this fragment. When an 850 * interaction occurs, such as touching an item from the ListView, these callbacks will 851 * be invoked to communicate the event back to the activity. 852 */ 853 public interface OnContactsInteractionListener { 854 /** 855 * Called when a contact is selected from the ListView. 856 * @param contactUri The contact Uri. 857 */ onContactSelected(Uri contactUri)858 public void onContactSelected(Uri contactUri); 859 860 /** 861 * Called when the ListView selection is cleared like when 862 * a contact search is taking place or is finishing. 863 */ onSelectionCleared()864 public void onSelectionCleared(); 865 } 866 867 /** 868 * This interface defines constants for the Cursor and CursorLoader, based on constants defined 869 * in the {@link android.provider.ContactsContract.Contacts} class. 870 */ 871 public interface ContactsQuery { 872 873 // An identifier for the loader 874 final static int QUERY_ID = 1; 875 876 // A content URI for the Contacts table 877 final static Uri CONTENT_URI = Contacts.CONTENT_URI; 878 879 // The search/filter query Uri 880 final static Uri FILTER_URI = Contacts.CONTENT_FILTER_URI; 881 882 // The selection clause for the CursorLoader query. The search criteria defined here 883 // restrict results to contacts that have a display name and are linked to visible groups. 884 // Notice that the search on the string provided by the user is implemented by appending 885 // the search string to CONTENT_FILTER_URI. 886 @SuppressLint("InlinedApi") 887 final static String SELECTION = 888 (Utils.hasHoneycomb() ? Contacts.DISPLAY_NAME_PRIMARY : Contacts.DISPLAY_NAME) + 889 "<>''" + " AND " + Contacts.IN_VISIBLE_GROUP + "=1"; 890 891 // The desired sort order for the returned Cursor. In Android 3.0 and later, the primary 892 // sort key allows for localization. In earlier versions. use the display name as the sort 893 // key. 894 @SuppressLint("InlinedApi") 895 final static String SORT_ORDER = 896 Utils.hasHoneycomb() ? Contacts.SORT_KEY_PRIMARY : Contacts.DISPLAY_NAME; 897 898 // The projection for the CursorLoader query. This is a list of columns that the Contacts 899 // Provider should return in the Cursor. 900 @SuppressLint("InlinedApi") 901 final static String[] PROJECTION = { 902 903 // The contact's row id 904 Contacts._ID, 905 906 // A pointer to the contact that is guaranteed to be more permanent than _ID. Given 907 // a contact's current _ID value and LOOKUP_KEY, the Contacts Provider can generate 908 // a "permanent" contact URI. 909 Contacts.LOOKUP_KEY, 910 911 // In platform version 3.0 and later, the Contacts table contains 912 // DISPLAY_NAME_PRIMARY, which either contains the contact's displayable name or 913 // some other useful identifier such as an email address. This column isn't 914 // available in earlier versions of Android, so you must use Contacts.DISPLAY_NAME 915 // instead. 916 Utils.hasHoneycomb() ? Contacts.DISPLAY_NAME_PRIMARY : Contacts.DISPLAY_NAME, 917 918 // In Android 3.0 and later, the thumbnail image is pointed to by 919 // PHOTO_THUMBNAIL_URI. In earlier versions, there is no direct pointer; instead, 920 // you generate the pointer from the contact's ID value and constants defined in 921 // android.provider.ContactsContract.Contacts. 922 Utils.hasHoneycomb() ? Contacts.PHOTO_THUMBNAIL_URI : Contacts._ID, 923 924 // The sort order column for the returned Cursor, used by the AlphabetIndexer 925 SORT_ORDER, 926 }; 927 928 // The query column numbers which map to each value in the projection 929 final static int ID = 0; 930 final static int LOOKUP_KEY = 1; 931 final static int DISPLAY_NAME = 2; 932 final static int PHOTO_THUMBNAIL_DATA = 3; 933 final static int SORT_KEY = 4; 934 } 935 } 936