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 17 package com.android.contacts; 18 19 import com.android.contacts.model.AccountType; 20 import com.android.contacts.model.AccountTypeManager; 21 import com.android.contacts.model.AccountTypeWithDataSet; 22 import com.android.contacts.model.EntityDeltaList; 23 import com.android.contacts.util.ContactLoaderUtils; 24 import com.android.contacts.util.DataStatus; 25 import com.android.contacts.util.StreamItemEntry; 26 import com.android.contacts.util.StreamItemPhotoEntry; 27 import com.android.contacts.util.UriUtils; 28 import com.google.common.annotations.VisibleForTesting; 29 import com.google.common.collect.Lists; 30 import com.google.common.collect.Maps; 31 import com.google.common.collect.Sets; 32 33 import android.content.AsyncTaskLoader; 34 import android.content.ContentResolver; 35 import android.content.ContentUris; 36 import android.content.ContentValues; 37 import android.content.Context; 38 import android.content.Entity; 39 import android.content.Entity.NamedContentValues; 40 import android.content.Intent; 41 import android.content.pm.PackageManager; 42 import android.content.pm.PackageManager.NameNotFoundException; 43 import android.content.res.AssetFileDescriptor; 44 import android.content.res.Resources; 45 import android.database.Cursor; 46 import android.net.Uri; 47 import android.provider.ContactsContract; 48 import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 49 import android.provider.ContactsContract.CommonDataKinds.Photo; 50 import android.provider.ContactsContract.Contacts; 51 import android.provider.ContactsContract.Data; 52 import android.provider.ContactsContract.Directory; 53 import android.provider.ContactsContract.DisplayNameSources; 54 import android.provider.ContactsContract.Groups; 55 import android.provider.ContactsContract.RawContacts; 56 import android.provider.ContactsContract.StreamItemPhotos; 57 import android.provider.ContactsContract.StreamItems; 58 import android.text.TextUtils; 59 import android.util.Log; 60 import android.util.LongSparseArray; 61 62 import java.io.ByteArrayOutputStream; 63 import java.io.FileInputStream; 64 import java.io.IOException; 65 import java.util.ArrayList; 66 import java.util.Collections; 67 import java.util.List; 68 import java.util.Map; 69 import java.util.Set; 70 71 /** 72 * Loads a single Contact and all it constituent RawContacts. 73 */ 74 public class ContactLoader extends AsyncTaskLoader<ContactLoader.Result> { 75 private static final String TAG = ContactLoader.class.getSimpleName(); 76 77 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 78 79 /** A short-lived cache that can be set by {@link #cacheResult()} */ 80 private static Result sCachedResult = null; 81 82 private final Uri mRequestedUri; 83 private Uri mLookupUri; 84 private boolean mLoadGroupMetaData; 85 private boolean mLoadStreamItems; 86 private boolean mLoadInvitableAccountTypes; 87 private boolean mPostViewNotification; 88 private Result mContact; 89 private ForceLoadContentObserver mObserver; 90 private final Set<Long> mNotifiedRawContactIds = Sets.newHashSet(); 91 ContactLoader(Context context, Uri lookupUri, boolean postViewNotification)92 public ContactLoader(Context context, Uri lookupUri, boolean postViewNotification) { 93 this(context, lookupUri, false, false, false, postViewNotification); 94 } 95 ContactLoader(Context context, Uri lookupUri, boolean loadGroupMetaData, boolean loadStreamItems, boolean loadInvitableAccountTypes, boolean postViewNotification)96 public ContactLoader(Context context, Uri lookupUri, boolean loadGroupMetaData, 97 boolean loadStreamItems, boolean loadInvitableAccountTypes, 98 boolean postViewNotification) { 99 super(context); 100 mLookupUri = lookupUri; 101 mRequestedUri = lookupUri; 102 mLoadGroupMetaData = loadGroupMetaData; 103 mLoadStreamItems = loadStreamItems; 104 mLoadInvitableAccountTypes = loadInvitableAccountTypes; 105 mPostViewNotification = postViewNotification; 106 } 107 108 /** 109 * The result of a load operation. Contains all data necessary to display the contact. 110 */ 111 public static final class Result { 112 private enum Status { 113 /** Contact is successfully loaded */ 114 LOADED, 115 /** There was an error loading the contact */ 116 ERROR, 117 /** Contact is not found */ 118 NOT_FOUND, 119 } 120 121 private final Uri mRequestedUri; 122 private final Uri mLookupUri; 123 private final Uri mUri; 124 private final long mDirectoryId; 125 private final String mLookupKey; 126 private final long mId; 127 private final long mNameRawContactId; 128 private final int mDisplayNameSource; 129 private final long mPhotoId; 130 private final String mPhotoUri; 131 private final String mDisplayName; 132 private final String mAltDisplayName; 133 private final String mPhoneticName; 134 private final boolean mStarred; 135 private final Integer mPresence; 136 private final ArrayList<Entity> mEntities; 137 private ArrayList<StreamItemEntry> mStreamItems; 138 private final LongSparseArray<DataStatus> mStatuses; 139 private ArrayList<AccountType> mInvitableAccountTypes; 140 141 private String mDirectoryDisplayName; 142 private String mDirectoryType; 143 private String mDirectoryAccountType; 144 private String mDirectoryAccountName; 145 private int mDirectoryExportSupport; 146 147 private ArrayList<GroupMetaData> mGroups; 148 149 private byte[] mPhotoBinaryData; 150 private final boolean mSendToVoicemail; 151 private final String mCustomRingtone; 152 private final boolean mIsUserProfile; 153 154 private final Status mStatus; 155 private final Exception mException; 156 157 /** 158 * Constructor for special results, namely "no contact found" and "error". 159 */ Result(Uri requestedUri, Status status, Exception exception)160 private Result(Uri requestedUri, Status status, Exception exception) { 161 if (status == Status.ERROR && exception == null) { 162 throw new IllegalArgumentException("ERROR result must have exception"); 163 } 164 mStatus = status; 165 mException = exception; 166 mRequestedUri = requestedUri; 167 mLookupUri = null; 168 mUri = null; 169 mDirectoryId = -1; 170 mLookupKey = null; 171 mId = -1; 172 mEntities = null; 173 mStreamItems = null; 174 mStatuses = null; 175 mNameRawContactId = -1; 176 mDisplayNameSource = DisplayNameSources.UNDEFINED; 177 mPhotoId = -1; 178 mPhotoUri = null; 179 mDisplayName = null; 180 mAltDisplayName = null; 181 mPhoneticName = null; 182 mStarred = false; 183 mPresence = null; 184 mInvitableAccountTypes = null; 185 mSendToVoicemail = false; 186 mCustomRingtone = null; 187 mIsUserProfile = false; 188 } 189 forError(Uri requestedUri, Exception exception)190 private static Result forError(Uri requestedUri, Exception exception) { 191 return new Result(requestedUri, Status.ERROR, exception); 192 } 193 forNotFound(Uri requestedUri)194 private static Result forNotFound(Uri requestedUri) { 195 return new Result(requestedUri, Status.NOT_FOUND, null); 196 } 197 198 /** 199 * Constructor to call when contact was found 200 */ Result(Uri requestedUri, Uri uri, Uri lookupUri, long directoryId, String lookupKey, long id, long nameRawContactId, int displayNameSource, long photoId, String photoUri, String displayName, String altDisplayName, String phoneticName, boolean starred, Integer presence, boolean sendToVoicemail, String customRingtone, boolean isUserProfile)201 private Result(Uri requestedUri, Uri uri, Uri lookupUri, long directoryId, String lookupKey, 202 long id, long nameRawContactId, int displayNameSource, long photoId, 203 String photoUri, String displayName, String altDisplayName, String phoneticName, 204 boolean starred, Integer presence, boolean sendToVoicemail, String customRingtone, 205 boolean isUserProfile) { 206 mStatus = Status.LOADED; 207 mException = null; 208 mRequestedUri = requestedUri; 209 mLookupUri = lookupUri; 210 mUri = uri; 211 mDirectoryId = directoryId; 212 mLookupKey = lookupKey; 213 mId = id; 214 mEntities = new ArrayList<Entity>(); 215 mStreamItems = null; 216 mStatuses = new LongSparseArray<DataStatus>(); 217 mNameRawContactId = nameRawContactId; 218 mDisplayNameSource = displayNameSource; 219 mPhotoId = photoId; 220 mPhotoUri = photoUri; 221 mDisplayName = displayName; 222 mAltDisplayName = altDisplayName; 223 mPhoneticName = phoneticName; 224 mStarred = starred; 225 mPresence = presence; 226 mInvitableAccountTypes = null; 227 mSendToVoicemail = sendToVoicemail; 228 mCustomRingtone = customRingtone; 229 mIsUserProfile = isUserProfile; 230 } 231 Result(Uri requestedUri, Result from)232 private Result(Uri requestedUri, Result from) { 233 mRequestedUri = requestedUri; 234 235 mStatus = from.mStatus; 236 mException = from.mException; 237 mLookupUri = from.mLookupUri; 238 mUri = from.mUri; 239 mDirectoryId = from.mDirectoryId; 240 mLookupKey = from.mLookupKey; 241 mId = from.mId; 242 mNameRawContactId = from.mNameRawContactId; 243 mDisplayNameSource = from.mDisplayNameSource; 244 mPhotoId = from.mPhotoId; 245 mPhotoUri = from.mPhotoUri; 246 mDisplayName = from.mDisplayName; 247 mAltDisplayName = from.mAltDisplayName; 248 mPhoneticName = from.mPhoneticName; 249 mStarred = from.mStarred; 250 mPresence = from.mPresence; 251 mEntities = from.mEntities; 252 mStreamItems = from.mStreamItems; 253 mStatuses = from.mStatuses; 254 mInvitableAccountTypes = from.mInvitableAccountTypes; 255 256 mDirectoryDisplayName = from.mDirectoryDisplayName; 257 mDirectoryType = from.mDirectoryType; 258 mDirectoryAccountType = from.mDirectoryAccountType; 259 mDirectoryAccountName = from.mDirectoryAccountName; 260 mDirectoryExportSupport = from.mDirectoryExportSupport; 261 262 mGroups = from.mGroups; 263 264 mPhotoBinaryData = from.mPhotoBinaryData; 265 mSendToVoicemail = from.mSendToVoicemail; 266 mCustomRingtone = from.mCustomRingtone; 267 mIsUserProfile = from.mIsUserProfile; 268 } 269 270 /** 271 * @param exportSupport See {@link Directory#EXPORT_SUPPORT}. 272 */ setDirectoryMetaData(String displayName, String directoryType, String accountType, String accountName, int exportSupport)273 private void setDirectoryMetaData(String displayName, String directoryType, 274 String accountType, String accountName, int exportSupport) { 275 mDirectoryDisplayName = displayName; 276 mDirectoryType = directoryType; 277 mDirectoryAccountType = accountType; 278 mDirectoryAccountName = accountName; 279 mDirectoryExportSupport = exportSupport; 280 } 281 setPhotoBinaryData(byte[] photoBinaryData)282 private void setPhotoBinaryData(byte[] photoBinaryData) { 283 mPhotoBinaryData = photoBinaryData; 284 } 285 286 /** 287 * Returns the URI for the contact that contains both the lookup key and the ID. This is 288 * the best URI to reference a contact. 289 * For directory contacts, this is the same a the URI as returned by {@link #getUri()} 290 */ getLookupUri()291 public Uri getLookupUri() { 292 return mLookupUri; 293 } 294 getLookupKey()295 public String getLookupKey() { 296 return mLookupKey; 297 } 298 299 /** 300 * Returns the contact Uri that was passed to the provider to make the query. This is 301 * the same as the requested Uri, unless the requested Uri doesn't specify a Contact: 302 * If it either references a Raw-Contact or a Person (a pre-Eclair style Uri), this Uri will 303 * always reference the full aggregate contact. 304 */ getUri()305 public Uri getUri() { 306 return mUri; 307 } 308 309 /** 310 * Returns the URI for which this {@link ContactLoader) was initially requested. 311 */ getRequestedUri()312 public Uri getRequestedUri() { 313 return mRequestedUri; 314 } 315 316 /** 317 * Instantiate a new EntityDeltaList for this contact. 318 */ createEntityDeltaList()319 public EntityDeltaList createEntityDeltaList() { 320 return EntityDeltaList.fromIterator(getEntities().iterator()); 321 } 322 323 /** 324 * Returns the contact ID. 325 */ 326 @VisibleForTesting getId()327 /* package */ long getId() { 328 return mId; 329 } 330 331 /** 332 * @return true when an exception happened during loading, in which case 333 * {@link #getException} returns the actual exception object. 334 * Note {@link #isNotFound()} and {@link #isError()} are mutually exclusive; If 335 * {@link #isError()} is {@code true}, {@link #isNotFound()} is always {@code false}, 336 * and vice versa. 337 */ isError()338 public boolean isError() { 339 return mStatus == Status.ERROR; 340 } 341 getException()342 public Exception getException() { 343 return mException; 344 } 345 346 /** 347 * @return true when the specified contact is not found. 348 * Note {@link #isNotFound()} and {@link #isError()} are mutually exclusive; If 349 * {@link #isError()} is {@code true}, {@link #isNotFound()} is always {@code false}, 350 * and vice versa. 351 */ isNotFound()352 public boolean isNotFound() { 353 return mStatus == Status.NOT_FOUND; 354 } 355 356 /** 357 * @return true if the specified contact is successfully loaded. 358 * i.e. neither {@link #isError()} nor {@link #isNotFound()}. 359 */ isLoaded()360 public boolean isLoaded() { 361 return mStatus == Status.LOADED; 362 } 363 getNameRawContactId()364 public long getNameRawContactId() { 365 return mNameRawContactId; 366 } 367 getDisplayNameSource()368 public int getDisplayNameSource() { 369 return mDisplayNameSource; 370 } 371 getPhotoId()372 public long getPhotoId() { 373 return mPhotoId; 374 } 375 getPhotoUri()376 public String getPhotoUri() { 377 return mPhotoUri; 378 } 379 getDisplayName()380 public String getDisplayName() { 381 return mDisplayName; 382 } 383 getAltDisplayName()384 public String getAltDisplayName() { 385 return mAltDisplayName; 386 } 387 getPhoneticName()388 public String getPhoneticName() { 389 return mPhoneticName; 390 } 391 getStarred()392 public boolean getStarred() { 393 return mStarred; 394 } 395 getPresence()396 public Integer getPresence() { 397 return mPresence; 398 } 399 getInvitableAccountTypes()400 public ArrayList<AccountType> getInvitableAccountTypes() { 401 return mInvitableAccountTypes; 402 } 403 getEntities()404 public ArrayList<Entity> getEntities() { 405 return mEntities; 406 } 407 getStreamItems()408 public ArrayList<StreamItemEntry> getStreamItems() { 409 return mStreamItems; 410 } 411 getStatuses()412 public LongSparseArray<DataStatus> getStatuses() { 413 return mStatuses; 414 } 415 getDirectoryId()416 public long getDirectoryId() { 417 return mDirectoryId; 418 } 419 isDirectoryEntry()420 public boolean isDirectoryEntry() { 421 return mDirectoryId != -1 && mDirectoryId != Directory.DEFAULT 422 && mDirectoryId != Directory.LOCAL_INVISIBLE; 423 } 424 425 /** 426 * @return true if this is a contact (not group, etc.) with at least one 427 * writable raw-contact, and false otherwise. 428 */ isWritableContact(final Context context)429 public boolean isWritableContact(final Context context) { 430 return getFirstWritableRawContactId(context) != -1; 431 } 432 433 /** 434 * Return the ID of the first raw-contact in the contact data that belongs to a 435 * contact-writable account, or -1 if no such entity exists. 436 */ getFirstWritableRawContactId(final Context context)437 public long getFirstWritableRawContactId(final Context context) { 438 // Directory entries are non-writable 439 if (isDirectoryEntry()) return -1; 440 441 // Iterate through raw-contacts; if we find a writable on, return its ID. 442 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(context); 443 for (Entity entity : getEntities()) { 444 ContentValues values = entity.getEntityValues(); 445 String type = values.getAsString(RawContacts.ACCOUNT_TYPE); 446 String dataSet = values.getAsString(RawContacts.DATA_SET); 447 448 AccountType accountType = accountTypes.getAccountType(type, dataSet); 449 if (accountType != null && accountType.areContactsWritable()) { 450 return values.getAsLong(RawContacts._ID); 451 } 452 } 453 // No writable raw-contact was found. 454 return -1; 455 } 456 getDirectoryExportSupport()457 public int getDirectoryExportSupport() { 458 return mDirectoryExportSupport; 459 } 460 getDirectoryDisplayName()461 public String getDirectoryDisplayName() { 462 return mDirectoryDisplayName; 463 } 464 getDirectoryType()465 public String getDirectoryType() { 466 return mDirectoryType; 467 } 468 getDirectoryAccountType()469 public String getDirectoryAccountType() { 470 return mDirectoryAccountType; 471 } 472 getDirectoryAccountName()473 public String getDirectoryAccountName() { 474 return mDirectoryAccountName; 475 } 476 getPhotoBinaryData()477 public byte[] getPhotoBinaryData() { 478 return mPhotoBinaryData; 479 } 480 getContentValues()481 public ArrayList<ContentValues> getContentValues() { 482 if (mEntities.size() != 1) { 483 throw new IllegalStateException( 484 "Cannot extract content values from an aggregated contact"); 485 } 486 487 Entity entity = mEntities.get(0); 488 ArrayList<ContentValues> result = new ArrayList<ContentValues>(); 489 ArrayList<NamedContentValues> subValues = entity.getSubValues(); 490 if (subValues != null) { 491 int size = subValues.size(); 492 for (int i = 0; i < size; i++) { 493 NamedContentValues pair = subValues.get(i); 494 if (Data.CONTENT_URI.equals(pair.uri)) { 495 result.add(pair.values); 496 } 497 } 498 } 499 500 // If the photo was loaded using the URI, create an entry for the photo 501 // binary data. 502 if (mPhotoId == 0 && mPhotoBinaryData != null) { 503 ContentValues photo = new ContentValues(); 504 photo.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE); 505 photo.put(Photo.PHOTO, mPhotoBinaryData); 506 result.add(photo); 507 } 508 509 return result; 510 } 511 getGroupMetaData()512 public List<GroupMetaData> getGroupMetaData() { 513 return mGroups; 514 } 515 isSendToVoicemail()516 public boolean isSendToVoicemail() { 517 return mSendToVoicemail; 518 } 519 getCustomRingtone()520 public String getCustomRingtone() { 521 return mCustomRingtone; 522 } 523 isUserProfile()524 public boolean isUserProfile() { 525 return mIsUserProfile; 526 } 527 528 @Override toString()529 public String toString() { 530 return "{requested=" + mRequestedUri + ",lookupkey=" + mLookupKey + 531 ",uri=" + mUri + ",status=" + mStatus + "}"; 532 } 533 } 534 535 /** 536 * Projection used for the query that loads all data for the entire contact (except for 537 * social stream items). 538 */ 539 private static class ContactQuery { 540 final static String[] COLUMNS = new String[] { 541 Contacts.NAME_RAW_CONTACT_ID, 542 Contacts.DISPLAY_NAME_SOURCE, 543 Contacts.LOOKUP_KEY, 544 Contacts.DISPLAY_NAME, 545 Contacts.DISPLAY_NAME_ALTERNATIVE, 546 Contacts.PHONETIC_NAME, 547 Contacts.PHOTO_ID, 548 Contacts.STARRED, 549 Contacts.CONTACT_PRESENCE, 550 Contacts.CONTACT_STATUS, 551 Contacts.CONTACT_STATUS_TIMESTAMP, 552 Contacts.CONTACT_STATUS_RES_PACKAGE, 553 Contacts.CONTACT_STATUS_LABEL, 554 Contacts.Entity.CONTACT_ID, 555 Contacts.Entity.RAW_CONTACT_ID, 556 557 RawContacts.ACCOUNT_NAME, 558 RawContacts.ACCOUNT_TYPE, 559 RawContacts.DATA_SET, 560 RawContacts.ACCOUNT_TYPE_AND_DATA_SET, 561 RawContacts.DIRTY, 562 RawContacts.VERSION, 563 RawContacts.SOURCE_ID, 564 RawContacts.SYNC1, 565 RawContacts.SYNC2, 566 RawContacts.SYNC3, 567 RawContacts.SYNC4, 568 RawContacts.DELETED, 569 RawContacts.NAME_VERIFIED, 570 571 Contacts.Entity.DATA_ID, 572 Data.DATA1, 573 Data.DATA2, 574 Data.DATA3, 575 Data.DATA4, 576 Data.DATA5, 577 Data.DATA6, 578 Data.DATA7, 579 Data.DATA8, 580 Data.DATA9, 581 Data.DATA10, 582 Data.DATA11, 583 Data.DATA12, 584 Data.DATA13, 585 Data.DATA14, 586 Data.DATA15, 587 Data.SYNC1, 588 Data.SYNC2, 589 Data.SYNC3, 590 Data.SYNC4, 591 Data.DATA_VERSION, 592 Data.IS_PRIMARY, 593 Data.IS_SUPER_PRIMARY, 594 Data.MIMETYPE, 595 Data.RES_PACKAGE, 596 597 GroupMembership.GROUP_SOURCE_ID, 598 599 Data.PRESENCE, 600 Data.CHAT_CAPABILITY, 601 Data.STATUS, 602 Data.STATUS_RES_PACKAGE, 603 Data.STATUS_ICON, 604 Data.STATUS_LABEL, 605 Data.STATUS_TIMESTAMP, 606 607 Contacts.PHOTO_URI, 608 Contacts.SEND_TO_VOICEMAIL, 609 Contacts.CUSTOM_RINGTONE, 610 Contacts.IS_USER_PROFILE, 611 }; 612 613 public final static int NAME_RAW_CONTACT_ID = 0; 614 public final static int DISPLAY_NAME_SOURCE = 1; 615 public final static int LOOKUP_KEY = 2; 616 public final static int DISPLAY_NAME = 3; 617 public final static int ALT_DISPLAY_NAME = 4; 618 public final static int PHONETIC_NAME = 5; 619 public final static int PHOTO_ID = 6; 620 public final static int STARRED = 7; 621 public final static int CONTACT_PRESENCE = 8; 622 public final static int CONTACT_STATUS = 9; 623 public final static int CONTACT_STATUS_TIMESTAMP = 10; 624 public final static int CONTACT_STATUS_RES_PACKAGE = 11; 625 public final static int CONTACT_STATUS_LABEL = 12; 626 public final static int CONTACT_ID = 13; 627 public final static int RAW_CONTACT_ID = 14; 628 629 public final static int ACCOUNT_NAME = 15; 630 public final static int ACCOUNT_TYPE = 16; 631 public final static int DATA_SET = 17; 632 public final static int ACCOUNT_TYPE_AND_DATA_SET = 18; 633 public final static int DIRTY = 19; 634 public final static int VERSION = 20; 635 public final static int SOURCE_ID = 21; 636 public final static int SYNC1 = 22; 637 public final static int SYNC2 = 23; 638 public final static int SYNC3 = 24; 639 public final static int SYNC4 = 25; 640 public final static int DELETED = 26; 641 public final static int NAME_VERIFIED = 27; 642 643 public final static int DATA_ID = 28; 644 public final static int DATA1 = 29; 645 public final static int DATA2 = 30; 646 public final static int DATA3 = 31; 647 public final static int DATA4 = 32; 648 public final static int DATA5 = 33; 649 public final static int DATA6 = 34; 650 public final static int DATA7 = 35; 651 public final static int DATA8 = 36; 652 public final static int DATA9 = 37; 653 public final static int DATA10 = 38; 654 public final static int DATA11 = 39; 655 public final static int DATA12 = 40; 656 public final static int DATA13 = 41; 657 public final static int DATA14 = 42; 658 public final static int DATA15 = 43; 659 public final static int DATA_SYNC1 = 44; 660 public final static int DATA_SYNC2 = 45; 661 public final static int DATA_SYNC3 = 46; 662 public final static int DATA_SYNC4 = 47; 663 public final static int DATA_VERSION = 48; 664 public final static int IS_PRIMARY = 49; 665 public final static int IS_SUPERPRIMARY = 50; 666 public final static int MIMETYPE = 51; 667 public final static int RES_PACKAGE = 52; 668 669 public final static int GROUP_SOURCE_ID = 53; 670 671 public final static int PRESENCE = 54; 672 public final static int CHAT_CAPABILITY = 55; 673 public final static int STATUS = 56; 674 public final static int STATUS_RES_PACKAGE = 57; 675 public final static int STATUS_ICON = 58; 676 public final static int STATUS_LABEL = 59; 677 public final static int STATUS_TIMESTAMP = 60; 678 679 public final static int PHOTO_URI = 61; 680 public final static int SEND_TO_VOICEMAIL = 62; 681 public final static int CUSTOM_RINGTONE = 63; 682 public final static int IS_USER_PROFILE = 64; 683 } 684 685 /** 686 * Projection used for the query that loads all data for the entire contact. 687 */ 688 private static class DirectoryQuery { 689 final static String[] COLUMNS = new String[] { 690 Directory.DISPLAY_NAME, 691 Directory.PACKAGE_NAME, 692 Directory.TYPE_RESOURCE_ID, 693 Directory.ACCOUNT_TYPE, 694 Directory.ACCOUNT_NAME, 695 Directory.EXPORT_SUPPORT, 696 }; 697 698 public final static int DISPLAY_NAME = 0; 699 public final static int PACKAGE_NAME = 1; 700 public final static int TYPE_RESOURCE_ID = 2; 701 public final static int ACCOUNT_TYPE = 3; 702 public final static int ACCOUNT_NAME = 4; 703 public final static int EXPORT_SUPPORT = 5; 704 } 705 706 private static class GroupQuery { 707 final static String[] COLUMNS = new String[] { 708 Groups.ACCOUNT_NAME, 709 Groups.ACCOUNT_TYPE, 710 Groups.DATA_SET, 711 Groups.ACCOUNT_TYPE_AND_DATA_SET, 712 Groups._ID, 713 Groups.TITLE, 714 Groups.AUTO_ADD, 715 Groups.FAVORITES, 716 }; 717 718 public final static int ACCOUNT_NAME = 0; 719 public final static int ACCOUNT_TYPE = 1; 720 public final static int DATA_SET = 2; 721 public final static int ACCOUNT_TYPE_AND_DATA_SET = 3; 722 public final static int ID = 4; 723 public final static int TITLE = 5; 724 public final static int AUTO_ADD = 6; 725 public final static int FAVORITES = 7; 726 } 727 728 @Override loadInBackground()729 public Result loadInBackground() { 730 try { 731 final ContentResolver resolver = getContext().getContentResolver(); 732 final Uri uriCurrentFormat = ContactLoaderUtils.ensureIsContactUri( 733 resolver, mLookupUri); 734 final Result cachedResult = sCachedResult; 735 sCachedResult = null; 736 // Is this the same Uri as what we had before already? In that case, reuse that result 737 final Result result; 738 final boolean resultIsCached; 739 if (cachedResult != null && 740 UriUtils.areEqual(cachedResult.getLookupUri(), mLookupUri)) { 741 // We are using a cached result from earlier. Below, we should make sure 742 // we are not doing any more network or disc accesses 743 result = new Result(mRequestedUri, cachedResult); 744 resultIsCached = true; 745 } else { 746 result = loadContactEntity(resolver, uriCurrentFormat); 747 resultIsCached = false; 748 } 749 if (result.isLoaded()) { 750 if (result.isDirectoryEntry()) { 751 if (!resultIsCached) { 752 loadDirectoryMetaData(result); 753 } 754 } else if (mLoadGroupMetaData) { 755 if (result.getGroupMetaData() == null) { 756 loadGroupMetaData(result); 757 } 758 } 759 if (mLoadStreamItems && result.getStreamItems() == null) { 760 loadStreamItems(result); 761 } 762 if (!resultIsCached) loadPhotoBinaryData(result); 763 764 // Note ME profile should never have "Add connection" 765 if (mLoadInvitableAccountTypes && result.getInvitableAccountTypes() == null) { 766 loadInvitableAccountTypes(result); 767 } 768 } 769 return result; 770 } catch (Exception e) { 771 Log.e(TAG, "Error loading the contact: " + mLookupUri, e); 772 return Result.forError(mRequestedUri, e); 773 } 774 } 775 loadContactEntity(ContentResolver resolver, Uri contactUri)776 private Result loadContactEntity(ContentResolver resolver, Uri contactUri) { 777 Uri entityUri = Uri.withAppendedPath(contactUri, Contacts.Entity.CONTENT_DIRECTORY); 778 Cursor cursor = resolver.query(entityUri, ContactQuery.COLUMNS, null, null, 779 Contacts.Entity.RAW_CONTACT_ID); 780 if (cursor == null) { 781 Log.e(TAG, "No cursor returned in loadContactEntity"); 782 return Result.forNotFound(mRequestedUri); 783 } 784 785 try { 786 if (!cursor.moveToFirst()) { 787 cursor.close(); 788 return Result.forNotFound(mRequestedUri); 789 } 790 791 // Create the loaded result starting with the Contact data. 792 Result result = loadContactHeaderData(cursor, contactUri); 793 794 // Fill in the raw contacts, which is wrapped in an Entity and any 795 // status data. Initially, result has empty entities and statuses. 796 long currentRawContactId = -1; 797 Entity entity = null; 798 ArrayList<Entity> entities = result.getEntities(); 799 LongSparseArray<DataStatus> statuses = result.getStatuses(); 800 for (; !cursor.isAfterLast(); cursor.moveToNext()) { 801 long rawContactId = cursor.getLong(ContactQuery.RAW_CONTACT_ID); 802 if (rawContactId != currentRawContactId) { 803 // First time to see this raw contact id, so create a new entity, and 804 // add it to the result's entities. 805 currentRawContactId = rawContactId; 806 entity = new android.content.Entity(loadRawContact(cursor)); 807 entities.add(entity); 808 } 809 if (!cursor.isNull(ContactQuery.DATA_ID)) { 810 ContentValues data = loadData(cursor); 811 entity.addSubValue(ContactsContract.Data.CONTENT_URI, data); 812 813 if (!cursor.isNull(ContactQuery.PRESENCE) 814 || !cursor.isNull(ContactQuery.STATUS)) { 815 final DataStatus status = new DataStatus(cursor); 816 final long dataId = cursor.getLong(ContactQuery.DATA_ID); 817 statuses.put(dataId, status); 818 } 819 } 820 } 821 822 return result; 823 } finally { 824 cursor.close(); 825 } 826 } 827 828 /** 829 * Looks for the photo data item in entities. If found, creates a new Bitmap instance. If 830 * not found, returns null 831 */ loadPhotoBinaryData(Result contactData)832 private void loadPhotoBinaryData(Result contactData) { 833 834 // If we have a photo URI, try loading that first. 835 String photoUri = contactData.getPhotoUri(); 836 if (photoUri != null) { 837 try { 838 AssetFileDescriptor fd = getContext().getContentResolver() 839 .openAssetFileDescriptor(Uri.parse(photoUri), "r"); 840 byte[] buffer = new byte[16 * 1024]; 841 FileInputStream fis = fd.createInputStream(); 842 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 843 try { 844 int size; 845 while ((size = fis.read(buffer)) != -1) { 846 baos.write(buffer, 0, size); 847 } 848 contactData.setPhotoBinaryData(baos.toByteArray()); 849 } finally { 850 fis.close(); 851 fd.close(); 852 } 853 return; 854 } catch (IOException ioe) { 855 // Just fall back to the case below. 856 } 857 } 858 859 // If we couldn't load from a file, fall back to the data blob. 860 final long photoId = contactData.getPhotoId(); 861 if (photoId <= 0) { 862 // No photo ID 863 return; 864 } 865 866 for (Entity entity : contactData.getEntities()) { 867 for (NamedContentValues subValue : entity.getSubValues()) { 868 final ContentValues entryValues = subValue.values; 869 final long dataId = entryValues.getAsLong(Data._ID); 870 if (dataId == photoId) { 871 final String mimeType = entryValues.getAsString(Data.MIMETYPE); 872 // Correct Data Id but incorrect MimeType? Don't load 873 if (!Photo.CONTENT_ITEM_TYPE.equals(mimeType)) { 874 return; 875 } 876 contactData.setPhotoBinaryData(entryValues.getAsByteArray(Photo.PHOTO)); 877 break; 878 } 879 } 880 } 881 } 882 883 /** 884 * Sets the "invitable" account types to {@link Result#mInvitableAccountTypes}. 885 */ loadInvitableAccountTypes(Result contactData)886 private void loadInvitableAccountTypes(Result contactData) { 887 final ArrayList<AccountType> resultList = Lists.newArrayList(); 888 if (!contactData.isUserProfile()) { 889 Map<AccountTypeWithDataSet, AccountType> invitables = 890 AccountTypeManager.getInstance(getContext()).getUsableInvitableAccountTypes(); 891 if (!invitables.isEmpty()) { 892 final Map<AccountTypeWithDataSet, AccountType> resultMap = 893 Maps.newHashMap(invitables); 894 895 // Remove the ones that already have a raw contact in the current contact 896 for (Entity entity : contactData.getEntities()) { 897 final ContentValues values = entity.getEntityValues(); 898 final AccountTypeWithDataSet type = AccountTypeWithDataSet.get( 899 values.getAsString(RawContacts.ACCOUNT_TYPE), 900 values.getAsString(RawContacts.DATA_SET)); 901 resultMap.remove(type); 902 } 903 904 resultList.addAll(resultMap.values()); 905 } 906 } 907 908 // Set to mInvitableAccountTypes 909 contactData.mInvitableAccountTypes = resultList; 910 } 911 912 /** 913 * Extracts Contact level columns from the cursor. 914 */ loadContactHeaderData(final Cursor cursor, Uri contactUri)915 private Result loadContactHeaderData(final Cursor cursor, Uri contactUri) { 916 final String directoryParameter = 917 contactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY); 918 final long directoryId = directoryParameter == null 919 ? Directory.DEFAULT 920 : Long.parseLong(directoryParameter); 921 final long contactId = cursor.getLong(ContactQuery.CONTACT_ID); 922 final String lookupKey = cursor.getString(ContactQuery.LOOKUP_KEY); 923 final long nameRawContactId = cursor.getLong(ContactQuery.NAME_RAW_CONTACT_ID); 924 final int displayNameSource = cursor.getInt(ContactQuery.DISPLAY_NAME_SOURCE); 925 final String displayName = cursor.getString(ContactQuery.DISPLAY_NAME); 926 final String altDisplayName = cursor.getString(ContactQuery.ALT_DISPLAY_NAME); 927 final String phoneticName = cursor.getString(ContactQuery.PHONETIC_NAME); 928 final long photoId = cursor.getLong(ContactQuery.PHOTO_ID); 929 final String photoUri = cursor.getString(ContactQuery.PHOTO_URI); 930 final boolean starred = cursor.getInt(ContactQuery.STARRED) != 0; 931 final Integer presence = cursor.isNull(ContactQuery.CONTACT_PRESENCE) 932 ? null 933 : cursor.getInt(ContactQuery.CONTACT_PRESENCE); 934 final boolean sendToVoicemail = cursor.getInt(ContactQuery.SEND_TO_VOICEMAIL) == 1; 935 final String customRingtone = cursor.getString(ContactQuery.CUSTOM_RINGTONE); 936 final boolean isUserProfile = cursor.getInt(ContactQuery.IS_USER_PROFILE) == 1; 937 938 Uri lookupUri; 939 if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) { 940 lookupUri = ContentUris.withAppendedId( 941 Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), contactId); 942 } else { 943 lookupUri = contactUri; 944 } 945 946 return new Result(mRequestedUri, contactUri, lookupUri, directoryId, lookupKey, 947 contactId, nameRawContactId, displayNameSource, photoId, photoUri, displayName, 948 altDisplayName, phoneticName, starred, presence, sendToVoicemail, 949 customRingtone, isUserProfile); 950 } 951 952 /** 953 * Extracts RawContact level columns from the cursor. 954 */ loadRawContact(Cursor cursor)955 private ContentValues loadRawContact(Cursor cursor) { 956 ContentValues cv = new ContentValues(); 957 958 cv.put(RawContacts._ID, cursor.getLong(ContactQuery.RAW_CONTACT_ID)); 959 960 cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_NAME); 961 cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE); 962 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SET); 963 cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE_AND_DATA_SET); 964 cursorColumnToContentValues(cursor, cv, ContactQuery.DIRTY); 965 cursorColumnToContentValues(cursor, cv, ContactQuery.VERSION); 966 cursorColumnToContentValues(cursor, cv, ContactQuery.SOURCE_ID); 967 cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC1); 968 cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC2); 969 cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC3); 970 cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC4); 971 cursorColumnToContentValues(cursor, cv, ContactQuery.DELETED); 972 cursorColumnToContentValues(cursor, cv, ContactQuery.CONTACT_ID); 973 cursorColumnToContentValues(cursor, cv, ContactQuery.STARRED); 974 cursorColumnToContentValues(cursor, cv, ContactQuery.NAME_VERIFIED); 975 976 return cv; 977 } 978 979 /** 980 * Extracts Data level columns from the cursor. 981 */ loadData(Cursor cursor)982 private ContentValues loadData(Cursor cursor) { 983 ContentValues cv = new ContentValues(); 984 985 cv.put(Data._ID, cursor.getLong(ContactQuery.DATA_ID)); 986 987 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA1); 988 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA2); 989 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA3); 990 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA4); 991 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA5); 992 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA6); 993 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA7); 994 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA8); 995 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA9); 996 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA10); 997 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA11); 998 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA12); 999 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA13); 1000 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA14); 1001 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA15); 1002 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC1); 1003 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC2); 1004 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC3); 1005 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC4); 1006 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_VERSION); 1007 cursorColumnToContentValues(cursor, cv, ContactQuery.IS_PRIMARY); 1008 cursorColumnToContentValues(cursor, cv, ContactQuery.IS_SUPERPRIMARY); 1009 cursorColumnToContentValues(cursor, cv, ContactQuery.MIMETYPE); 1010 cursorColumnToContentValues(cursor, cv, ContactQuery.RES_PACKAGE); 1011 cursorColumnToContentValues(cursor, cv, ContactQuery.GROUP_SOURCE_ID); 1012 cursorColumnToContentValues(cursor, cv, ContactQuery.CHAT_CAPABILITY); 1013 1014 return cv; 1015 } 1016 cursorColumnToContentValues( Cursor cursor, ContentValues values, int index)1017 private void cursorColumnToContentValues( 1018 Cursor cursor, ContentValues values, int index) { 1019 switch (cursor.getType(index)) { 1020 case Cursor.FIELD_TYPE_NULL: 1021 // don't put anything in the content values 1022 break; 1023 case Cursor.FIELD_TYPE_INTEGER: 1024 values.put(ContactQuery.COLUMNS[index], cursor.getLong(index)); 1025 break; 1026 case Cursor.FIELD_TYPE_STRING: 1027 values.put(ContactQuery.COLUMNS[index], cursor.getString(index)); 1028 break; 1029 case Cursor.FIELD_TYPE_BLOB: 1030 values.put(ContactQuery.COLUMNS[index], cursor.getBlob(index)); 1031 break; 1032 default: 1033 throw new IllegalStateException("Invalid or unhandled data type"); 1034 } 1035 } 1036 loadDirectoryMetaData(Result result)1037 private void loadDirectoryMetaData(Result result) { 1038 long directoryId = result.getDirectoryId(); 1039 1040 Cursor cursor = getContext().getContentResolver().query( 1041 ContentUris.withAppendedId(Directory.CONTENT_URI, directoryId), 1042 DirectoryQuery.COLUMNS, null, null, null); 1043 if (cursor == null) { 1044 return; 1045 } 1046 try { 1047 if (cursor.moveToFirst()) { 1048 final String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME); 1049 final String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME); 1050 final int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID); 1051 final String accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE); 1052 final String accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME); 1053 final int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT); 1054 String directoryType = null; 1055 if (!TextUtils.isEmpty(packageName)) { 1056 PackageManager pm = getContext().getPackageManager(); 1057 try { 1058 Resources resources = pm.getResourcesForApplication(packageName); 1059 directoryType = resources.getString(typeResourceId); 1060 } catch (NameNotFoundException e) { 1061 Log.w(TAG, "Contact directory resource not found: " 1062 + packageName + "." + typeResourceId); 1063 } 1064 } 1065 1066 result.setDirectoryMetaData( 1067 displayName, directoryType, accountType, accountName, exportSupport); 1068 } 1069 } finally { 1070 cursor.close(); 1071 } 1072 } 1073 1074 /** 1075 * Loads groups meta-data for all groups associated with all constituent raw contacts' 1076 * accounts. 1077 */ loadGroupMetaData(Result result)1078 private void loadGroupMetaData(Result result) { 1079 StringBuilder selection = new StringBuilder(); 1080 ArrayList<String> selectionArgs = new ArrayList<String>(); 1081 for (Entity entity : result.mEntities) { 1082 ContentValues values = entity.getEntityValues(); 1083 String accountName = values.getAsString(RawContacts.ACCOUNT_NAME); 1084 String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE); 1085 String dataSet = values.getAsString(RawContacts.DATA_SET); 1086 if (accountName != null && accountType != null) { 1087 if (selection.length() != 0) { 1088 selection.append(" OR "); 1089 } 1090 selection.append( 1091 "(" + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?"); 1092 selectionArgs.add(accountName); 1093 selectionArgs.add(accountType); 1094 1095 if (dataSet != null) { 1096 selection.append(" AND " + Groups.DATA_SET + "=?"); 1097 selectionArgs.add(dataSet); 1098 } else { 1099 selection.append(" AND " + Groups.DATA_SET + " IS NULL"); 1100 } 1101 selection.append(")"); 1102 } 1103 } 1104 final ArrayList<GroupMetaData> groupList = new ArrayList<GroupMetaData>(); 1105 final Cursor cursor = getContext().getContentResolver().query(Groups.CONTENT_URI, 1106 GroupQuery.COLUMNS, selection.toString(), selectionArgs.toArray(new String[0]), 1107 null); 1108 try { 1109 while (cursor.moveToNext()) { 1110 final String accountName = cursor.getString(GroupQuery.ACCOUNT_NAME); 1111 final String accountType = cursor.getString(GroupQuery.ACCOUNT_TYPE); 1112 final String dataSet = cursor.getString(GroupQuery.DATA_SET); 1113 final long groupId = cursor.getLong(GroupQuery.ID); 1114 final String title = cursor.getString(GroupQuery.TITLE); 1115 final boolean defaultGroup = cursor.isNull(GroupQuery.AUTO_ADD) 1116 ? false 1117 : cursor.getInt(GroupQuery.AUTO_ADD) != 0; 1118 final boolean favorites = cursor.isNull(GroupQuery.FAVORITES) 1119 ? false 1120 : cursor.getInt(GroupQuery.FAVORITES) != 0; 1121 1122 groupList.add(new GroupMetaData( 1123 accountName, accountType, dataSet, groupId, title, defaultGroup, 1124 favorites)); 1125 } 1126 } finally { 1127 cursor.close(); 1128 } 1129 result.mGroups = groupList; 1130 } 1131 1132 /** 1133 * Loads all stream items and stream item photos belonging to this contact. 1134 */ loadStreamItems(Result result)1135 private void loadStreamItems(Result result) { 1136 Cursor cursor = getContext().getContentResolver().query( 1137 Contacts.CONTENT_LOOKUP_URI.buildUpon() 1138 .appendPath(result.getLookupKey()) 1139 .appendPath(Contacts.StreamItems.CONTENT_DIRECTORY).build(), 1140 null, null, null, null); 1141 LongSparseArray<StreamItemEntry> streamItemsById = 1142 new LongSparseArray<StreamItemEntry>(); 1143 ArrayList<StreamItemEntry> streamItems = new ArrayList<StreamItemEntry>(); 1144 try { 1145 while (cursor.moveToNext()) { 1146 StreamItemEntry streamItem = new StreamItemEntry(cursor); 1147 streamItemsById.put(streamItem.getId(), streamItem); 1148 streamItems.add(streamItem); 1149 } 1150 } finally { 1151 cursor.close(); 1152 } 1153 1154 // Pre-decode all HTMLs 1155 final long start = System.currentTimeMillis(); 1156 for (StreamItemEntry streamItem : streamItems) { 1157 streamItem.decodeHtml(getContext()); 1158 } 1159 final long end = System.currentTimeMillis(); 1160 if (DEBUG) { 1161 Log.d(TAG, "Decoded HTML for " + streamItems.size() + " items, took " 1162 + (end - start) + " ms"); 1163 } 1164 1165 // Now retrieve any photo records associated with the stream items. 1166 if (!streamItems.isEmpty()) { 1167 if (result.isUserProfile()) { 1168 // If the stream items we're loading are for the profile, we can't bulk-load the 1169 // stream items with a custom selection. 1170 for (StreamItemEntry entry : streamItems) { 1171 Cursor siCursor = getContext().getContentResolver().query( 1172 Uri.withAppendedPath( 1173 ContentUris.withAppendedId( 1174 StreamItems.CONTENT_URI, entry.getId()), 1175 StreamItems.StreamItemPhotos.CONTENT_DIRECTORY), 1176 null, null, null, null); 1177 try { 1178 while (siCursor.moveToNext()) { 1179 entry.addPhoto(new StreamItemPhotoEntry(siCursor)); 1180 } 1181 } finally { 1182 siCursor.close(); 1183 } 1184 } 1185 } else { 1186 String[] streamItemIdArr = new String[streamItems.size()]; 1187 StringBuilder streamItemPhotoSelection = new StringBuilder(); 1188 streamItemPhotoSelection.append(StreamItemPhotos.STREAM_ITEM_ID + " IN ("); 1189 for (int i = 0; i < streamItems.size(); i++) { 1190 if (i > 0) { 1191 streamItemPhotoSelection.append(","); 1192 } 1193 streamItemPhotoSelection.append("?"); 1194 streamItemIdArr[i] = String.valueOf(streamItems.get(i).getId()); 1195 } 1196 streamItemPhotoSelection.append(")"); 1197 Cursor sipCursor = getContext().getContentResolver().query( 1198 StreamItems.CONTENT_PHOTO_URI, 1199 null, streamItemPhotoSelection.toString(), streamItemIdArr, 1200 StreamItemPhotos.STREAM_ITEM_ID); 1201 try { 1202 while (sipCursor.moveToNext()) { 1203 long streamItemId = sipCursor.getLong( 1204 sipCursor.getColumnIndex(StreamItemPhotos.STREAM_ITEM_ID)); 1205 StreamItemEntry streamItem = streamItemsById.get(streamItemId); 1206 streamItem.addPhoto(new StreamItemPhotoEntry(sipCursor)); 1207 } 1208 } finally { 1209 sipCursor.close(); 1210 } 1211 } 1212 } 1213 1214 // Set the sorted stream items on the result. 1215 Collections.sort(streamItems); 1216 result.mStreamItems = streamItems; 1217 } 1218 1219 @Override deliverResult(Result result)1220 public void deliverResult(Result result) { 1221 unregisterObserver(); 1222 1223 // The creator isn't interested in any further updates 1224 if (isReset() || result == null) { 1225 return; 1226 } 1227 1228 mContact = result; 1229 1230 if (result.isLoaded()) { 1231 mLookupUri = result.getLookupUri(); 1232 1233 if (!result.isDirectoryEntry()) { 1234 Log.i(TAG, "Registering content observer for " + mLookupUri); 1235 if (mObserver == null) { 1236 mObserver = new ForceLoadContentObserver(); 1237 } 1238 getContext().getContentResolver().registerContentObserver( 1239 mLookupUri, true, mObserver); 1240 } 1241 1242 if (mPostViewNotification) { 1243 // inform the source of the data that this contact is being looked at 1244 postViewNotificationToSyncAdapter(); 1245 } 1246 } 1247 1248 super.deliverResult(mContact); 1249 } 1250 1251 /** 1252 * Posts a message to the contributing sync adapters that have opted-in, notifying them 1253 * that the contact has just been loaded 1254 */ postViewNotificationToSyncAdapter()1255 private void postViewNotificationToSyncAdapter() { 1256 Context context = getContext(); 1257 for (Entity entity : mContact.getEntities()) { 1258 final ContentValues entityValues = entity.getEntityValues(); 1259 final long rawContactId = entityValues.getAsLong(RawContacts.Entity._ID); 1260 if (mNotifiedRawContactIds.contains(rawContactId)) { 1261 continue; // Already notified for this raw contact. 1262 } 1263 mNotifiedRawContactIds.add(rawContactId); 1264 final String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE); 1265 final String dataSet = entityValues.getAsString(RawContacts.DATA_SET); 1266 final AccountType accountType = AccountTypeManager.getInstance(context).getAccountType( 1267 type, dataSet); 1268 final String serviceName = accountType.getViewContactNotifyServiceClassName(); 1269 final String servicePackageName = accountType.getViewContactNotifyServicePackageName(); 1270 if (!TextUtils.isEmpty(serviceName) && !TextUtils.isEmpty(servicePackageName)) { 1271 final Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId); 1272 final Intent intent = new Intent(); 1273 intent.setClassName(servicePackageName, serviceName); 1274 intent.setAction(Intent.ACTION_VIEW); 1275 intent.setDataAndType(uri, RawContacts.CONTENT_ITEM_TYPE); 1276 try { 1277 context.startService(intent); 1278 } catch (Exception e) { 1279 Log.e(TAG, "Error sending message to source-app", e); 1280 } 1281 } 1282 } 1283 } 1284 unregisterObserver()1285 private void unregisterObserver() { 1286 if (mObserver != null) { 1287 getContext().getContentResolver().unregisterContentObserver(mObserver); 1288 mObserver = null; 1289 } 1290 } 1291 1292 /** 1293 * Sets whether to load stream items. Will trigger a reload if the value has changed. 1294 * At the moment, this is only used for debugging purposes 1295 */ setLoadStreamItems(boolean value)1296 public void setLoadStreamItems(boolean value) { 1297 if (mLoadStreamItems != value) { 1298 mLoadStreamItems = value; 1299 onContentChanged(); 1300 } 1301 } 1302 1303 /** 1304 * Fully upgrades this ContactLoader to one with all lists fully loaded. When done, the 1305 * new result will be delivered 1306 */ upgradeToFullContact()1307 public void upgradeToFullContact() { 1308 // Everything requested already? Nothing to do, so let's bail out 1309 if (mLoadGroupMetaData && mLoadInvitableAccountTypes && mLoadStreamItems 1310 && mPostViewNotification) return; 1311 1312 mLoadGroupMetaData = true; 1313 mLoadInvitableAccountTypes = true; 1314 mLoadStreamItems = true; 1315 mPostViewNotification = true; 1316 1317 // Cache the current result, so that we only load the "missing" parts of the contact. 1318 cacheResult(); 1319 1320 // Our load parameters have changed, so let's pretend the data has changed. Its the same 1321 // thing, essentially. 1322 onContentChanged(); 1323 } 1324 getLoadStreamItems()1325 public boolean getLoadStreamItems() { 1326 return mLoadStreamItems; 1327 } 1328 getLookupUri()1329 public Uri getLookupUri() { 1330 return mLookupUri; 1331 } 1332 1333 @Override onStartLoading()1334 protected void onStartLoading() { 1335 if (mContact != null) { 1336 deliverResult(mContact); 1337 } 1338 1339 if (takeContentChanged() || mContact == null) { 1340 forceLoad(); 1341 } 1342 } 1343 1344 @Override onStopLoading()1345 protected void onStopLoading() { 1346 cancelLoad(); 1347 } 1348 1349 @Override onReset()1350 protected void onReset() { 1351 super.onReset(); 1352 cancelLoad(); 1353 unregisterObserver(); 1354 mContact = null; 1355 } 1356 1357 /** 1358 * Caches the result, which is useful when we switch from activity to activity, using the same 1359 * contact. If the next load is for a different contact, the cached result will be dropped 1360 */ cacheResult()1361 public void cacheResult() { 1362 if (mContact == null || !mContact.isLoaded()) { 1363 sCachedResult = null; 1364 } else { 1365 sCachedResult = mContact; 1366 } 1367 } 1368 } 1369