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 {
onPhotoClick(View view)86         public void onPhotoClick(View view);
onDisplayNameClick(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 
ContactHeaderWidget(Context context)157     public ContactHeaderWidget(Context context) {
158         this(context, null);
159     }
160 
ContactHeaderWidget(Context context, AttributeSet attrs)161     public ContactHeaderWidget(Context context, AttributeSet attrs) {
162         this(context, attrs, 0);
163     }
164 
ContactHeaderWidget(Context context, AttributeSet attrs, int defStyle)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 
enableClickListeners()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      */
setContactHeaderListener(ContactHeaderListener listener)210     public void setContactHeaderListener(ContactHeaderListener listener) {
211         mListener = listener;
212     }
213 
performPhotoClick()214     private void performPhotoClick() {
215         if (mListener != null) {
216             mListener.onPhotoClick(mPhotoView);
217         }
218     }
219 
performDisplayNameClick()220     private void performDisplayNameClick() {
221         if (mListener != null) {
222             mListener.onDisplayNameClick(mDisplayNameView);
223         }
224     }
225 
226     private class QueryHandler extends AsyncQueryHandler {
227 
QueryHandler(ContentResolver cr)228         public QueryHandler(ContentResolver cr) {
229             super(cr);
230         }
231 
232         @Override
onQueryComplete(int token, Object cookie, Cursor cursor)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      */
setPresence(int presence)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      */
setPresence(Integer presence)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      */
showPresence(boolean showPresence)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      */
setContactUri(Uri uri)352     public void setContactUri(Uri uri) {
353         setContactUri(uri, true);
354     }
355 
356     /**
357      * Manually set the contact uri without loading any data
358      */
setContactUri(Uri uri, boolean sendToQuickContact)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      */
setPhoto(Bitmap bitmap)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      */
setPhotoId(final long photoId, final Uri lookupUri)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      */
setDisplayName(CharSequence displayName, CharSequence phoneticName)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      */
setSocialSnippet(CharSequence snippet)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      */
setStatusAttribution(CharSequence attribution)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      */
setExcludeMimes(String[] excludeMimes)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      */
setStatus(final String status, final Long statusTimestamp, final Integer statusLabel, final String statusResPackage)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      */
bindFromContactLookupUri(Uri contactLookupUri)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      */
bindFromContactUriInternal(Uri contactUri, boolean resetQueryHandler)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      */
bindFromEmail(String emailAddress)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      */
bindFromPhoneNumber(String number)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      */
startContactQuery(Uri contactUri, boolean resetQueryHandler)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      */
startPhotoQuery(long photoId, Uri lookupKey, boolean resetQueryHandler)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      */
wipeClean()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 
resetAsyncQueryHandler()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      */
bindContactInfo(Cursor c)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 
onClick(View view)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 
loadPlaceholderPhoto(BitmapFactory.Options options)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