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 * @hide 336 */ 337 public void setSelectedContactsAppTabIndex(int value) { 338 mPhotoView.setSelectedContactsAppTabIndex(value); 339 } 340 341 /** 342 * Turn on/off showing of the aggregate badge element. 343 */ 344 public void showAggregateBadge(boolean showBagde) { 345 mAggregateBadge.setVisibility(showBagde ? View.VISIBLE : View.GONE); 346 } 347 348 /** 349 * Turn on/off showing of the star element. 350 */ 351 public void showStar(boolean showStar) { 352 mStarredView.setVisibility(showStar ? View.VISIBLE : View.GONE); 353 } 354 355 /** 356 * Manually set the starred state of this header widget. This doesn't change 357 * the underlying {@link Contacts} value, only the UI state. 358 */ 359 public void setStared(boolean starred) { 360 mStarredView.setChecked(starred); 361 } 362 363 /** 364 * Manually set the presence. 365 */ 366 public void setPresence(int presence) { 367 mPresenceView.setImageResource(StatusUpdates.getPresenceIconResourceId(presence)); 368 } 369 370 /** 371 * Manually set the contact uri 372 */ 373 public void setContactUri(Uri uri) { 374 setContactUri(uri, true); 375 } 376 377 /** 378 * Manually set the contact uri 379 */ 380 public void setContactUri(Uri uri, boolean sendToFastrack) { 381 mContactUri = uri; 382 if (sendToFastrack) { 383 mPhotoView.assignContactUri(uri); 384 } 385 } 386 387 /** 388 * Manually set the photo to display in the header. This doesn't change the 389 * underlying {@link Contacts}, only the UI state. 390 */ 391 public void setPhoto(Bitmap bitmap) { 392 mPhotoView.setImageBitmap(bitmap); 393 } 394 395 /** 396 * Manually set the display name and phonetic name to show in the header. 397 * This doesn't change the underlying {@link Contacts}, only the UI state. 398 */ 399 public void setDisplayName(CharSequence displayName, CharSequence phoneticName) { 400 mDisplayNameView.setText(displayName); 401 if (!TextUtils.isEmpty(phoneticName)) { 402 mPhoneticNameView.setText(phoneticName); 403 mPhoneticNameView.setVisibility(View.VISIBLE); 404 } else { 405 mPhoneticNameView.setVisibility(View.GONE); 406 } 407 } 408 409 /** 410 * Manually set the social snippet text to display in the header. 411 */ 412 public void setSocialSnippet(CharSequence snippet) { 413 if (snippet == null) { 414 mStatusView.setVisibility(View.GONE); 415 mStatusAttributionView.setVisibility(View.GONE); 416 } else { 417 mStatusView.setText(snippet); 418 mStatusView.setVisibility(View.VISIBLE); 419 } 420 } 421 422 /** 423 * Set a list of specific MIME-types to exclude and not display. For 424 * example, this can be used to hide the {@link Contacts#CONTENT_ITEM_TYPE} 425 * profile icon. 426 */ 427 public void setExcludeMimes(String[] excludeMimes) { 428 mExcludeMimes = excludeMimes; 429 mPhotoView.setExcludeMimes(excludeMimes); 430 } 431 432 /** 433 * Convenience method for binding all available data from an existing 434 * contact. 435 * 436 * @param contactLookupUri a {Contacts.CONTENT_LOOKUP_URI} style URI. 437 */ 438 public void bindFromContactLookupUri(Uri contactLookupUri) { 439 bindFromContactUriInternal(contactLookupUri, true /* reset query handler */); 440 } 441 442 /** 443 * Convenience method for binding all available data from an existing 444 * contact. 445 * 446 * @param contactUri a {Contacts.CONTENT_URI} style URI. 447 * @param resetQueryHandler whether to use a new AsyncQueryHandler or not. 448 */ 449 private void bindFromContactUriInternal(Uri contactUri, boolean resetQueryHandler) { 450 mContactUri = contactUri; 451 startContactQuery(contactUri, resetQueryHandler); 452 } 453 454 /** 455 * Convenience method for binding all available data from an existing 456 * contact. 457 * 458 * @param emailAddress The email address used to do a reverse lookup in 459 * the contacts database. If more than one contact contains this email 460 * address, one of them will be chosen to bind to. 461 */ 462 public void bindFromEmail(String emailAddress) { 463 resetAsyncQueryHandler(); 464 465 mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP, emailAddress, 466 Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(emailAddress)), 467 EMAIL_LOOKUP_PROJECTION, null, null, null); 468 } 469 470 /** 471 * Convenience method for binding all available data from an existing 472 * contact. 473 * 474 * @param number The phone number used to do a reverse lookup in 475 * the contacts database. If more than one contact contains this phone 476 * number, one of them will be chosen to bind to. 477 */ 478 public void bindFromPhoneNumber(String number) { 479 resetAsyncQueryHandler(); 480 481 mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP, number, 482 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)), 483 PHONE_LOOKUP_PROJECTION, null, null, null); 484 } 485 486 /** 487 * startContactQuery 488 * 489 * internal method to query contact by Uri. 490 * 491 * @param contactUri the contact uri 492 * @param resetQueryHandler whether to use a new AsyncQueryHandler or not 493 */ 494 private void startContactQuery(Uri contactUri, boolean resetQueryHandler) { 495 if (resetQueryHandler) { 496 resetAsyncQueryHandler(); 497 } 498 499 mQueryHandler.startQuery(TOKEN_CONTACT_INFO, contactUri, contactUri, ContactQuery.COLUMNS, 500 null, null, null); 501 } 502 503 /** 504 * startPhotoQuery 505 * 506 * internal method to query contact photo by photo id and uri. 507 * 508 * @param photoId the photo id. 509 * @param lookupKey the lookup uri. 510 * @param resetQueryHandler whether to use a new AsyncQueryHandler or not. 511 */ 512 protected void startPhotoQuery(long photoId, Uri lookupKey, boolean resetQueryHandler) { 513 if (resetQueryHandler) { 514 resetAsyncQueryHandler(); 515 } 516 517 mQueryHandler.startQuery(TOKEN_PHOTO_QUERY, lookupKey, 518 ContentUris.withAppendedId(Data.CONTENT_URI, photoId), PhotoQuery.COLUMNS, 519 null, null, null); 520 } 521 522 /** 523 * Method to force this widget to forget everything it knows about the contact. 524 * We need to stop any existing async queries for phone, email, contact, and photos. 525 */ 526 public void wipeClean() { 527 resetAsyncQueryHandler(); 528 529 setDisplayName(null, null); 530 setPhoto(loadPlaceholderPhoto(null)); 531 setSocialSnippet(null); 532 setPresence(0); 533 mContactUri = null; 534 mExcludeMimes = null; 535 } 536 537 538 private void resetAsyncQueryHandler() { 539 // the api AsyncQueryHandler.cancelOperation() doesn't really work. Since we really 540 // need the old async queries to be cancelled, let's do it the hard way. 541 mQueryHandler = new QueryHandler(mContentResolver); 542 } 543 544 /** 545 * Bind the contact details provided by the given {@link Cursor}. 546 */ 547 protected void bindContactInfo(Cursor c) { 548 final String displayName = c.getString(ContactQuery.DISPLAY_NAME); 549 final String phoneticName = c.getString(ContactQuery.PHONETIC_NAME); 550 this.setDisplayName(displayName, phoneticName); 551 552 final boolean starred = c.getInt(ContactQuery.STARRED) != 0; 553 mStarredView.setChecked(starred); 554 555 //Set the presence status 556 if (!c.isNull(ContactQuery.CONTACT_PRESENCE_STATUS)) { 557 int presence = c.getInt(ContactQuery.CONTACT_PRESENCE_STATUS); 558 mPresenceView.setImageResource(StatusUpdates.getPresenceIconResourceId(presence)); 559 mPresenceView.setVisibility(View.VISIBLE); 560 } else { 561 mPresenceView.setVisibility(View.GONE); 562 } 563 564 //Set the status update 565 String status = c.getString(ContactQuery.CONTACT_STATUS); 566 if (!TextUtils.isEmpty(status)) { 567 mStatusView.setText(status); 568 mStatusView.setVisibility(View.VISIBLE); 569 570 CharSequence timestamp = null; 571 572 if (!c.isNull(ContactQuery.CONTACT_STATUS_TIMESTAMP)) { 573 long date = c.getLong(ContactQuery.CONTACT_STATUS_TIMESTAMP); 574 575 // Set the date/time field by mixing relative and absolute 576 // times. 577 int flags = DateUtils.FORMAT_ABBREV_RELATIVE; 578 579 timestamp = DateUtils.getRelativeTimeSpanString(date, 580 System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, flags); 581 } 582 583 String label = null; 584 585 if (!c.isNull(ContactQuery.CONTACT_STATUS_LABEL)) { 586 String resPackage = c.getString(ContactQuery.CONTACT_STATUS_RES_PACKAGE); 587 int labelResource = c.getInt(ContactQuery.CONTACT_STATUS_LABEL); 588 Resources resources; 589 if (TextUtils.isEmpty(resPackage)) { 590 resources = getResources(); 591 } else { 592 PackageManager pm = getContext().getPackageManager(); 593 try { 594 resources = pm.getResourcesForApplication(resPackage); 595 } catch (NameNotFoundException e) { 596 Log.w(TAG, "Contact status update resource package not found: " 597 + resPackage); 598 resources = null; 599 } 600 } 601 602 if (resources != null) { 603 try { 604 label = resources.getString(labelResource); 605 } catch (NotFoundException e) { 606 Log.w(TAG, "Contact status update resource not found: " + resPackage + "@" 607 + labelResource); 608 } 609 } 610 } 611 612 CharSequence attribution; 613 if (timestamp != null && label != null) { 614 attribution = getContext().getString( 615 R.string.contact_status_update_attribution_with_date, 616 timestamp, label); 617 } else if (timestamp == null && label != null) { 618 attribution = getContext().getString( 619 R.string.contact_status_update_attribution, 620 label); 621 } else if (timestamp != null) { 622 attribution = timestamp; 623 } else { 624 attribution = null; 625 } 626 if (attribution != null) { 627 mStatusAttributionView.setText(attribution); 628 mStatusAttributionView.setVisibility(View.VISIBLE); 629 } else { 630 mStatusAttributionView.setVisibility(View.GONE); 631 } 632 } else { 633 mStatusView.setVisibility(View.GONE); 634 mStatusAttributionView.setVisibility(View.GONE); 635 } 636 } 637 638 public void onClick(View view) { 639 switch (view.getId()) { 640 case R.id.star: { 641 // Toggle "starred" state 642 // Make sure there is a contact 643 if (mContactUri != null) { 644 final ContentValues values = new ContentValues(1); 645 values.put(Contacts.STARRED, mStarredView.isChecked()); 646 mContentResolver.update(mContactUri, values, null, null); 647 } 648 break; 649 } 650 case R.id.photo: { 651 performPhotoClick(); 652 break; 653 } 654 case R.id.name: { 655 performDisplayNameClick(); 656 break; 657 } 658 } 659 } 660 661 private Bitmap loadPlaceholderPhoto(BitmapFactory.Options options) { 662 if (mNoPhotoResource == 0) { 663 return null; 664 } 665 return BitmapFactory.decodeResource(mContext.getResources(), 666 mNoPhotoResource, options); 667 } 668 } 669