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.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      */
    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      */
    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      */
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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      */
    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      */
    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      */
    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      */
    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)
    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