Home | History | Annotate | Download | only in loaderapp
      1 /*
      2  * Copyright (C) 2009 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.android.loaderapp;
     18 
     19 import android.Manifest;
     20 import android.content.AsyncQueryHandler;
     21 import android.content.ContentResolver;
     22 import android.content.ContentUris;
     23 import android.content.ContentValues;
     24 import android.content.Context;
     25 import android.content.pm.PackageManager;
     26 import android.content.pm.PackageManager.NameNotFoundException;
     27 import android.content.res.Resources;
     28 import android.content.res.Resources.NotFoundException;
     29 import android.database.Cursor;
     30 import android.graphics.Bitmap;
     31 import android.graphics.BitmapFactory;
     32 import android.net.Uri;
     33 import android.os.SystemClock;
     34 import android.provider.ContactsContract.Contacts;
     35 import android.provider.ContactsContract.Data;
     36 import android.provider.ContactsContract.PhoneLookup;
     37 import android.provider.ContactsContract.RawContacts;
     38 import android.provider.ContactsContract.StatusUpdates;
     39 import android.provider.ContactsContract.CommonDataKinds.Email;
     40 import android.provider.ContactsContract.CommonDataKinds.Photo;
     41 import android.text.TextUtils;
     42 import android.text.format.DateUtils;
     43 import android.util.AttributeSet;
     44 import android.util.Log;
     45 import android.view.LayoutInflater;
     46 import android.view.View;
     47 import android.widget.CheckBox;
     48 import android.widget.QuickContactBadge;
     49 import android.widget.FrameLayout;
     50 import android.widget.ImageView;
     51 import android.widget.TextView;
     52 
     53 /**
     54  * Header used across system for displaying a title bar with contact info. You
     55  * can bind specific values on the header, or use helper methods like
     56  * {@link #bindFromContactLookupUri(Uri)} to populate asynchronously.
     57  * <p>
     58  * The parent must request the {@link Manifest.permission#READ_CONTACTS}
     59  * permission to access contact data.
     60  */
     61 public class ContactHeaderWidget extends FrameLayout implements View.OnClickListener {
     62 
     63     private static final String TAG = "ContactHeaderWidget";
     64 
     65     private TextView mDisplayNameView;
     66     private View mAggregateBadge;
     67     private TextView mPhoneticNameView;
     68     private CheckBox mStarredView;
     69     private QuickContactBadge mPhotoView;
     70     private ImageView mPresenceView;
     71     private TextView mStatusView;
     72     private TextView mStatusAttributionView;
     73     private int mNoPhotoResource;
     74     private QueryHandler mQueryHandler;
     75 
     76     protected Uri mContactUri;
     77 
     78     protected String[] mExcludeMimes = null;
     79 
     80     protected ContentResolver mContentResolver;
     81 
     82     /**
     83      * Interface for callbacks invoked when the user interacts with a header.
     84      */
     85     public interface ContactHeaderListener {
     86         public void onPhotoClick(View view);
     87         public void onDisplayNameClick(View view);
     88     }
     89 
     90     private ContactHeaderListener mListener;
     91 
     92 
     93     private interface ContactQuery {
     94         //Projection used for the summary info in the header.
     95         String[] COLUMNS = new String[] {
     96             Contacts._ID,
     97             Contacts.LOOKUP_KEY,
     98             Contacts.PHOTO_ID,
     99             Contacts.DISPLAY_NAME,
    100             Contacts.PHONETIC_NAME,
    101             Contacts.STARRED,
    102             Contacts.CONTACT_PRESENCE,
    103             Contacts.CONTACT_STATUS,
    104             Contacts.CONTACT_STATUS_TIMESTAMP,
    105             Contacts.CONTACT_STATUS_RES_PACKAGE,
    106             Contacts.CONTACT_STATUS_LABEL,
    107         };
    108         int _ID = 0;
    109         int LOOKUP_KEY = 1;
    110         int PHOTO_ID = 2;
    111         int DISPLAY_NAME = 3;
    112         int PHONETIC_NAME = 4;
    113         //TODO: We need to figure out how we're going to get the phonetic name.
    114         //static final int HEADER_PHONETIC_NAME_COLUMN_INDEX
    115         int STARRED = 5;
    116         int CONTACT_PRESENCE_STATUS = 6;
    117         int CONTACT_STATUS = 7;
    118         int CONTACT_STATUS_TIMESTAMP = 8;
    119         int CONTACT_STATUS_RES_PACKAGE = 9;
    120         int CONTACT_STATUS_LABEL = 10;
    121     }
    122 
    123     private interface PhotoQuery {
    124         String[] COLUMNS = new String[] {
    125             Photo.PHOTO
    126         };
    127 
    128         int PHOTO = 0;
    129     }
    130 
    131     //Projection used for looking up contact id from phone number
    132     protected static final String[] PHONE_LOOKUP_PROJECTION = new String[] {
    133         PhoneLookup._ID,
    134         PhoneLookup.LOOKUP_KEY,
    135     };
    136     protected static final int PHONE_LOOKUP_CONTACT_ID_COLUMN_INDEX = 0;
    137     protected static final int PHONE_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX = 1;
    138 
    139     //Projection used for looking up contact id from email address
    140     protected static final String[] EMAIL_LOOKUP_PROJECTION = new String[] {
    141         RawContacts.CONTACT_ID,
    142         Contacts.LOOKUP_KEY,
    143     };
    144     protected static final int EMAIL_LOOKUP_CONTACT_ID_COLUMN_INDEX = 0;
    145     protected static final int EMAIL_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX = 1;
    146 
    147     protected static final String[] CONTACT_LOOKUP_PROJECTION = new String[] {
    148         Contacts._ID,
    149     };
    150     protected static final int CONTACT_LOOKUP_ID_COLUMN_INDEX = 0;
    151 
    152     private static final int TOKEN_CONTACT_INFO = 0;
    153     private static final int TOKEN_PHONE_LOOKUP = 1;
    154     private static final int TOKEN_EMAIL_LOOKUP = 2;
    155     private static final int TOKEN_PHOTO_QUERY = 3;
    156 
    157     public ContactHeaderWidget(Context context) {
    158         this(context, null);
    159     }
    160 
    161     public ContactHeaderWidget(Context context, AttributeSet attrs) {
    162         this(context, attrs, 0);
    163     }
    164 
    165     public ContactHeaderWidget(Context context, AttributeSet attrs, int defStyle) {
    166         super(context, attrs, defStyle);
    167 
    168         mContentResolver = mContext.getContentResolver();
    169 
    170         LayoutInflater inflater =
    171             (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    172         inflater.inflate(R.layout.contact_header, this);
    173 
    174         mDisplayNameView = (TextView) findViewById(R.id.name);
    175 
    176         mPhoneticNameView = (TextView) findViewById(R.id.phonetic_name);
    177 
    178         mPhotoView = (QuickContactBadge) findViewById(R.id.photo);
    179 
    180         mPresenceView = (ImageView) findViewById(R.id.presence);
    181 
    182         mStatusView = (TextView)findViewById(R.id.status);
    183         mStatusAttributionView = (TextView)findViewById(R.id.status_date);
    184 
    185         // Set the photo with a random "no contact" image
    186         long now = SystemClock.elapsedRealtime();
    187         int num = (int) now & 0xf;
    188         if (num < 9) {
    189             // Leaning in from right, common
    190             mNoPhotoResource = R.drawable.ic_contact_picture;
    191         } else if (num < 14) {
    192             // Leaning in from left uncommon
    193             mNoPhotoResource = R.drawable.ic_contact_picture_2;
    194         } else {
    195             // Coming in from the top, rare
    196             mNoPhotoResource = R.drawable.ic_contact_picture_3;
    197         }
    198 
    199         resetAsyncQueryHandler();
    200     }
    201 
    202     public void enableClickListeners() {
    203         mDisplayNameView.setOnClickListener(this);
    204         mPhotoView.setOnClickListener(this);
    205     }
    206 
    207     /**
    208      * Set the given {@link ContactHeaderListener} to handle header events.
    209      */
    210     public void setContactHeaderListener(ContactHeaderListener listener) {
    211         mListener = listener;
    212     }
    213 
    214     private void performPhotoClick() {
    215         if (mListener != null) {
    216             mListener.onPhotoClick(mPhotoView);
    217         }
    218     }
    219 
    220     private void performDisplayNameClick() {
    221         if (mListener != null) {
    222             mListener.onDisplayNameClick(mDisplayNameView);
    223         }
    224     }
    225 
    226     private class QueryHandler extends AsyncQueryHandler {
    227 
    228         public QueryHandler(ContentResolver cr) {
    229             super(cr);
    230         }
    231 
    232         @Override
    233         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
    234             try{
    235                 if (this != mQueryHandler) {
    236                     Log.d(TAG, "onQueryComplete: discard result, the query handler is reset!");
    237                     return;
    238                 }
    239 
    240                 switch (token) {
    241                     case TOKEN_PHOTO_QUERY: {
    242                         //Set the photo
    243                         Bitmap photoBitmap = null;
    244                         if (cursor != null && cursor.moveToFirst()
    245                                 && !cursor.isNull(PhotoQuery.PHOTO)) {
    246                             byte[] photoData = cursor.getBlob(PhotoQuery.PHOTO);
    247                             photoBitmap = BitmapFactory.decodeByteArray(photoData, 0,
    248                                     photoData.length, null);
    249                         }
    250 
    251                         if (photoBitmap == null) {
    252                             photoBitmap = loadPlaceholderPhoto(null);
    253                         }
    254                         setPhoto(photoBitmap);
    255                         if (cookie != null && cookie instanceof Uri) {
    256                             mPhotoView.assignContactUri((Uri) cookie);
    257                         }
    258                         invalidate();
    259                         break;
    260                     }
    261                     case TOKEN_CONTACT_INFO: {
    262                         if (cursor != null && cursor.moveToFirst()) {
    263                             bindContactInfo(cursor);
    264                             final Uri lookupUri = Contacts.getLookupUri(
    265                                     cursor.getLong(ContactQuery._ID),
    266                                     cursor.getString(ContactQuery.LOOKUP_KEY));
    267 
    268                             final long photoId = cursor.getLong(ContactQuery.PHOTO_ID);
    269 
    270                             setPhotoId(photoId, lookupUri);
    271                         } else {
    272                             // shouldn't really happen
    273                             setDisplayName(null, null);
    274                             setSocialSnippet(null);
    275                             setPhoto(loadPlaceholderPhoto(null));
    276                         }
    277                         break;
    278                     }
    279                     case TOKEN_PHONE_LOOKUP: {
    280                         if (cursor != null && cursor.moveToFirst()) {
    281                             long contactId = cursor.getLong(PHONE_LOOKUP_CONTACT_ID_COLUMN_INDEX);
    282                             String lookupKey = cursor.getString(
    283                                     PHONE_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX);
    284                             bindFromContactUriInternal(Contacts.getLookupUri(contactId, lookupKey),
    285                                     false /* don't reset query handler */);
    286                         } else {
    287                             String phoneNumber = (String) cookie;
    288                             setDisplayName(phoneNumber, null);
    289                             setSocialSnippet(null);
    290                             setPhoto(loadPlaceholderPhoto(null));
    291                             mPhotoView.assignContactFromPhone(phoneNumber, true);
    292                         }
    293                         break;
    294                     }
    295                     case TOKEN_EMAIL_LOOKUP: {
    296                         if (cursor != null && cursor.moveToFirst()) {
    297                             long contactId = cursor.getLong(EMAIL_LOOKUP_CONTACT_ID_COLUMN_INDEX);
    298                             String lookupKey = cursor.getString(
    299                                     EMAIL_LOOKUP_CONTACT_LOOKUP_KEY_COLUMN_INDEX);
    300                             bindFromContactUriInternal(Contacts.getLookupUri(contactId, lookupKey),
    301                                     false /* don't reset query handler */);
    302                         } else {
    303                             String emailAddress = (String) cookie;
    304                             setDisplayName(emailAddress, null);
    305                             setSocialSnippet(null);
    306                             setPhoto(loadPlaceholderPhoto(null));
    307                             mPhotoView.assignContactFromEmail(emailAddress, true);
    308                         }
    309                         break;
    310                     }
    311                 }
    312             } finally {
    313                 if (cursor != null) {
    314                     cursor.close();
    315                 }
    316             }
    317         }
    318     }
    319 
    320     /**
    321      * Manually set the presence.
    322      */
    323     public void setPresence(int presence) {
    324         mPresenceView.setImageResource(StatusUpdates.getPresenceIconResourceId(presence));
    325     }
    326 
    327     /**
    328      * Manually set the presence. If presence is null, it is hidden.
    329      * This doesn't change the underlying {@link Contacts} value, only the UI state.
    330      * @hide
    331      */
    332     public void setPresence(Integer presence) {
    333         if (presence == null) {
    334             showPresence(false);
    335         } else {
    336             showPresence(true);
    337             setPresence(presence.intValue());
    338         }
    339     }
    340 
    341     /**
    342      * Turn on/off showing the presence.
    343      * @hide this is here for consistency with setStared/showStar and should be public
    344      */
    345     public void showPresence(boolean showPresence) {
    346         mPresenceView.setVisibility(showPresence ? View.VISIBLE : View.GONE);
    347     }
    348 
    349     /**
    350      * Manually set the contact uri without loading any data
    351      */
    352     public void setContactUri(Uri uri) {
    353         setContactUri(uri, true);
    354     }
    355 
    356     /**
    357      * Manually set the contact uri without loading any data
    358      */
    359     public void setContactUri(Uri uri, boolean sendToQuickContact) {
    360         mContactUri = uri;
    361         if (sendToQuickContact) {
    362             mPhotoView.assignContactUri(uri);
    363         }
    364     }
    365 
    366     /**
    367      * Manually set the photo to display in the header. This doesn't change the
    368      * underlying {@link Contacts}, only the UI state.
    369      */
    370     public void setPhoto(Bitmap bitmap) {
    371         mPhotoView.setImageBitmap(bitmap);
    372     }
    373 
    374     /**
    375      * Manually set the photo given its id. If the id is 0, a placeholder picture will
    376      * be loaded. For any other Id, an async query is started
    377      * @hide
    378      */
    379     public void setPhotoId(final long photoId, final Uri lookupUri) {
    380         if (photoId == 0) {
    381             setPhoto(loadPlaceholderPhoto(null));
    382             mPhotoView.assignContactUri(lookupUri);
    383             invalidate();
    384         } else {
    385             startPhotoQuery(photoId, lookupUri,
    386                     false /* don't reset query handler */);
    387         }
    388     }
    389 
    390     /**
    391      * Manually set the display name and phonetic name to show in the header.
    392      * This doesn't change the underlying {@link Contacts}, only the UI state.
    393      */
    394     public void setDisplayName(CharSequence displayName, CharSequence phoneticName) {
    395         mDisplayNameView.setText(displayName);
    396         if (!TextUtils.isEmpty(phoneticName)) {
    397             mPhoneticNameView.setText(phoneticName);
    398             mPhoneticNameView.setVisibility(View.VISIBLE);
    399         } else {
    400             mPhoneticNameView.setVisibility(View.GONE);
    401         }
    402     }
    403 
    404     /**
    405      * Manually set the social snippet text to display in the header. This doesn't change the
    406      * underlying {@link Contacts}, only the UI state.
    407      */
    408     public void setSocialSnippet(CharSequence snippet) {
    409         if (snippet == null) {
    410             mStatusView.setVisibility(View.GONE);
    411             mStatusAttributionView.setVisibility(View.GONE);
    412         } else {
    413             mStatusView.setText(snippet);
    414             mStatusView.setVisibility(View.VISIBLE);
    415         }
    416     }
    417 
    418     /**
    419      * Manually set the status attribution text to display in the header.
    420      * This doesn't change the underlying {@link Contacts}, only the UI state.
    421      * @hide
    422      */
    423     public void setStatusAttribution(CharSequence attribution) {
    424         if (attribution != null) {
    425             mStatusAttributionView.setText(attribution);
    426             mStatusAttributionView.setVisibility(View.VISIBLE);
    427         } else {
    428             mStatusAttributionView.setVisibility(View.GONE);
    429         }
    430     }
    431 
    432     /**
    433      * Set a list of specific MIME-types to exclude and not display. For
    434      * example, this can be used to hide the {@link Contacts#CONTENT_ITEM_TYPE}
    435      * profile icon.
    436      */
    437     public void setExcludeMimes(String[] excludeMimes) {
    438         mExcludeMimes = excludeMimes;
    439         mPhotoView.setExcludeMimes(excludeMimes);
    440     }
    441 
    442     /**
    443      * Manually set all the status values to display in the header.
    444      * This doesn't change the underlying {@link Contacts}, only the UI state.
    445      * @hide
    446      * @param status             The status of the contact. If this is either null or empty,
    447      *                           the status is cleared and the other parameters are ignored.
    448      * @param statusTimestamp    The timestamp (retrieved via a call to
    449      *                           {@link System#currentTimeMillis()}) of the last status update.
    450      *                           This value can be null if it is not known.
    451      * @param statusLabel        The id of a resource string that specifies the current
    452      *                           status. This value can be null if no Label should be used.
    453      * @param statusResPackage   The name of the resource package containing the resource string
    454      *                           referenced in the parameter statusLabel.
    455      */
    456     public void setStatus(final String status, final Long statusTimestamp,
    457             final Integer statusLabel, final String statusResPackage) {
    458         if (TextUtils.isEmpty(status)) {
    459             setSocialSnippet(null);
    460             return;
    461         }
    462 
    463         setSocialSnippet(status);
    464 
    465         final CharSequence timestampDisplayValue;
    466 
    467         if (statusTimestamp != null) {
    468             // Set the date/time field by mixing relative and absolute
    469             // times.
    470             int flags = DateUtils.FORMAT_ABBREV_RELATIVE;
    471 
    472             timestampDisplayValue = DateUtils.getRelativeTimeSpanString(
    473                     statusTimestamp.longValue(), System.currentTimeMillis(),
    474                     DateUtils.MINUTE_IN_MILLIS, flags);
    475         } else {
    476             timestampDisplayValue = null;
    477         }
    478 
    479 
    480         String labelDisplayValue = null;
    481 
    482         if (statusLabel != null) {
    483             Resources resources;
    484             if (TextUtils.isEmpty(statusResPackage)) {
    485                 resources = getResources();
    486             } else {
    487                 PackageManager pm = getContext().getPackageManager();
    488                 try {
    489                     resources = pm.getResourcesForApplication(statusResPackage);
    490                 } catch (NameNotFoundException e) {
    491                     Log.w(TAG, "Contact status update resource package not found: "
    492                             + statusResPackage);
    493                     resources = null;
    494                 }
    495             }
    496 
    497             if (resources != null) {
    498                 try {
    499                     labelDisplayValue = resources.getString(statusLabel.intValue());
    500                 } catch (NotFoundException e) {
    501                     Log.w(TAG, "Contact status update resource not found: " + statusResPackage + "@"
    502                             + statusLabel.intValue());
    503                 }
    504             }
    505         }
    506 
    507         final CharSequence attribution;
    508         if (timestampDisplayValue != null && labelDisplayValue != null) {
    509             attribution = getContext().getString(
    510                     R.string.contact_status_update_attribution_with_date,
    511                     timestampDisplayValue, labelDisplayValue);
    512         } else if (timestampDisplayValue == null && labelDisplayValue != null) {
    513             attribution = getContext().getString(
    514                     R.string.contact_status_update_attribution,
    515                     labelDisplayValue);
    516         } else if (timestampDisplayValue != null) {
    517             attribution = timestampDisplayValue;
    518         } else {
    519             attribution = null;
    520         }
    521         setStatusAttribution(attribution);
    522     }
    523 
    524     /**
    525      * Convenience method for binding all available data from an existing
    526      * contact.
    527      *
    528      * @param contactLookupUri a {Contacts.CONTENT_LOOKUP_URI} style URI.
    529      */
    530     public void bindFromContactLookupUri(Uri contactLookupUri) {
    531         bindFromContactUriInternal(contactLookupUri, true /* reset query handler */);
    532     }
    533 
    534     /**
    535      * Convenience method for binding all available data from an existing
    536      * contact.
    537      *
    538      * @param contactUri a {Contacts.CONTENT_URI} style URI.
    539      * @param resetQueryHandler whether to use a new AsyncQueryHandler or not.
    540      */
    541     private void bindFromContactUriInternal(Uri contactUri, boolean resetQueryHandler) {
    542         mContactUri = contactUri;
    543         startContactQuery(contactUri, resetQueryHandler);
    544     }
    545 
    546     /**
    547      * Convenience method for binding all available data from an existing
    548      * contact.
    549      *
    550      * @param emailAddress The email address used to do a reverse lookup in
    551      * the contacts database. If more than one contact contains this email
    552      * address, one of them will be chosen to bind to.
    553      */
    554     public void bindFromEmail(String emailAddress) {
    555         resetAsyncQueryHandler();
    556 
    557         mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP, emailAddress,
    558                 Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(emailAddress)),
    559                 EMAIL_LOOKUP_PROJECTION, null, null, null);
    560     }
    561 
    562     /**
    563      * Convenience method for binding all available data from an existing
    564      * contact.
    565      *
    566      * @param number The phone number used to do a reverse lookup in
    567      * the contacts database. If more than one contact contains this phone
    568      * number, one of them will be chosen to bind to.
    569      */
    570     public void bindFromPhoneNumber(String number) {
    571         resetAsyncQueryHandler();
    572 
    573         mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP, number,
    574                 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)),
    575                 PHONE_LOOKUP_PROJECTION, null, null, null);
    576     }
    577 
    578     /**
    579      * startContactQuery
    580      *
    581      * internal method to query contact by Uri.
    582      *
    583      * @param contactUri the contact uri
    584      * @param resetQueryHandler whether to use a new AsyncQueryHandler or not
    585      */
    586     private void startContactQuery(Uri contactUri, boolean resetQueryHandler) {
    587         if (resetQueryHandler) {
    588             resetAsyncQueryHandler();
    589         }
    590 
    591         mQueryHandler.startQuery(TOKEN_CONTACT_INFO, contactUri, contactUri, ContactQuery.COLUMNS,
    592                 null, null, null);
    593     }
    594 
    595     /**
    596      * startPhotoQuery
    597      *
    598      * internal method to query contact photo by photo id and uri.
    599      *
    600      * @param photoId the photo id.
    601      * @param lookupKey the lookup uri.
    602      * @param resetQueryHandler whether to use a new AsyncQueryHandler or not.
    603      */
    604     protected void startPhotoQuery(long photoId, Uri lookupKey, boolean resetQueryHandler) {
    605         if (resetQueryHandler) {
    606             resetAsyncQueryHandler();
    607         }
    608 
    609         mQueryHandler.startQuery(TOKEN_PHOTO_QUERY, lookupKey,
    610                 ContentUris.withAppendedId(Data.CONTENT_URI, photoId), PhotoQuery.COLUMNS,
    611                 null, null, null);
    612     }
    613 
    614     /**
    615      * Method to force this widget to forget everything it knows about the contact.
    616      * We need to stop any existing async queries for phone, email, contact, and photos.
    617      */
    618     public void wipeClean() {
    619         resetAsyncQueryHandler();
    620 
    621         setDisplayName(null, null);
    622         setPhoto(loadPlaceholderPhoto(null));
    623         setSocialSnippet(null);
    624         setPresence(0);
    625         mContactUri = null;
    626         mExcludeMimes = null;
    627     }
    628 
    629 
    630     private void resetAsyncQueryHandler() {
    631         // the api AsyncQueryHandler.cancelOperation() doesn't really work. Since we really
    632         // need the old async queries to be cancelled, let's do it the hard way.
    633         mQueryHandler = new QueryHandler(mContentResolver);
    634     }
    635 
    636     /**
    637      * Bind the contact details provided by the given {@link Cursor}.
    638      */
    639     protected void bindContactInfo(Cursor c) {
    640         final String displayName = c.getString(ContactQuery.DISPLAY_NAME);
    641         final String phoneticName = c.getString(ContactQuery.PHONETIC_NAME);
    642         this.setDisplayName(displayName, phoneticName);
    643 
    644         //Set the presence status
    645         if (!c.isNull(ContactQuery.CONTACT_PRESENCE_STATUS)) {
    646             int presence = c.getInt(ContactQuery.CONTACT_PRESENCE_STATUS);
    647             setPresence(presence);
    648             showPresence(true);
    649         } else {
    650             showPresence(false);
    651         }
    652 
    653         //Set the status update
    654         final String status = c.getString(ContactQuery.CONTACT_STATUS);
    655         final Long statusTimestamp = c.isNull(ContactQuery.CONTACT_STATUS_TIMESTAMP)
    656                 ? null
    657                 : c.getLong(ContactQuery.CONTACT_STATUS_TIMESTAMP);
    658         final Integer statusLabel = c.isNull(ContactQuery.CONTACT_STATUS_LABEL)
    659                 ? null
    660                 : c.getInt(ContactQuery.CONTACT_STATUS_LABEL);
    661         final String statusResPackage = c.getString(ContactQuery.CONTACT_STATUS_RES_PACKAGE);
    662 
    663         setStatus(status, statusTimestamp, statusLabel, statusResPackage);
    664     }
    665 
    666     public void onClick(View view) {
    667         switch (view.getId()) {
    668             case R.id.photo: {
    669                 performPhotoClick();
    670                 break;
    671             }
    672             case R.id.name: {
    673                 performDisplayNameClick();
    674                 break;
    675             }
    676         }
    677     }
    678 
    679     private Bitmap loadPlaceholderPhoto(BitmapFactory.Options options) {
    680         if (mNoPhotoResource == 0) {
    681             return null;
    682         }
    683         return BitmapFactory.decodeResource(mContext.getResources(),
    684                 mNoPhotoResource, options);
    685     }
    686 }
    687