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