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