Home | History | Annotate | Download | only in ui
      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      */
    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      */
    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
    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
    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
    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
    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
    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
    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      */
    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
    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
    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
    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
    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
    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
    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      */
    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      */
    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          */
    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          */
    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
    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
    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
    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
    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
    810         public Object[] getSections() {
    811             return mAlphabetIndexer.getSections();
    812         }
    813 
    814         /**
    815          * Defines the SectionIndexer.getPositionForSection() interface.
    816          */
    817         @Override
    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
    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          */
    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          */
    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