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.content.ContentResolver;
22 import android.content.Intent;
23 import android.content.pm.PackageManager;
24 import android.content.res.AssetFileDescriptor;
25 import android.database.Cursor;
26 import android.graphics.Bitmap;
27 import android.net.Uri;
28 import android.os.Build;
29 import android.os.Bundle;
30 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
31 import android.provider.ContactsContract.Contacts;
32 import android.provider.ContactsContract.Contacts.Photo;
33 import android.provider.ContactsContract.Data;
34 import android.support.v4.app.Fragment;
35 import android.support.v4.app.LoaderManager;
36 import android.support.v4.content.CursorLoader;
37 import android.support.v4.content.Loader;
38 import android.util.DisplayMetrics;
39 import android.util.Log;
40 import android.view.LayoutInflater;
41 import android.view.Menu;
42 import android.view.MenuInflater;
43 import android.view.MenuItem;
44 import android.view.View;
45 import android.view.ViewGroup;
46 import android.widget.ImageButton;
47 import android.widget.ImageView;
48 import android.widget.LinearLayout;
49 import android.widget.TextView;
50 import android.widget.Toast;
51 
52 import com.example.android.contactslist.BuildConfig;
53 import com.example.android.contactslist.R;
54 import com.example.android.contactslist.util.ImageLoader;
55 import com.example.android.contactslist.util.Utils;
56 
57 import java.io.FileNotFoundException;
58 import java.io.IOException;
59 
60 /**
61  * This fragment displays details of a specific contact from the contacts provider. It shows the
62  * contact's display photo, name and all its mailing addresses. You can also modify this fragment
63  * to show other information, such as phone numbers, email addresses and so forth.
64  *
65  * This fragment appears full-screen in an activity on devices with small screen sizes, and as
66  * part of a two-pane layout on devices with larger screens, alongside the
67  * {@link ContactsListFragment}.
68  *
69  * To create an instance of this fragment, use the factory method
70  * {@link ContactDetailFragment#newInstance(android.net.Uri)}, passing as an argument the contact
71  * Uri for the contact you want to display.
72  */
73 public class ContactDetailFragment extends Fragment implements
74         LoaderManager.LoaderCallbacks<Cursor> {
75 
76     public static final String EXTRA_CONTACT_URI =
77             "com.example.android.contactslist.ui.EXTRA_CONTACT_URI";
78 
79     // Defines a tag for identifying log entries
80     private static final String TAG = "ContactDetailFragment";
81 
82     // The geo Uri scheme prefix, used with Intent.ACTION_VIEW to form a geographical address
83     // intent that will trigger available apps to handle viewing a location (such as Maps)
84     private static final String GEO_URI_SCHEME_PREFIX = "geo:0,0?q=";
85 
86     // Whether or not this fragment is showing in a two pane layout
87     private boolean mIsTwoPaneLayout;
88 
89     private Uri mContactUri; // Stores the contact Uri for this fragment instance
90     private ImageLoader mImageLoader; // Handles loading the contact image in a background thread
91 
92     // Used to store references to key views, layouts and menu items as these need to be updated
93     // in multiple methods throughout this class.
94     private ImageView mImageView;
95     private LinearLayout mDetailsLayout;
96     private TextView mEmptyView;
97     private TextView mContactName;
98     private MenuItem mEditContactMenuItem;
99 
100     /**
101      * Factory method to generate a new instance of the fragment given a contact Uri. A factory
102      * method is preferable to simply using the constructor as it handles creating the bundle and
103      * setting the bundle as an argument.
104      *
105      * @param contactUri The contact Uri to load
106      * @return A new instance of {@link ContactDetailFragment}
107      */
newInstance(Uri contactUri)108     public static ContactDetailFragment newInstance(Uri contactUri) {
109         // Create new instance of this fragment
110         final ContactDetailFragment fragment = new ContactDetailFragment();
111 
112         // Create and populate the args bundle
113         final Bundle args = new Bundle();
114         args.putParcelable(EXTRA_CONTACT_URI, contactUri);
115 
116         // Assign the args bundle to the new fragment
117         fragment.setArguments(args);
118 
119         // Return fragment
120         return fragment;
121     }
122 
123     /**
124      * Fragments require an empty constructor.
125      */
ContactDetailFragment()126     public ContactDetailFragment() {}
127 
128     /**
129      * Sets the contact that this Fragment displays, or clears the display if the contact argument
130      * is null. This will re-initialize all the views and start the queries to the system contacts
131      * provider to populate the contact information.
132      *
133      * @param contactLookupUri The contact lookup Uri to load and display in this fragment. Passing
134      *                         null is valid and the fragment will display a message that no
135      *                         contact is currently selected instead.
136      */
setContact(Uri contactLookupUri)137     public void setContact(Uri contactLookupUri) {
138 
139         // In version 3.0 and later, stores the provided contact lookup Uri in a class field. This
140         // Uri is then used at various points in this class to map to the provided contact.
141         if (Utils.hasHoneycomb()) {
142             mContactUri = contactLookupUri;
143         } else {
144             // For versions earlier than Android 3.0, stores a contact Uri that's constructed from
145             // contactLookupUri. Later on, the resulting Uri is combined with
146             // Contacts.Data.CONTENT_DIRECTORY to map to the provided contact. It's done
147             // differently for these earlier versions because Contacts.Data.CONTENT_DIRECTORY works
148             // differently for Android versions before 3.0.
149             mContactUri = Contacts.lookupContact(getActivity().getContentResolver(),
150                     contactLookupUri);
151         }
152 
153         // If the Uri contains data, load the contact's image and load contact details.
154         if (contactLookupUri != null) {
155             // Asynchronously loads the contact image
156             mImageLoader.loadImage(mContactUri, mImageView);
157 
158             // Shows the contact photo ImageView and hides the empty view
159             mImageView.setVisibility(View.VISIBLE);
160             mEmptyView.setVisibility(View.GONE);
161 
162             // Shows the edit contact action/menu item
163             if (mEditContactMenuItem != null) {
164                 mEditContactMenuItem.setVisible(true);
165             }
166 
167             // Starts two queries to to retrieve contact information from the Contacts Provider.
168             // restartLoader() is used instead of initLoader() as this method may be called
169             // multiple times.
170             getLoaderManager().restartLoader(ContactDetailQuery.QUERY_ID, null, this);
171             getLoaderManager().restartLoader(ContactAddressQuery.QUERY_ID, null, this);
172         } else {
173             // If contactLookupUri is null, then the method was called when no contact was selected
174             // in the contacts list. This should only happen in a two-pane layout when the user
175             // hasn't yet selected a contact. Don't display an image for the contact, and don't
176             // account for the view's space in the layout. Turn on the TextView that appears when
177             // the layout is empty, and set the contact name to the empty string. Turn off any menu
178             // items that are visible.
179             mImageView.setVisibility(View.GONE);
180             mEmptyView.setVisibility(View.VISIBLE);
181             mDetailsLayout.removeAllViews();
182             if (mContactName != null) {
183                 mContactName.setText("");
184             }
185             if (mEditContactMenuItem != null) {
186                 mEditContactMenuItem.setVisible(false);
187             }
188         }
189     }
190 
191     /**
192      * When the Fragment is first created, this callback is invoked. It initializes some key
193      * class fields.
194      */
195     @Override
onCreate(Bundle savedInstanceState)196     public void onCreate(Bundle savedInstanceState) {
197         super.onCreate(savedInstanceState);
198 
199         // Check if this fragment is part of a two pane set up or a single pane
200         mIsTwoPaneLayout = getResources().getBoolean(R.bool.has_two_panes);
201 
202         // Let this fragment contribute menu items
203         setHasOptionsMenu(true);
204 
205         /*
206          * The ImageLoader takes care of loading and resizing images asynchronously into the
207          * ImageView. More thorough sample code demonstrating background image loading as well as
208          * details on how it works can be found in the following Android Training class:
209          * http://developer.android.com/training/displaying-bitmaps/
210          */
211         mImageLoader = new ImageLoader(getActivity(), getLargestScreenDimension()) {
212             @Override
213             protected Bitmap processBitmap(Object data) {
214                 // This gets called in a background thread and passed the data from
215                 // ImageLoader.loadImage().
216                 return loadContactPhoto((Uri) data, getImageSize());
217 
218             }
219         };
220 
221         // Set a placeholder loading image for the image loader
222         mImageLoader.setLoadingImage(R.drawable.ic_contact_picture_180_holo_light);
223 
224         // Tell the image loader to set the image directly when it's finished loading
225         // rather than fading in
226         mImageLoader.setImageFadeIn(false);
227     }
228 
229     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)230     public View onCreateView(LayoutInflater inflater, ViewGroup container,
231             Bundle savedInstanceState) {
232 
233         // Inflates the main layout to be used by this fragment
234         final View detailView =
235                 inflater.inflate(R.layout.contact_detail_fragment, container, false);
236 
237         // Gets handles to view objects in the layout
238         mImageView = (ImageView) detailView.findViewById(R.id.contact_image);
239         mDetailsLayout = (LinearLayout) detailView.findViewById(R.id.contact_details_layout);
240         mEmptyView = (TextView) detailView.findViewById(android.R.id.empty);
241 
242         if (mIsTwoPaneLayout) {
243             // If this is a two pane view, the following code changes the visibility of the contact
244             // name in details. For a one-pane view, the contact name is displayed as a title.
245             mContactName = (TextView) detailView.findViewById(R.id.contact_name);
246             mContactName.setVisibility(View.VISIBLE);
247         }
248 
249         return detailView;
250     }
251 
252     @Override
onActivityCreated(Bundle savedInstanceState)253     public void onActivityCreated(Bundle savedInstanceState) {
254         super.onActivityCreated(savedInstanceState);
255         // If not being created from a previous state
256         if (savedInstanceState == null) {
257             // Sets the argument extra as the currently displayed contact
258             setContact(getArguments() != null ?
259                     (Uri) getArguments().getParcelable(EXTRA_CONTACT_URI) : null);
260         } else {
261             // If being recreated from a saved state, sets the contact from the incoming
262             // savedInstanceState Bundle
263             setContact((Uri) savedInstanceState.getParcelable(EXTRA_CONTACT_URI));
264         }
265     }
266 
267     /**
268      * When the Fragment is being saved in order to change activity state, save the
269      * currently-selected contact.
270      */
271     @Override
onSaveInstanceState(Bundle outState)272     public void onSaveInstanceState(Bundle outState) {
273         super.onSaveInstanceState(outState);
274         // Saves the contact Uri
275         outState.putParcelable(EXTRA_CONTACT_URI, mContactUri);
276     }
277 
278     @Override
onOptionsItemSelected(MenuItem item)279     public boolean onOptionsItemSelected(MenuItem item) {
280         switch (item.getItemId()) {
281             // When "edit" menu option selected
282             case R.id.menu_edit_contact:
283                 // Standard system edit contact intent
284                 Intent intent = new Intent(Intent.ACTION_EDIT, mContactUri);
285 
286                 // Because of an issue in Android 4.0 (API level 14), clicking Done or Back in the
287                 // People app doesn't return the user to your app; instead, it displays the People
288                 // app's contact list. A workaround, introduced in Android 4.0.3 (API level 15) is
289                 // to set a special flag in the extended data for the Intent you send to the People
290                 // app. The issue is does not appear in versions prior to Android 4.0. You can use
291                 // the flag with any version of the People app; if the workaround isn't needed,
292                 // the flag is ignored.
293                 intent.putExtra("finishActivityOnSaveCompleted", true);
294 
295                 // Start the edit activity
296                 startActivity(intent);
297                 return true;
298         }
299         return super.onOptionsItemSelected(item);
300     }
301 
302     @Override
onCreateOptionsMenu(Menu menu, MenuInflater inflater)303     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
304         super.onCreateOptionsMenu(menu, inflater);
305 
306         // Inflates the options menu for this fragment
307         inflater.inflate(R.menu.contact_detail_menu, menu);
308 
309         // Gets a handle to the "find" menu item
310         mEditContactMenuItem = menu.findItem(R.id.menu_edit_contact);
311 
312         // If contactUri is null the edit menu item should be hidden, otherwise
313         // it is visible.
314         mEditContactMenuItem.setVisible(mContactUri != null);
315     }
316 
317     @Override
onCreateLoader(int id, Bundle args)318     public Loader<Cursor> onCreateLoader(int id, Bundle args) {
319         switch (id) {
320             // Two main queries to load the required information
321             case ContactDetailQuery.QUERY_ID:
322                 // This query loads main contact details, see
323                 // ContactDetailQuery for more information.
324                 return new CursorLoader(getActivity(), mContactUri,
325                         ContactDetailQuery.PROJECTION,
326                         null, null, null);
327             case ContactAddressQuery.QUERY_ID:
328                 // This query loads contact address details, see
329                 // ContactAddressQuery for more information.
330                 final Uri uri = Uri.withAppendedPath(mContactUri, Contacts.Data.CONTENT_DIRECTORY);
331                 return new CursorLoader(getActivity(), uri,
332                         ContactAddressQuery.PROJECTION,
333                         ContactAddressQuery.SELECTION,
334                         null, null);
335         }
336         return null;
337     }
338 
339     @Override
onLoadFinished(Loader<Cursor> loader, Cursor data)340     public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
341 
342         // If this fragment was cleared while the query was running
343         // eg. from from a call like setContact(uri) then don't do
344         // anything.
345         if (mContactUri == null) {
346             return;
347         }
348 
349         switch (loader.getId()) {
350             case ContactDetailQuery.QUERY_ID:
351                 // Moves to the first row in the Cursor
352                 if (data.moveToFirst()) {
353                     // For the contact details query, fetches the contact display name.
354                     // ContactDetailQuery.DISPLAY_NAME maps to the appropriate display
355                     // name field based on OS version.
356                     final String contactName = data.getString(ContactDetailQuery.DISPLAY_NAME);
357                     if (mIsTwoPaneLayout && mContactName != null) {
358                         // In the two pane layout, there is a dedicated TextView
359                         // that holds the contact name.
360                         mContactName.setText(contactName);
361                     } else {
362                         // In the single pane layout, sets the activity title
363                         // to the contact name. On HC+ this will be set as
364                         // the ActionBar title text.
365                         getActivity().setTitle(contactName);
366                     }
367                 }
368                 break;
369             case ContactAddressQuery.QUERY_ID:
370                 // This query loads the contact address details. More than
371                 // one contact address is possible, so move each one to a
372                 // LinearLayout in a Scrollview so multiple addresses can
373                 // be scrolled by the user.
374 
375                 // Each LinearLayout has the same LayoutParams so this can
376                 // be created once and used for each address.
377                 final LinearLayout.LayoutParams layoutParams =
378                         new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
379                                 ViewGroup.LayoutParams.WRAP_CONTENT);
380 
381                 // Clears out the details layout first in case the details
382                 // layout has addresses from a previous data load still
383                 // added as children.
384                 mDetailsLayout.removeAllViews();
385 
386                 // Loops through all the rows in the Cursor
387                 if (data.moveToFirst()) {
388                     do {
389                         // Builds the address layout
390                         final LinearLayout layout = buildAddressLayout(
391                                 data.getInt(ContactAddressQuery.TYPE),
392                                 data.getString(ContactAddressQuery.LABEL),
393                                 data.getString(ContactAddressQuery.ADDRESS));
394                         // Adds the new address layout to the details layout
395                         mDetailsLayout.addView(layout, layoutParams);
396                     } while (data.moveToNext());
397                 } else {
398                     // If nothing found, adds an empty address layout
399                     mDetailsLayout.addView(buildEmptyAddressLayout(), layoutParams);
400                 }
401                 break;
402         }
403     }
404 
405     @Override
onLoaderReset(Loader<Cursor> loader)406     public void onLoaderReset(Loader<Cursor> loader) {
407         // Nothing to do here. The Cursor does not need to be released as it was never directly
408         // bound to anything (like an adapter).
409     }
410 
411     /**
412      * Builds an empty address layout that just shows that no addresses
413      * were found for this contact.
414      *
415      * @return A LinearLayout to add to the contact details layout
416      */
buildEmptyAddressLayout()417     private LinearLayout buildEmptyAddressLayout() {
418         return buildAddressLayout(0, null, null);
419     }
420 
421     /**
422      * Builds an address LinearLayout based on address information from the Contacts Provider.
423      * Each address for the contact gets its own LinearLayout object; for example, if the contact
424      * has three postal addresses, then 3 LinearLayouts are generated.
425      *
426      * @param addressType From
427      * {@link android.provider.ContactsContract.CommonDataKinds.StructuredPostal#TYPE}
428      * @param addressTypeLabel From
429      * {@link android.provider.ContactsContract.CommonDataKinds.StructuredPostal#LABEL}
430      * @param address From
431      * {@link android.provider.ContactsContract.CommonDataKinds.StructuredPostal#FORMATTED_ADDRESS}
432      * @return A LinearLayout to add to the contact details layout,
433      *         populated with the provided address details.
434      */
buildAddressLayout(int addressType, String addressTypeLabel, final String address)435     private LinearLayout buildAddressLayout(int addressType, String addressTypeLabel,
436             final String address) {
437 
438         // Inflates the address layout
439         final LinearLayout addressLayout =
440                 (LinearLayout) LayoutInflater.from(getActivity()).inflate(
441                         R.layout.contact_detail_item, mDetailsLayout, false);
442 
443         // Gets handles to the view objects in the layout
444         final TextView headerTextView =
445                 (TextView) addressLayout.findViewById(R.id.contact_detail_header);
446         final TextView addressTextView =
447                 (TextView) addressLayout.findViewById(R.id.contact_detail_item);
448         final ImageButton viewAddressButton =
449                 (ImageButton) addressLayout.findViewById(R.id.button_view_address);
450 
451         // If there's no addresses for the contact, shows the empty view and message, and hides the
452         // header and button.
453         if (addressTypeLabel == null && addressType == 0) {
454             headerTextView.setVisibility(View.GONE);
455             viewAddressButton.setVisibility(View.GONE);
456             addressTextView.setText(R.string.no_address);
457         } else {
458             // Gets postal address label type
459             CharSequence label =
460                     StructuredPostal.getTypeLabel(getResources(), addressType, addressTypeLabel);
461 
462             // Sets TextView objects in the layout
463             headerTextView.setText(label);
464             addressTextView.setText(address);
465 
466             // Defines an onClickListener object for the address button
467             viewAddressButton.setOnClickListener(new View.OnClickListener() {
468                 // Defines what to do when users click the address button
469                 @Override
470                 public void onClick(View view) {
471 
472                     final Intent viewIntent =
473                             new Intent(Intent.ACTION_VIEW, constructGeoUri(address));
474 
475                     // A PackageManager instance is needed to verify that there's a default app
476                     // that handles ACTION_VIEW and a geo Uri.
477                     final PackageManager packageManager = getActivity().getPackageManager();
478 
479                     // Checks for an activity that can handle this intent. Preferred in this
480                     // case over Intent.createChooser() as it will still let the user choose
481                     // a default (or use a previously set default) for geo Uris.
482                     if (packageManager.resolveActivity(
483                             viewIntent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
484                         startActivity(viewIntent);
485                     } else {
486                         // If no default is found, displays a message that no activity can handle
487                         // the view button.
488                         Toast.makeText(getActivity(),
489                                 R.string.no_intent_found, Toast.LENGTH_SHORT).show();
490                     }
491                 }
492             });
493 
494         }
495         return addressLayout;
496     }
497 
498     /**
499      * Constructs a geo scheme Uri from a postal address.
500      *
501      * @param postalAddress A postal address.
502      * @return the geo:// Uri for the postal address.
503      */
constructGeoUri(String postalAddress)504     private Uri constructGeoUri(String postalAddress) {
505         // Concatenates the geo:// prefix to the postal address. The postal address must be
506         // converted to Uri format and encoded for special characters.
507         return Uri.parse(GEO_URI_SCHEME_PREFIX + Uri.encode(postalAddress));
508     }
509 
510     /**
511      * Fetches the width or height of the screen in pixels, whichever is larger. This is used to
512      * set a maximum size limit on the contact photo that is retrieved from the Contacts Provider.
513      * This limit prevents the app from trying to decode and load an image that is much larger than
514      * the available screen area.
515      *
516      * @return The largest screen dimension in pixels.
517      */
getLargestScreenDimension()518     private int getLargestScreenDimension() {
519         // Gets a DisplayMetrics object, which is used to retrieve the display's pixel height and
520         // width
521         final DisplayMetrics displayMetrics = new DisplayMetrics();
522 
523         // Retrieves a displayMetrics object for the device's default display
524         getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
525         final int height = displayMetrics.heightPixels;
526         final int width = displayMetrics.widthPixels;
527 
528         // Returns the larger of the two values
529         return height > width ? height : width;
530     }
531 
532     /**
533      * Decodes and returns the contact's thumbnail image.
534      * @param contactUri The Uri of the contact containing the image.
535      * @param imageSize The desired target width and height of the output image in pixels.
536      * @return If a thumbnail image exists for the contact, a Bitmap image, otherwise null.
537      */
538     @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
loadContactPhoto(Uri contactUri, int imageSize)539     private Bitmap loadContactPhoto(Uri contactUri, int imageSize) {
540 
541         // Ensures the Fragment is still added to an activity. As this method is called in a
542         // background thread, there's the possibility the Fragment is no longer attached and
543         // added to an activity. If so, no need to spend resources loading the contact photo.
544         if (!isAdded() || getActivity() == null) {
545             return null;
546         }
547 
548         // Instantiates a ContentResolver for retrieving the Uri of the image
549         final ContentResolver contentResolver = getActivity().getContentResolver();
550 
551         // Instantiates an AssetFileDescriptor. Given a content Uri pointing to an image file, the
552         // ContentResolver can return an AssetFileDescriptor for the file.
553         AssetFileDescriptor afd = null;
554 
555         if (Utils.hasICS()) {
556             // On platforms running Android 4.0 (API version 14) and later, a high resolution image
557             // is available from Photo.DISPLAY_PHOTO.
558             try {
559                 // Constructs the content Uri for the image
560                 Uri displayImageUri = Uri.withAppendedPath(contactUri, Photo.DISPLAY_PHOTO);
561 
562                 // Retrieves an AssetFileDescriptor from the Contacts Provider, using the
563                 // constructed Uri
564                 afd = contentResolver.openAssetFileDescriptor(displayImageUri, "r");
565                 // If the file exists
566                 if (afd != null) {
567                     // Reads and decodes the file to a Bitmap and scales it to the desired size
568                     return ImageLoader.decodeSampledBitmapFromDescriptor(
569                             afd.getFileDescriptor(), imageSize, imageSize);
570                 }
571             } catch (FileNotFoundException e) {
572                 // Catches file not found exceptions
573                 if (BuildConfig.DEBUG) {
574                     // Log debug message, this is not an error message as this exception is thrown
575                     // when a contact is legitimately missing a contact photo (which will be quite
576                     // frequently in a long contacts list).
577                     Log.d(TAG, "Contact photo not found for contact " + contactUri.toString()
578                             + ": " + e.toString());
579                 }
580             } finally {
581                 // Once the decode is complete, this closes the file. You must do this each time
582                 // you access an AssetFileDescriptor; otherwise, every image load you do will open
583                 // a new descriptor.
584                 if (afd != null) {
585                     try {
586                         afd.close();
587                     } catch (IOException e) {
588                         // Closing a file descriptor might cause an IOException if the file is
589                         // already closed. Nothing extra is needed to handle this.
590                     }
591                 }
592             }
593         }
594 
595         // If the platform version is less than Android 4.0 (API Level 14), use the only available
596         // image URI, which points to a normal-sized image.
597         try {
598             // Constructs the image Uri from the contact Uri and the directory twig from the
599             // Contacts.Photo table
600             Uri imageUri = Uri.withAppendedPath(contactUri, Photo.CONTENT_DIRECTORY);
601 
602             // Retrieves an AssetFileDescriptor from the Contacts Provider, using the constructed
603             // Uri
604             afd = getActivity().getContentResolver().openAssetFileDescriptor(imageUri, "r");
605 
606             // If the file exists
607             if (afd != null) {
608                 // Reads the image from the file, decodes it, and scales it to the available screen
609                 // area
610                 return ImageLoader.decodeSampledBitmapFromDescriptor(
611                         afd.getFileDescriptor(), imageSize, imageSize);
612             }
613         } catch (FileNotFoundException e) {
614             // Catches file not found exceptions
615             if (BuildConfig.DEBUG) {
616                 // Log debug message, this is not an error message as this exception is thrown
617                 // when a contact is legitimately missing a contact photo (which will be quite
618                 // frequently in a long contacts list).
619                 Log.d(TAG, "Contact photo not found for contact " + contactUri.toString()
620                         + ": " + e.toString());
621             }
622         } finally {
623             // Once the decode is complete, this closes the file. You must do this each time you
624             // access an AssetFileDescriptor; otherwise, every image load you do will open a new
625             // descriptor.
626             if (afd != null) {
627                 try {
628                     afd.close();
629                 } catch (IOException e) {
630                     // Closing a file descriptor might cause an IOException if the file is
631                     // already closed. Ignore this.
632                 }
633             }
634         }
635 
636         // If none of the case selectors match, returns null.
637         return null;
638     }
639 
640     /**
641      * This interface defines constants used by contact retrieval queries.
642      */
643     public interface ContactDetailQuery {
644         // A unique query ID to distinguish queries being run by the
645         // LoaderManager.
646         final static int QUERY_ID = 1;
647 
648         // The query projection (columns to fetch from the provider)
649         @SuppressLint("InlinedApi")
650         final static String[] PROJECTION = {
651                 Contacts._ID,
652                 Utils.hasHoneycomb() ? Contacts.DISPLAY_NAME_PRIMARY : Contacts.DISPLAY_NAME,
653         };
654 
655         // The query column numbers which map to each value in the projection
656         final static int ID = 0;
657         final static int DISPLAY_NAME = 1;
658     }
659 
660     /**
661      * This interface defines constants used by address retrieval queries.
662      */
663     public interface ContactAddressQuery {
664         // A unique query ID to distinguish queries being run by the
665         // LoaderManager.
666         final static int QUERY_ID = 2;
667 
668         // The query projection (columns to fetch from the provider)
669         final static String[] PROJECTION = {
670                 StructuredPostal._ID,
671                 StructuredPostal.FORMATTED_ADDRESS,
672                 StructuredPostal.TYPE,
673                 StructuredPostal.LABEL,
674         };
675 
676         // The query selection criteria. In this case matching against the
677         // StructuredPostal content mime type.
678         final static String SELECTION =
679                 Data.MIMETYPE + "='" + StructuredPostal.CONTENT_ITEM_TYPE + "'";
680 
681         // The query column numbers which map to each value in the projection
682         final static int ID = 0;
683         final static int ADDRESS = 1;
684         final static int TYPE = 2;
685         final static int LABEL = 3;
686     }
687 }
688