ContactLoader.java revision 97e90c6d0938e31c2af4ab4b6b055cda853502c5
1/* 2 * Copyright (C) 2010 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 17package com.android.contacts; 18 19import com.android.contacts.util.DataStatus; 20 21import android.content.ContentResolver; 22import android.content.ContentUris; 23import android.content.ContentValues; 24import android.content.Context; 25import android.content.Entity; 26import android.content.Entity.NamedContentValues; 27import android.content.Loader; 28import android.content.pm.PackageManager; 29import android.content.pm.PackageManager.NameNotFoundException; 30import android.content.res.Resources; 31import android.database.Cursor; 32import android.net.Uri; 33import android.os.AsyncTask; 34import android.provider.ContactsContract; 35import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 36import android.provider.ContactsContract.CommonDataKinds.Photo; 37import android.provider.ContactsContract.Contacts; 38import android.provider.ContactsContract.Data; 39import android.provider.ContactsContract.Directory; 40import android.provider.ContactsContract.DisplayNameSources; 41import android.provider.ContactsContract.Groups; 42import android.provider.ContactsContract.RawContacts; 43import android.text.TextUtils; 44import android.util.Log; 45 46import java.io.ByteArrayOutputStream; 47import java.io.IOException; 48import java.io.InputStream; 49import java.util.ArrayList; 50import java.util.HashMap; 51import java.util.List; 52 53/** 54 * Loads a single Contact and all it constituent RawContacts. 55 */ 56public class ContactLoader extends Loader<ContactLoader.Result> { 57 private static final String TAG = "ContactLoader"; 58 59 private Uri mLookupUri; 60 private boolean mLoadGroupMetaData; 61 private Result mContact; 62 private ForceLoadContentObserver mObserver; 63 private boolean mDestroyed; 64 65 66 public interface Listener { 67 public void onContactLoaded(Result contact); 68 } 69 70 /** 71 * The result of a load operation. Contains all data necessary to display the contact. 72 */ 73 public static final class Result { 74 /** 75 * Singleton instance that represents "No Contact Found" 76 */ 77 public static final Result NOT_FOUND = new Result(); 78 79 /** 80 * Singleton instance that represents an error, e.g. because of an invalid Uri 81 * TODO: We should come up with something nicer here. Maybe use an Either type so 82 * that we can capture the Exception? 83 */ 84 public static final Result ERROR = new Result(); 85 86 private final Uri mLookupUri; 87 private final Uri mUri; 88 private final long mDirectoryId; 89 private final String mLookupKey; 90 private final long mId; 91 private final long mNameRawContactId; 92 private final int mDisplayNameSource; 93 private final long mPhotoId; 94 private final String mPhotoUri; 95 private final String mDisplayName; 96 private final String mPhoneticName; 97 private final boolean mStarred; 98 private final Integer mPresence; 99 private final ArrayList<Entity> mEntities; 100 private final HashMap<Long, DataStatus> mStatuses; 101 private final String mStatus; 102 private final Long mStatusTimestamp; 103 private final Integer mStatusLabel; 104 private final String mStatusResPackage; 105 106 private String mDirectoryDisplayName; 107 private String mDirectoryType; 108 private String mDirectoryAccountType; 109 private String mDirectoryAccountName; 110 private int mDirectoryExportSupport; 111 112 private ArrayList<GroupMetaData> mGroups; 113 114 private boolean mLoadingPhoto; 115 private byte[] mPhotoBinaryData; 116 117 /** 118 * Constructor for case "no contact found". This must only be used for the 119 * final {@link Result#NOT_FOUND} singleton 120 */ 121 private Result() { 122 mLookupUri = null; 123 mUri = null; 124 mDirectoryId = -1; 125 mLookupKey = null; 126 mId = -1; 127 mEntities = null; 128 mStatuses = null; 129 mNameRawContactId = -1; 130 mDisplayNameSource = DisplayNameSources.UNDEFINED; 131 mPhotoId = -1; 132 mPhotoUri = null; 133 mDisplayName = null; 134 mPhoneticName = null; 135 mStarred = false; 136 mPresence = null; 137 mStatus = null; 138 mStatusTimestamp = null; 139 mStatusLabel = null; 140 mStatusResPackage = null; 141 } 142 143 /** 144 * Constructor to call when contact was found 145 */ 146 private Result(Uri uri, Uri lookupUri, long directoryId, String lookupKey, long id, 147 long nameRawContactId, int displayNameSource, long photoId, String photoUri, 148 String displayName, String phoneticName, boolean starred, Integer presence, 149 String status, Long statusTimestamp, Integer statusLabel, String statusResPackage) { 150 mLookupUri = lookupUri; 151 mUri = uri; 152 mDirectoryId = directoryId; 153 mLookupKey = lookupKey; 154 mId = id; 155 mEntities = new ArrayList<Entity>(); 156 mStatuses = new HashMap<Long, DataStatus>(); 157 mNameRawContactId = nameRawContactId; 158 mDisplayNameSource = displayNameSource; 159 mPhotoId = photoId; 160 mPhotoUri = photoUri; 161 mDisplayName = displayName; 162 mPhoneticName = phoneticName; 163 mStarred = starred; 164 mPresence = presence; 165 mStatus = status; 166 mStatusTimestamp = statusTimestamp; 167 mStatusLabel = statusLabel; 168 mStatusResPackage = statusResPackage; 169 } 170 171 /** 172 * @param exportSupport See {@link Directory#EXPORT_SUPPORT}. 173 */ 174 private void setDirectoryMetaData(String displayName, String directoryType, 175 String accountType, String accountName, int exportSupport) { 176 mDirectoryDisplayName = displayName; 177 mDirectoryType = directoryType; 178 mDirectoryAccountType = accountType; 179 mDirectoryAccountName = accountName; 180 mDirectoryExportSupport = exportSupport; 181 } 182 183 private void setLoadingPhoto(boolean flag) { 184 mLoadingPhoto = flag; 185 } 186 187 private void setPhotoBinaryData(byte[] photoBinaryData) { 188 mPhotoBinaryData = photoBinaryData; 189 } 190 191 public Uri getLookupUri() { 192 return mLookupUri; 193 } 194 195 public String getLookupKey() { 196 return mLookupKey; 197 } 198 199 public Uri getUri() { 200 return mUri; 201 } 202 203 public long getId() { 204 return mId; 205 } 206 207 public long getNameRawContactId() { 208 return mNameRawContactId; 209 } 210 211 public int getDisplayNameSource() { 212 return mDisplayNameSource; 213 } 214 215 public long getPhotoId() { 216 return mPhotoId; 217 } 218 219 public String getPhotoUri() { 220 return mPhotoUri; 221 } 222 223 public String getDisplayName() { 224 return mDisplayName; 225 } 226 227 public String getPhoneticName() { 228 return mPhoneticName; 229 } 230 231 public boolean getStarred() { 232 return mStarred; 233 } 234 235 public Integer getPresence() { 236 return mPresence; 237 } 238 239 public String getSocialSnippet() { 240 return mStatus; 241 } 242 243 public Long getStatusTimestamp() { 244 return mStatusTimestamp; 245 } 246 247 public Integer getStatusLabel() { 248 return mStatusLabel; 249 } 250 251 public String getStatusResPackage() { 252 return mStatusResPackage; 253 } 254 255 public ArrayList<Entity> getEntities() { 256 return mEntities; 257 } 258 259 public HashMap<Long, DataStatus> getStatuses() { 260 return mStatuses; 261 } 262 263 public long getDirectoryId() { 264 return mDirectoryId; 265 } 266 267 public boolean isDirectoryEntry() { 268 return mDirectoryId != -1 && mDirectoryId != Directory.DEFAULT 269 && mDirectoryId != Directory.LOCAL_INVISIBLE; 270 } 271 272 public int getDirectoryExportSupport() { 273 return mDirectoryExportSupport; 274 } 275 276 public String getDirectoryDisplayName() { 277 return mDirectoryDisplayName; 278 } 279 280 public String getDirectoryType() { 281 return mDirectoryType; 282 } 283 284 public String getDirectoryAccountType() { 285 return mDirectoryAccountType; 286 } 287 288 public String getDirectoryAccountName() { 289 return mDirectoryAccountName; 290 } 291 292 public boolean isLoadingPhoto() { 293 return mLoadingPhoto; 294 } 295 296 public byte[] getPhotoBinaryData() { 297 return mPhotoBinaryData; 298 } 299 300 public ArrayList<ContentValues> getContentValues() { 301 if (mEntities.size() != 1) { 302 throw new IllegalStateException( 303 "Cannot extract content values from an aggregated contact"); 304 } 305 306 Entity entity = mEntities.get(0); 307 ArrayList<ContentValues> result = new ArrayList<ContentValues>(); 308 ArrayList<NamedContentValues> subValues = entity.getSubValues(); 309 if (subValues != null) { 310 int size = subValues.size(); 311 for (int i = 0; i < size; i++) { 312 NamedContentValues pair = subValues.get(i); 313 if (Data.CONTENT_URI.equals(pair.uri)) { 314 result.add(pair.values); 315 } 316 } 317 } 318 319 // If the photo was loaded using the URI, create an entry for the photo 320 // binary data. 321 if (mPhotoId == 0 && mPhotoBinaryData != null) { 322 ContentValues photo = new ContentValues(); 323 photo.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE); 324 photo.put(Photo.PHOTO, mPhotoBinaryData); 325 result.add(photo); 326 } 327 328 return result; 329 } 330 331 private void addGroupMetaData(GroupMetaData group) { 332 if (mGroups == null) { 333 mGroups = new ArrayList<GroupMetaData>(); 334 } 335 mGroups.add(group); 336 } 337 338 public List<GroupMetaData> getGroupMetaData() { 339 return mGroups; 340 } 341 } 342 343 private static class ContactQuery { 344 // Projection used for the query that loads all data for the entire contact. 345 final static String[] COLUMNS = new String[] { 346 Contacts.NAME_RAW_CONTACT_ID, 347 Contacts.DISPLAY_NAME_SOURCE, 348 Contacts.LOOKUP_KEY, 349 Contacts.DISPLAY_NAME, 350 Contacts.PHONETIC_NAME, 351 Contacts.PHOTO_ID, 352 Contacts.STARRED, 353 Contacts.CONTACT_PRESENCE, 354 Contacts.CONTACT_STATUS, 355 Contacts.CONTACT_STATUS_TIMESTAMP, 356 Contacts.CONTACT_STATUS_RES_PACKAGE, 357 Contacts.CONTACT_STATUS_LABEL, 358 Contacts.Entity.CONTACT_ID, 359 Contacts.Entity.RAW_CONTACT_ID, 360 361 RawContacts.ACCOUNT_NAME, 362 RawContacts.ACCOUNT_TYPE, 363 RawContacts.DIRTY, 364 RawContacts.VERSION, 365 RawContacts.SOURCE_ID, 366 RawContacts.SYNC1, 367 RawContacts.SYNC2, 368 RawContacts.SYNC3, 369 RawContacts.SYNC4, 370 RawContacts.DELETED, 371 RawContacts.IS_RESTRICTED, 372 RawContacts.NAME_VERIFIED, 373 374 Contacts.Entity.DATA_ID, 375 Data.DATA1, 376 Data.DATA2, 377 Data.DATA3, 378 Data.DATA4, 379 Data.DATA5, 380 Data.DATA6, 381 Data.DATA7, 382 Data.DATA8, 383 Data.DATA9, 384 Data.DATA10, 385 Data.DATA11, 386 Data.DATA12, 387 Data.DATA13, 388 Data.DATA14, 389 Data.DATA15, 390 Data.SYNC1, 391 Data.SYNC2, 392 Data.SYNC3, 393 Data.SYNC4, 394 Data.DATA_VERSION, 395 Data.IS_PRIMARY, 396 Data.IS_SUPER_PRIMARY, 397 Data.MIMETYPE, 398 Data.RES_PACKAGE, 399 400 GroupMembership.GROUP_SOURCE_ID, 401 402 Data.PRESENCE, 403 Data.CHAT_CAPABILITY, 404 Data.STATUS, 405 Data.STATUS_RES_PACKAGE, 406 Data.STATUS_ICON, 407 Data.STATUS_LABEL, 408 Data.STATUS_TIMESTAMP, 409 410 Contacts.PHOTO_URI, 411 }; 412 413 public final static int NAME_RAW_CONTACT_ID = 0; 414 public final static int DISPLAY_NAME_SOURCE = 1; 415 public final static int LOOKUP_KEY = 2; 416 public final static int DISPLAY_NAME = 3; 417 public final static int PHONETIC_NAME = 4; 418 public final static int PHOTO_ID = 5; 419 public final static int STARRED = 6; 420 public final static int CONTACT_PRESENCE = 7; 421 public final static int CONTACT_STATUS = 8; 422 public final static int CONTACT_STATUS_TIMESTAMP = 9; 423 public final static int CONTACT_STATUS_RES_PACKAGE = 10; 424 public final static int CONTACT_STATUS_LABEL = 11; 425 public final static int CONTACT_ID = 12; 426 public final static int RAW_CONTACT_ID = 13; 427 428 public final static int ACCOUNT_NAME = 14; 429 public final static int ACCOUNT_TYPE = 15; 430 public final static int DIRTY = 16; 431 public final static int VERSION = 17; 432 public final static int SOURCE_ID = 18; 433 public final static int SYNC1 = 19; 434 public final static int SYNC2 = 20; 435 public final static int SYNC3 = 21; 436 public final static int SYNC4 = 22; 437 public final static int DELETED = 23; 438 public final static int IS_RESTRICTED = 24; 439 public final static int NAME_VERIFIED = 25; 440 441 public final static int DATA_ID = 26; 442 public final static int DATA1 = 27; 443 public final static int DATA2 = 28; 444 public final static int DATA3 = 29; 445 public final static int DATA4 = 30; 446 public final static int DATA5 = 31; 447 public final static int DATA6 = 32; 448 public final static int DATA7 = 33; 449 public final static int DATA8 = 34; 450 public final static int DATA9 = 35; 451 public final static int DATA10 = 36; 452 public final static int DATA11 = 37; 453 public final static int DATA12 = 38; 454 public final static int DATA13 = 39; 455 public final static int DATA14 = 40; 456 public final static int DATA15 = 41; 457 public final static int DATA_SYNC1 = 42; 458 public final static int DATA_SYNC2 = 43; 459 public final static int DATA_SYNC3 = 44; 460 public final static int DATA_SYNC4 = 45; 461 public final static int DATA_VERSION = 46; 462 public final static int IS_PRIMARY = 47; 463 public final static int IS_SUPERPRIMARY = 48; 464 public final static int MIMETYPE = 49; 465 public final static int RES_PACKAGE = 50; 466 467 public final static int GROUP_SOURCE_ID = 51; 468 469 public final static int PRESENCE = 52; 470 public final static int CHAT_CAPABILITY = 53; 471 public final static int STATUS = 54; 472 public final static int STATUS_RES_PACKAGE = 55; 473 public final static int STATUS_ICON = 56; 474 public final static int STATUS_LABEL = 57; 475 public final static int STATUS_TIMESTAMP = 58; 476 477 public final static int PHOTO_URI = 59; 478 } 479 480 private static class DirectoryQuery { 481 // Projection used for the query that loads all data for the entire contact. 482 final static String[] COLUMNS = new String[] { 483 Directory.DISPLAY_NAME, 484 Directory.PACKAGE_NAME, 485 Directory.TYPE_RESOURCE_ID, 486 Directory.ACCOUNT_TYPE, 487 Directory.ACCOUNT_NAME, 488 Directory.EXPORT_SUPPORT, 489 }; 490 491 public final static int DISPLAY_NAME = 0; 492 public final static int PACKAGE_NAME = 1; 493 public final static int TYPE_RESOURCE_ID = 2; 494 public final static int ACCOUNT_TYPE = 3; 495 public final static int ACCOUNT_NAME = 4; 496 public final static int EXPORT_SUPPORT = 5; 497 } 498 499 private static class GroupQuery { 500 final static String[] COLUMNS = new String[] { 501 Groups.ACCOUNT_NAME, 502 Groups.ACCOUNT_TYPE, 503 Groups._ID, 504 Groups.TITLE, 505 Groups.AUTO_ADD, 506 Groups.FAVORITES, 507 }; 508 509 public final static int ACCOUNT_NAME = 0; 510 public final static int ACCOUNT_TYPE = 1; 511 public final static int ID = 2; 512 public final static int TITLE = 3; 513 public final static int AUTO_ADD = 4; 514 public final static int FAVORITES = 5; 515 } 516 517 private final class LoadContactTask extends AsyncTask<Void, Void, Result> { 518 519 @Override 520 protected Result doInBackground(Void... args) { 521 try { 522 final ContentResolver resolver = getContext().getContentResolver(); 523 final Uri uriCurrentFormat = ensureIsContactUri(resolver, mLookupUri); 524 Result result = loadContactEntity(resolver, uriCurrentFormat); 525 if (result != Result.NOT_FOUND) { 526 if (result.isDirectoryEntry()) { 527 loadDirectoryMetaData(result); 528 } else if (mLoadGroupMetaData) { 529 loadGroupMetaData(result); 530 } 531 loadPhotoBinaryData(result); 532 } 533 return result; 534 } catch (Exception e) { 535 Log.e(TAG, "Error loading the contact: " + mLookupUri, e); 536 return Result.ERROR; 537 } 538 } 539 540 /** 541 * Transforms the given Uri and returns a Lookup-Uri that represents the contact. 542 * For legacy contacts, a raw-contact lookup is performed. 543 * @param resolver 544 */ 545 private Uri ensureIsContactUri(final ContentResolver resolver, final Uri uri) { 546 if (uri == null) throw new IllegalArgumentException("uri must not be null"); 547 548 final String authority = uri.getAuthority(); 549 550 // Current Style Uri? 551 if (ContactsContract.AUTHORITY.equals(authority)) { 552 final String type = resolver.getType(uri); 553 // Contact-Uri? Good, return it 554 if (Contacts.CONTENT_ITEM_TYPE.equals(type)) { 555 return uri; 556 } 557 558 // RawContact-Uri? Transform it to ContactUri 559 if (RawContacts.CONTENT_ITEM_TYPE.equals(type)) { 560 final long rawContactId = ContentUris.parseId(uri); 561 return RawContacts.getContactLookupUri(getContext().getContentResolver(), 562 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)); 563 } 564 565 // Anything else? We don't know what this is 566 throw new IllegalArgumentException("uri format is unknown"); 567 } 568 569 // Legacy Style? Convert to RawContact 570 final String OBSOLETE_AUTHORITY = "contacts"; 571 if (OBSOLETE_AUTHORITY.equals(authority)) { 572 // Legacy Format. Convert to RawContact-Uri and then lookup the contact 573 final long rawContactId = ContentUris.parseId(uri); 574 return RawContacts.getContactLookupUri(resolver, 575 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId)); 576 } 577 578 throw new IllegalArgumentException("uri authority is unknown"); 579 } 580 581 private Result loadContactEntity(ContentResolver resolver, Uri contactUri) { 582 Uri entityUri = Uri.withAppendedPath(contactUri, Contacts.Entity.CONTENT_DIRECTORY); 583 Cursor cursor = resolver.query(entityUri, ContactQuery.COLUMNS, null, null, 584 Contacts.Entity.RAW_CONTACT_ID); 585 if (cursor == null) { 586 Log.e(TAG, "No cursor returned in loadContactEntity"); 587 return Result.NOT_FOUND; 588 } 589 590 try { 591 if (!cursor.moveToFirst()) { 592 cursor.close(); 593 return Result.NOT_FOUND; 594 } 595 596 long currentRawContactId = -1; 597 Entity entity = null; 598 Result result = loadContactHeaderData(cursor, contactUri); 599 ArrayList<Entity> entities = result.getEntities(); 600 HashMap<Long, DataStatus> statuses = result.getStatuses(); 601 for (; !cursor.isAfterLast(); cursor.moveToNext()) { 602 long rawContactId = cursor.getLong(ContactQuery.RAW_CONTACT_ID); 603 if (rawContactId != currentRawContactId) { 604 currentRawContactId = rawContactId; 605 entity = new android.content.Entity(loadRawContact(cursor)); 606 entities.add(entity); 607 } 608 if (!cursor.isNull(ContactQuery.DATA_ID)) { 609 ContentValues data = loadData(cursor); 610 entity.addSubValue(ContactsContract.Data.CONTENT_URI, data); 611 612 if (!cursor.isNull(ContactQuery.PRESENCE) 613 || !cursor.isNull(ContactQuery.STATUS)) { 614 final DataStatus status = new DataStatus(cursor); 615 final long dataId = cursor.getLong(ContactQuery.DATA_ID); 616 statuses.put(dataId, status); 617 } 618 } 619 } 620 621 return result; 622 } finally { 623 cursor.close(); 624 } 625 } 626 627 /** 628 * Looks for the photo data item in entities. If found, creates a new Bitmap instance. If 629 * not found, returns null 630 */ 631 private void loadPhotoBinaryData(Result contactData) { 632 final long photoId = contactData.getPhotoId(); 633 if (photoId <= 0) { 634 // No photo ID 635 return; 636 } 637 638 for (Entity entity : contactData.getEntities()) { 639 for (NamedContentValues subValue : entity.getSubValues()) { 640 final ContentValues entryValues = subValue.values; 641 final long dataId = entryValues.getAsLong(Data._ID); 642 if (dataId == photoId) { 643 final String mimeType = entryValues.getAsString(Data.MIMETYPE); 644 // Correct Data Id but incorrect MimeType? Don't load 645 if (!Photo.CONTENT_ITEM_TYPE.equals(mimeType)) { 646 return; 647 } 648 contactData.setPhotoBinaryData(entryValues.getAsByteArray(Photo.PHOTO)); 649 break; 650 } 651 } 652 } 653 } 654 655 /** 656 * Extracts Contact level columns from the cursor. 657 */ 658 private Result loadContactHeaderData(final Cursor cursor, Uri contactUri) { 659 final String directoryParameter = 660 contactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY); 661 final long directoryId = directoryParameter == null 662 ? Directory.DEFAULT 663 : Long.parseLong(directoryParameter); 664 final long contactId = cursor.getLong(ContactQuery.CONTACT_ID); 665 final String lookupKey = cursor.getString(ContactQuery.LOOKUP_KEY); 666 final long nameRawContactId = cursor.getLong(ContactQuery.NAME_RAW_CONTACT_ID); 667 final int displayNameSource = cursor.getInt(ContactQuery.DISPLAY_NAME_SOURCE); 668 final String displayName = cursor.getString(ContactQuery.DISPLAY_NAME); 669 final String phoneticName = cursor.getString(ContactQuery.PHONETIC_NAME); 670 final long photoId = cursor.getLong(ContactQuery.PHOTO_ID); 671 final String photoUri = cursor.getString(ContactQuery.PHOTO_URI); 672 final boolean starred = cursor.getInt(ContactQuery.STARRED) != 0; 673 final Integer presence = cursor.isNull(ContactQuery.CONTACT_PRESENCE) 674 ? null 675 : cursor.getInt(ContactQuery.CONTACT_PRESENCE); 676 final String status = cursor.getString(ContactQuery.CONTACT_STATUS); 677 final Long statusTimestamp = cursor.isNull(ContactQuery.CONTACT_STATUS_TIMESTAMP) 678 ? null 679 : cursor.getLong(ContactQuery.CONTACT_STATUS_TIMESTAMP); 680 final Integer statusLabel = cursor.isNull(ContactQuery.CONTACT_STATUS_LABEL) 681 ? null 682 : cursor.getInt(ContactQuery.CONTACT_STATUS_LABEL); 683 final String statusResPackage = cursor.getString( 684 ContactQuery.CONTACT_STATUS_RES_PACKAGE); 685 686 Uri lookupUri; 687 if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) { 688 lookupUri = ContentUris.withAppendedId( 689 Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), contactId); 690 } else { 691 lookupUri = contactUri; 692 } 693 694 return new Result(contactUri, lookupUri, directoryId, lookupKey, contactId, 695 nameRawContactId, displayNameSource, photoId, photoUri, displayName, 696 phoneticName, starred, presence, status, statusTimestamp, statusLabel, 697 statusResPackage); 698 } 699 700 /** 701 * Extracts RawContact level columns from the cursor. 702 */ 703 private ContentValues loadRawContact(Cursor cursor) { 704 ContentValues cv = new ContentValues(); 705 706 cv.put(RawContacts._ID, cursor.getLong(ContactQuery.RAW_CONTACT_ID)); 707 708 cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_NAME); 709 cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE); 710 cursorColumnToContentValues(cursor, cv, ContactQuery.DIRTY); 711 cursorColumnToContentValues(cursor, cv, ContactQuery.VERSION); 712 cursorColumnToContentValues(cursor, cv, ContactQuery.SOURCE_ID); 713 cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC1); 714 cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC2); 715 cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC3); 716 cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC4); 717 cursorColumnToContentValues(cursor, cv, ContactQuery.DELETED); 718 cursorColumnToContentValues(cursor, cv, ContactQuery.CONTACT_ID); 719 cursorColumnToContentValues(cursor, cv, ContactQuery.STARRED); 720 cursorColumnToContentValues(cursor, cv, ContactQuery.IS_RESTRICTED); 721 cursorColumnToContentValues(cursor, cv, ContactQuery.NAME_VERIFIED); 722 723 return cv; 724 } 725 726 /** 727 * Extracts Data level columns from the cursor. 728 */ 729 private ContentValues loadData(Cursor cursor) { 730 ContentValues cv = new ContentValues(); 731 732 cv.put(Data._ID, cursor.getLong(ContactQuery.DATA_ID)); 733 734 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA1); 735 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA2); 736 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA3); 737 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA4); 738 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA5); 739 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA6); 740 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA7); 741 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA8); 742 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA9); 743 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA10); 744 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA11); 745 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA12); 746 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA13); 747 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA14); 748 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA15); 749 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC1); 750 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC2); 751 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC3); 752 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC4); 753 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_VERSION); 754 cursorColumnToContentValues(cursor, cv, ContactQuery.IS_PRIMARY); 755 cursorColumnToContentValues(cursor, cv, ContactQuery.IS_SUPERPRIMARY); 756 cursorColumnToContentValues(cursor, cv, ContactQuery.MIMETYPE); 757 cursorColumnToContentValues(cursor, cv, ContactQuery.RES_PACKAGE); 758 cursorColumnToContentValues(cursor, cv, ContactQuery.GROUP_SOURCE_ID); 759 cursorColumnToContentValues(cursor, cv, ContactQuery.CHAT_CAPABILITY); 760 761 return cv; 762 } 763 764 private void cursorColumnToContentValues( 765 Cursor cursor, ContentValues values, int index) { 766 switch (cursor.getType(index)) { 767 case Cursor.FIELD_TYPE_NULL: 768 // don't put anything in the content values 769 break; 770 case Cursor.FIELD_TYPE_INTEGER: 771 values.put(ContactQuery.COLUMNS[index], cursor.getLong(index)); 772 break; 773 case Cursor.FIELD_TYPE_STRING: 774 values.put(ContactQuery.COLUMNS[index], cursor.getString(index)); 775 break; 776 case Cursor.FIELD_TYPE_BLOB: 777 values.put(ContactQuery.COLUMNS[index], cursor.getBlob(index)); 778 break; 779 default: 780 throw new IllegalStateException("Invalid or unhandled data type"); 781 } 782 } 783 784 private void loadDirectoryMetaData(Result result) { 785 long directoryId = result.getDirectoryId(); 786 787 Cursor cursor = getContext().getContentResolver().query( 788 ContentUris.withAppendedId(Directory.CONTENT_URI, directoryId), 789 DirectoryQuery.COLUMNS, null, null, null); 790 if (cursor == null) { 791 return; 792 } 793 try { 794 if (cursor.moveToFirst()) { 795 final String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME); 796 final String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME); 797 final int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID); 798 final String accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE); 799 final String accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME); 800 final int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT); 801 String directoryType = null; 802 if (!TextUtils.isEmpty(packageName)) { 803 PackageManager pm = getContext().getPackageManager(); 804 try { 805 Resources resources = pm.getResourcesForApplication(packageName); 806 directoryType = resources.getString(typeResourceId); 807 } catch (NameNotFoundException e) { 808 Log.w(TAG, "Contact directory resource not found: " 809 + packageName + "." + typeResourceId); 810 } 811 } 812 813 result.setDirectoryMetaData( 814 displayName, directoryType, accountType, accountName, exportSupport); 815 } 816 } finally { 817 cursor.close(); 818 } 819 } 820 821 /** 822 * Loads groups meta-data for all groups associated with all constituent raw contacts' 823 * accounts. 824 */ 825 private void loadGroupMetaData(Result result) { 826 StringBuilder selection = new StringBuilder(); 827 ArrayList<String> selectionArgs = new ArrayList<String>(); 828 for (Entity entity : result.mEntities) { 829 ContentValues values = entity.getEntityValues(); 830 String accountName = values.getAsString(RawContacts.ACCOUNT_NAME); 831 String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE); 832 if (accountName != null && accountType != null) { 833 if (selection.length() != 0) { 834 selection.append(" OR "); 835 } 836 selection.append( 837 "(" + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?)"); 838 selectionArgs.add(accountName); 839 selectionArgs.add(accountType); 840 } 841 } 842 Cursor cursor = getContext().getContentResolver().query(Groups.CONTENT_URI, 843 GroupQuery.COLUMNS, selection.toString(), selectionArgs.toArray(new String[0]), 844 null); 845 try { 846 while (cursor.moveToNext()) { 847 final String accountName = cursor.getString(GroupQuery.ACCOUNT_NAME); 848 final String accountType = cursor.getString(GroupQuery.ACCOUNT_TYPE); 849 final long groupId = cursor.getLong(GroupQuery.ID); 850 final String title = cursor.getString(GroupQuery.TITLE); 851 final boolean defaultGroup = cursor.isNull(GroupQuery.AUTO_ADD) 852 ? false 853 : cursor.getInt(GroupQuery.AUTO_ADD) != 0; 854 final boolean favorites = cursor.isNull(GroupQuery.FAVORITES) 855 ? false 856 : cursor.getInt(GroupQuery.FAVORITES) != 0; 857 858 result.addGroupMetaData(new GroupMetaData( 859 accountName, accountType, groupId, title, defaultGroup, favorites)); 860 } 861 } finally { 862 cursor.close(); 863 } 864 } 865 866 @Override 867 protected void onPostExecute(Result result) { 868 unregisterObserver(); 869 870 // The creator isn't interested in any further updates 871 if (mDestroyed || result == null) { 872 return; 873 } 874 875 mContact = result; 876 877 if (result != Result.ERROR && result != Result.NOT_FOUND) { 878 mLookupUri = result.getLookupUri(); 879 880 if (!result.isDirectoryEntry()) { 881 Log.i(TAG, "Registering content observer for " + mLookupUri); 882 if (mObserver == null) { 883 mObserver = new ForceLoadContentObserver(); 884 } 885 getContext().getContentResolver().registerContentObserver( 886 mLookupUri, true, mObserver); 887 } 888 889 if (mContact.getPhotoBinaryData() == null && mContact.getPhotoUri() != null) { 890 mContact.setLoadingPhoto(true); 891 new AsyncPhotoLoader().execute(mContact.getPhotoUri()); 892 } 893 } 894 895 deliverResult(mContact); 896 } 897 } 898 899 private class AsyncPhotoLoader extends AsyncTask<String, Void, byte[]> { 900 901 private static final int BUFFER_SIZE = 1024*16; 902 903 @Override 904 protected byte[] doInBackground(String... params) { 905 Uri uri = Uri.parse(params[0]); 906 byte[] data = null; 907 try { 908 InputStream is = getContext().getContentResolver().openInputStream(uri); 909 if (is != null) { 910 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 911 try { 912 byte[] mBuffer = new byte[BUFFER_SIZE]; 913 914 int size; 915 while ((size = is.read(mBuffer)) != -1) { 916 baos.write(mBuffer, 0, size); 917 } 918 data = baos.toByteArray(); 919 } finally { 920 is.close(); 921 } 922 } else { 923 Log.v(TAG, "Cannot load photo " + uri); 924 } 925 } catch (IOException e) { 926 Log.e(TAG, "Cannot load photo " + uri, e); 927 } 928 929 return data; 930 } 931 932 @Override 933 protected void onPostExecute(byte[] data) { 934 if (mContact != null) { 935 mContact.setPhotoBinaryData(data); 936 mContact.setLoadingPhoto(false); 937 deliverResult(mContact); 938 } 939 } 940 } 941 942 private void unregisterObserver() { 943 if (mObserver != null) { 944 getContext().getContentResolver().unregisterContentObserver(mObserver); 945 mObserver = null; 946 } 947 } 948 949 public ContactLoader(Context context, Uri lookupUri) { 950 this(context, lookupUri, false); 951 } 952 953 public ContactLoader(Context context, Uri lookupUri, boolean loadGroupMetaData) { 954 super(context); 955 mLookupUri = lookupUri; 956 mLoadGroupMetaData = loadGroupMetaData; 957 } 958 959 public Uri getLookupUri() { 960 return mLookupUri; 961 } 962 963 @Override 964 protected void onStartLoading() { 965 if (mContact != null) { 966 deliverResult(mContact); 967 } 968 969 if (takeContentChanged() || mContact == null) { 970 forceLoad(); 971 } 972 } 973 974 @Override 975 protected void onForceLoad() { 976 final LoadContactTask task = new LoadContactTask(); 977 task.execute((Void[])null); 978 } 979 980 @Override 981 protected void onReset() { 982 unregisterObserver(); 983 mContact = null; 984 mDestroyed = true; 985 } 986} 987