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.model; 18 19 import android.content.AsyncTaskLoader; 20 import android.content.ContentResolver; 21 import android.content.ContentUris; 22 import android.content.ContentValues; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.pm.PackageManager; 26 import android.content.pm.PackageManager.NameNotFoundException; 27 import android.content.res.AssetFileDescriptor; 28 import android.content.res.Resources; 29 import android.database.Cursor; 30 import android.net.Uri; 31 import android.provider.ContactsContract; 32 import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 33 import android.provider.ContactsContract.Contacts; 34 import android.provider.ContactsContract.Data; 35 import android.provider.ContactsContract.Directory; 36 import android.provider.ContactsContract.Groups; 37 import android.provider.ContactsContract.RawContacts; 38 import android.provider.ContactsContract.StreamItemPhotos; 39 import android.provider.ContactsContract.StreamItems; 40 import android.text.TextUtils; 41 import android.util.Log; 42 import android.util.LongSparseArray; 43 44 import com.android.contacts.ContactsUtils; 45 import com.android.contacts.GroupMetaData; 46 import com.android.contacts.model.account.AccountType; 47 import com.android.contacts.model.account.AccountTypeWithDataSet; 48 import com.android.contacts.model.dataitem.DataItem; 49 import com.android.contacts.model.dataitem.PhoneDataItem; 50 import com.android.contacts.model.dataitem.PhotoDataItem; 51 import com.android.contacts.util.ContactLoaderUtils; 52 import com.android.contacts.util.DataStatus; 53 import com.android.contacts.util.StreamItemEntry; 54 import com.android.contacts.util.StreamItemPhotoEntry; 55 import com.android.contacts.util.UriUtils; 56 import com.google.common.collect.ImmutableList; 57 import com.google.common.collect.ImmutableMap; 58 import com.google.common.collect.Maps; 59 import com.google.common.collect.Sets; 60 61 import java.io.ByteArrayOutputStream; 62 import java.io.FileInputStream; 63 import java.io.IOException; 64 import java.util.ArrayList; 65 import java.util.Collections; 66 import java.util.List; 67 import java.util.Map; 68 import java.util.Set; 69 70 /** 71 * Loads a single Contact and all it constituent RawContacts. 72 */ 73 public class ContactLoader extends AsyncTaskLoader<Contact> { 74 private static final String TAG = ContactLoader.class.getSimpleName(); 75 76 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 77 78 /** A short-lived cache that can be set by {@link #cacheResult()} */ 79 private static Contact sCachedResult = null; 80 81 private final Uri mRequestedUri; 82 private Uri mLookupUri; 83 private boolean mLoadGroupMetaData; 84 private boolean mLoadStreamItems; 85 private boolean mLoadInvitableAccountTypes; 86 private boolean mPostViewNotification; 87 private boolean mComputeFormattedPhoneNumber; 88 private Contact mContact; 89 private ForceLoadContentObserver mObserver; 90 private final Set<Long> mNotifiedRawContactIds = Sets.newHashSet(); 91 92 public ContactLoader(Context context, Uri lookupUri, boolean postViewNotification) { 93 this(context, lookupUri, false, false, false, postViewNotification, false); 94 } 95 96 public ContactLoader(Context context, Uri lookupUri, boolean loadGroupMetaData, 97 boolean loadStreamItems, boolean loadInvitableAccountTypes, 98 boolean postViewNotification, boolean computeFormattedPhoneNumber) { 99 super(context); 100 mLookupUri = lookupUri; 101 mRequestedUri = lookupUri; 102 mLoadGroupMetaData = loadGroupMetaData; 103 mLoadStreamItems = loadStreamItems; 104 mLoadInvitableAccountTypes = loadInvitableAccountTypes; 105 mPostViewNotification = postViewNotification; 106 mComputeFormattedPhoneNumber = computeFormattedPhoneNumber; 107 } 108 109 /** 110 * Projection used for the query that loads all data for the entire contact (except for 111 * social stream items). 112 */ 113 private static class ContactQuery { 114 static final String[] COLUMNS = new String[] { 115 Contacts.NAME_RAW_CONTACT_ID, 116 Contacts.DISPLAY_NAME_SOURCE, 117 Contacts.LOOKUP_KEY, 118 Contacts.DISPLAY_NAME, 119 Contacts.DISPLAY_NAME_ALTERNATIVE, 120 Contacts.PHONETIC_NAME, 121 Contacts.PHOTO_ID, 122 Contacts.STARRED, 123 Contacts.CONTACT_PRESENCE, 124 Contacts.CONTACT_STATUS, 125 Contacts.CONTACT_STATUS_TIMESTAMP, 126 Contacts.CONTACT_STATUS_RES_PACKAGE, 127 Contacts.CONTACT_STATUS_LABEL, 128 Contacts.Entity.CONTACT_ID, 129 Contacts.Entity.RAW_CONTACT_ID, 130 131 RawContacts.ACCOUNT_NAME, 132 RawContacts.ACCOUNT_TYPE, 133 RawContacts.DATA_SET, 134 RawContacts.ACCOUNT_TYPE_AND_DATA_SET, 135 RawContacts.DIRTY, 136 RawContacts.VERSION, 137 RawContacts.SOURCE_ID, 138 RawContacts.SYNC1, 139 RawContacts.SYNC2, 140 RawContacts.SYNC3, 141 RawContacts.SYNC4, 142 RawContacts.DELETED, 143 RawContacts.NAME_VERIFIED, 144 145 Contacts.Entity.DATA_ID, 146 Data.DATA1, 147 Data.DATA2, 148 Data.DATA3, 149 Data.DATA4, 150 Data.DATA5, 151 Data.DATA6, 152 Data.DATA7, 153 Data.DATA8, 154 Data.DATA9, 155 Data.DATA10, 156 Data.DATA11, 157 Data.DATA12, 158 Data.DATA13, 159 Data.DATA14, 160 Data.DATA15, 161 Data.SYNC1, 162 Data.SYNC2, 163 Data.SYNC3, 164 Data.SYNC4, 165 Data.DATA_VERSION, 166 Data.IS_PRIMARY, 167 Data.IS_SUPER_PRIMARY, 168 Data.MIMETYPE, 169 Data.RES_PACKAGE, 170 171 GroupMembership.GROUP_SOURCE_ID, 172 173 Data.PRESENCE, 174 Data.CHAT_CAPABILITY, 175 Data.STATUS, 176 Data.STATUS_RES_PACKAGE, 177 Data.STATUS_ICON, 178 Data.STATUS_LABEL, 179 Data.STATUS_TIMESTAMP, 180 181 Contacts.PHOTO_URI, 182 Contacts.SEND_TO_VOICEMAIL, 183 Contacts.CUSTOM_RINGTONE, 184 Contacts.IS_USER_PROFILE, 185 }; 186 187 public static final int NAME_RAW_CONTACT_ID = 0; 188 public static final int DISPLAY_NAME_SOURCE = 1; 189 public static final int LOOKUP_KEY = 2; 190 public static final int DISPLAY_NAME = 3; 191 public static final int ALT_DISPLAY_NAME = 4; 192 public static final int PHONETIC_NAME = 5; 193 public static final int PHOTO_ID = 6; 194 public static final int STARRED = 7; 195 public static final int CONTACT_PRESENCE = 8; 196 public static final int CONTACT_STATUS = 9; 197 public static final int CONTACT_STATUS_TIMESTAMP = 10; 198 public static final int CONTACT_STATUS_RES_PACKAGE = 11; 199 public static final int CONTACT_STATUS_LABEL = 12; 200 public static final int CONTACT_ID = 13; 201 public static final int RAW_CONTACT_ID = 14; 202 203 public static final int ACCOUNT_NAME = 15; 204 public static final int ACCOUNT_TYPE = 16; 205 public static final int DATA_SET = 17; 206 public static final int ACCOUNT_TYPE_AND_DATA_SET = 18; 207 public static final int DIRTY = 19; 208 public static final int VERSION = 20; 209 public static final int SOURCE_ID = 21; 210 public static final int SYNC1 = 22; 211 public static final int SYNC2 = 23; 212 public static final int SYNC3 = 24; 213 public static final int SYNC4 = 25; 214 public static final int DELETED = 26; 215 public static final int NAME_VERIFIED = 27; 216 217 public static final int DATA_ID = 28; 218 public static final int DATA1 = 29; 219 public static final int DATA2 = 30; 220 public static final int DATA3 = 31; 221 public static final int DATA4 = 32; 222 public static final int DATA5 = 33; 223 public static final int DATA6 = 34; 224 public static final int DATA7 = 35; 225 public static final int DATA8 = 36; 226 public static final int DATA9 = 37; 227 public static final int DATA10 = 38; 228 public static final int DATA11 = 39; 229 public static final int DATA12 = 40; 230 public static final int DATA13 = 41; 231 public static final int DATA14 = 42; 232 public static final int DATA15 = 43; 233 public static final int DATA_SYNC1 = 44; 234 public static final int DATA_SYNC2 = 45; 235 public static final int DATA_SYNC3 = 46; 236 public static final int DATA_SYNC4 = 47; 237 public static final int DATA_VERSION = 48; 238 public static final int IS_PRIMARY = 49; 239 public static final int IS_SUPERPRIMARY = 50; 240 public static final int MIMETYPE = 51; 241 public static final int RES_PACKAGE = 52; 242 243 public static final int GROUP_SOURCE_ID = 53; 244 245 public static final int PRESENCE = 54; 246 public static final int CHAT_CAPABILITY = 55; 247 public static final int STATUS = 56; 248 public static final int STATUS_RES_PACKAGE = 57; 249 public static final int STATUS_ICON = 58; 250 public static final int STATUS_LABEL = 59; 251 public static final int STATUS_TIMESTAMP = 60; 252 253 public static final int PHOTO_URI = 61; 254 public static final int SEND_TO_VOICEMAIL = 62; 255 public static final int CUSTOM_RINGTONE = 63; 256 public static final int IS_USER_PROFILE = 64; 257 } 258 259 /** 260 * Projection used for the query that loads all data for the entire contact. 261 */ 262 private static class DirectoryQuery { 263 static final String[] COLUMNS = new String[] { 264 Directory.DISPLAY_NAME, 265 Directory.PACKAGE_NAME, 266 Directory.TYPE_RESOURCE_ID, 267 Directory.ACCOUNT_TYPE, 268 Directory.ACCOUNT_NAME, 269 Directory.EXPORT_SUPPORT, 270 }; 271 272 public static final int DISPLAY_NAME = 0; 273 public static final int PACKAGE_NAME = 1; 274 public static final int TYPE_RESOURCE_ID = 2; 275 public static final int ACCOUNT_TYPE = 3; 276 public static final int ACCOUNT_NAME = 4; 277 public static final int EXPORT_SUPPORT = 5; 278 } 279 280 private static class GroupQuery { 281 static final String[] COLUMNS = new String[] { 282 Groups.ACCOUNT_NAME, 283 Groups.ACCOUNT_TYPE, 284 Groups.DATA_SET, 285 Groups.ACCOUNT_TYPE_AND_DATA_SET, 286 Groups._ID, 287 Groups.TITLE, 288 Groups.AUTO_ADD, 289 Groups.FAVORITES, 290 }; 291 292 public static final int ACCOUNT_NAME = 0; 293 public static final int ACCOUNT_TYPE = 1; 294 public static final int DATA_SET = 2; 295 public static final int ACCOUNT_TYPE_AND_DATA_SET = 3; 296 public static final int ID = 4; 297 public static final int TITLE = 5; 298 public static final int AUTO_ADD = 6; 299 public static final int FAVORITES = 7; 300 } 301 302 @Override 303 public Contact loadInBackground() { 304 try { 305 final ContentResolver resolver = getContext().getContentResolver(); 306 final Uri uriCurrentFormat = ContactLoaderUtils.ensureIsContactUri( 307 resolver, mLookupUri); 308 final Contact cachedResult = sCachedResult; 309 sCachedResult = null; 310 // Is this the same Uri as what we had before already? In that case, reuse that result 311 final Contact result; 312 final boolean resultIsCached; 313 if (cachedResult != null && 314 UriUtils.areEqual(cachedResult.getLookupUri(), mLookupUri)) { 315 // We are using a cached result from earlier. Below, we should make sure 316 // we are not doing any more network or disc accesses 317 result = new Contact(mRequestedUri, cachedResult); 318 resultIsCached = true; 319 } else { 320 result = loadContactEntity(resolver, uriCurrentFormat); 321 resultIsCached = false; 322 } 323 if (result.isLoaded()) { 324 if (result.isDirectoryEntry()) { 325 if (!resultIsCached) { 326 loadDirectoryMetaData(result); 327 } 328 } else if (mLoadGroupMetaData) { 329 if (result.getGroupMetaData() == null) { 330 loadGroupMetaData(result); 331 } 332 } 333 if (mLoadStreamItems && result.getStreamItems() == null) { 334 loadStreamItems(result); 335 } 336 if (mComputeFormattedPhoneNumber) { 337 computeFormattedPhoneNumbers(result); 338 } 339 if (!resultIsCached) loadPhotoBinaryData(result); 340 341 // Note ME profile should never have "Add connection" 342 if (mLoadInvitableAccountTypes && result.getInvitableAccountTypes() == null) { 343 loadInvitableAccountTypes(result); 344 } 345 } 346 return result; 347 } catch (Exception e) { 348 Log.e(TAG, "Error loading the contact: " + mLookupUri, e); 349 return Contact.forError(mRequestedUri, e); 350 } 351 } 352 353 private Contact loadContactEntity(ContentResolver resolver, Uri contactUri) { 354 Uri entityUri = Uri.withAppendedPath(contactUri, Contacts.Entity.CONTENT_DIRECTORY); 355 Cursor cursor = resolver.query(entityUri, ContactQuery.COLUMNS, null, null, 356 Contacts.Entity.RAW_CONTACT_ID); 357 if (cursor == null) { 358 Log.e(TAG, "No cursor returned in loadContactEntity"); 359 return Contact.forNotFound(mRequestedUri); 360 } 361 362 try { 363 if (!cursor.moveToFirst()) { 364 cursor.close(); 365 return Contact.forNotFound(mRequestedUri); 366 } 367 368 // Create the loaded contact starting with the header data. 369 Contact contact = loadContactHeaderData(cursor, contactUri); 370 371 // Fill in the raw contacts, which is wrapped in an Entity and any 372 // status data. Initially, result has empty entities and statuses. 373 long currentRawContactId = -1; 374 RawContact rawContact = null; 375 ImmutableList.Builder<RawContact> rawContactsBuilder = 376 new ImmutableList.Builder<RawContact>(); 377 ImmutableMap.Builder<Long, DataStatus> statusesBuilder = 378 new ImmutableMap.Builder<Long, DataStatus>(); 379 do { 380 long rawContactId = cursor.getLong(ContactQuery.RAW_CONTACT_ID); 381 if (rawContactId != currentRawContactId) { 382 // First time to see this raw contact id, so create a new entity, and 383 // add it to the result's entities. 384 currentRawContactId = rawContactId; 385 rawContact = new RawContact(getContext(), loadRawContactValues(cursor)); 386 rawContactsBuilder.add(rawContact); 387 } 388 if (!cursor.isNull(ContactQuery.DATA_ID)) { 389 ContentValues data = loadDataValues(cursor); 390 final DataItem item = rawContact.addDataItemValues(data); 391 392 if (!cursor.isNull(ContactQuery.PRESENCE) 393 || !cursor.isNull(ContactQuery.STATUS)) { 394 final DataStatus status = new DataStatus(cursor); 395 final long dataId = cursor.getLong(ContactQuery.DATA_ID); 396 statusesBuilder.put(dataId, status); 397 } 398 } 399 } while (cursor.moveToNext()); 400 401 contact.setRawContacts(rawContactsBuilder.build()); 402 contact.setStatuses(statusesBuilder.build()); 403 404 return contact; 405 } finally { 406 cursor.close(); 407 } 408 } 409 410 /** 411 * Looks for the photo data item in entities. If found, creates a new Bitmap instance. If 412 * not found, returns null 413 */ 414 private void loadPhotoBinaryData(Contact contactData) { 415 416 // If we have a photo URI, try loading that first. 417 String photoUri = contactData.getPhotoUri(); 418 if (photoUri != null) { 419 try { 420 AssetFileDescriptor fd = getContext().getContentResolver() 421 .openAssetFileDescriptor(Uri.parse(photoUri), "r"); 422 byte[] buffer = new byte[16 * 1024]; 423 FileInputStream fis = fd.createInputStream(); 424 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 425 try { 426 int size; 427 while ((size = fis.read(buffer)) != -1) { 428 baos.write(buffer, 0, size); 429 } 430 contactData.setPhotoBinaryData(baos.toByteArray()); 431 } finally { 432 fis.close(); 433 fd.close(); 434 } 435 return; 436 } catch (IOException ioe) { 437 // Just fall back to the case below. 438 } 439 } 440 441 // If we couldn't load from a file, fall back to the data blob. 442 final long photoId = contactData.getPhotoId(); 443 if (photoId <= 0) { 444 // No photo ID 445 return; 446 } 447 448 for (RawContact rawContact : contactData.getRawContacts()) { 449 for (DataItem dataItem : rawContact.getDataItems()) { 450 if (dataItem.getId() == photoId) { 451 if (!(dataItem instanceof PhotoDataItem)) { 452 break; 453 } 454 455 final PhotoDataItem photo = (PhotoDataItem) dataItem; 456 contactData.setPhotoBinaryData(photo.getPhoto()); 457 break; 458 } 459 } 460 } 461 } 462 463 /** 464 * Sets the "invitable" account types to {@link Contact#mInvitableAccountTypes}. 465 */ 466 private void loadInvitableAccountTypes(Contact contactData) { 467 final ImmutableList.Builder<AccountType> resultListBuilder = 468 new ImmutableList.Builder<AccountType>(); 469 if (!contactData.isUserProfile()) { 470 Map<AccountTypeWithDataSet, AccountType> invitables = 471 AccountTypeManager.getInstance(getContext()).getUsableInvitableAccountTypes(); 472 if (!invitables.isEmpty()) { 473 final Map<AccountTypeWithDataSet, AccountType> resultMap = 474 Maps.newHashMap(invitables); 475 476 // Remove the ones that already have a raw contact in the current contact 477 for (RawContact rawContact : contactData.getRawContacts()) { 478 final AccountTypeWithDataSet type = AccountTypeWithDataSet.get( 479 rawContact.getAccountTypeString(), 480 rawContact.getDataSet()); 481 resultMap.remove(type); 482 } 483 484 resultListBuilder.addAll(resultMap.values()); 485 } 486 } 487 488 // Set to mInvitableAccountTypes 489 contactData.setInvitableAccountTypes(resultListBuilder.build()); 490 } 491 492 /** 493 * Extracts Contact level columns from the cursor. 494 */ 495 private Contact loadContactHeaderData(final Cursor cursor, Uri contactUri) { 496 final String directoryParameter = 497 contactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY); 498 final long directoryId = directoryParameter == null 499 ? Directory.DEFAULT 500 : Long.parseLong(directoryParameter); 501 final long contactId = cursor.getLong(ContactQuery.CONTACT_ID); 502 final String lookupKey = cursor.getString(ContactQuery.LOOKUP_KEY); 503 final long nameRawContactId = cursor.getLong(ContactQuery.NAME_RAW_CONTACT_ID); 504 final int displayNameSource = cursor.getInt(ContactQuery.DISPLAY_NAME_SOURCE); 505 final String displayName = cursor.getString(ContactQuery.DISPLAY_NAME); 506 final String altDisplayName = cursor.getString(ContactQuery.ALT_DISPLAY_NAME); 507 final String phoneticName = cursor.getString(ContactQuery.PHONETIC_NAME); 508 final long photoId = cursor.getLong(ContactQuery.PHOTO_ID); 509 final String photoUri = cursor.getString(ContactQuery.PHOTO_URI); 510 final boolean starred = cursor.getInt(ContactQuery.STARRED) != 0; 511 final Integer presence = cursor.isNull(ContactQuery.CONTACT_PRESENCE) 512 ? null 513 : cursor.getInt(ContactQuery.CONTACT_PRESENCE); 514 final boolean sendToVoicemail = cursor.getInt(ContactQuery.SEND_TO_VOICEMAIL) == 1; 515 final String customRingtone = cursor.getString(ContactQuery.CUSTOM_RINGTONE); 516 final boolean isUserProfile = cursor.getInt(ContactQuery.IS_USER_PROFILE) == 1; 517 518 Uri lookupUri; 519 if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) { 520 lookupUri = ContentUris.withAppendedId( 521 Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), contactId); 522 } else { 523 lookupUri = contactUri; 524 } 525 526 return new Contact(mRequestedUri, contactUri, lookupUri, directoryId, lookupKey, 527 contactId, nameRawContactId, displayNameSource, photoId, photoUri, displayName, 528 altDisplayName, phoneticName, starred, presence, sendToVoicemail, 529 customRingtone, isUserProfile); 530 } 531 532 /** 533 * Extracts RawContact level columns from the cursor. 534 */ 535 private ContentValues loadRawContactValues(Cursor cursor) { 536 ContentValues cv = new ContentValues(); 537 538 cv.put(RawContacts._ID, cursor.getLong(ContactQuery.RAW_CONTACT_ID)); 539 540 cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_NAME); 541 cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE); 542 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SET); 543 cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE_AND_DATA_SET); 544 cursorColumnToContentValues(cursor, cv, ContactQuery.DIRTY); 545 cursorColumnToContentValues(cursor, cv, ContactQuery.VERSION); 546 cursorColumnToContentValues(cursor, cv, ContactQuery.SOURCE_ID); 547 cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC1); 548 cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC2); 549 cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC3); 550 cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC4); 551 cursorColumnToContentValues(cursor, cv, ContactQuery.DELETED); 552 cursorColumnToContentValues(cursor, cv, ContactQuery.CONTACT_ID); 553 cursorColumnToContentValues(cursor, cv, ContactQuery.STARRED); 554 cursorColumnToContentValues(cursor, cv, ContactQuery.NAME_VERIFIED); 555 556 return cv; 557 } 558 559 /** 560 * Extracts Data level columns from the cursor. 561 */ 562 private ContentValues loadDataValues(Cursor cursor) { 563 ContentValues cv = new ContentValues(); 564 565 cv.put(Data._ID, cursor.getLong(ContactQuery.DATA_ID)); 566 567 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA1); 568 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA2); 569 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA3); 570 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA4); 571 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA5); 572 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA6); 573 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA7); 574 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA8); 575 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA9); 576 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA10); 577 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA11); 578 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA12); 579 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA13); 580 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA14); 581 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA15); 582 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC1); 583 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC2); 584 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC3); 585 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC4); 586 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_VERSION); 587 cursorColumnToContentValues(cursor, cv, ContactQuery.IS_PRIMARY); 588 cursorColumnToContentValues(cursor, cv, ContactQuery.IS_SUPERPRIMARY); 589 cursorColumnToContentValues(cursor, cv, ContactQuery.MIMETYPE); 590 cursorColumnToContentValues(cursor, cv, ContactQuery.RES_PACKAGE); 591 cursorColumnToContentValues(cursor, cv, ContactQuery.GROUP_SOURCE_ID); 592 cursorColumnToContentValues(cursor, cv, ContactQuery.CHAT_CAPABILITY); 593 594 return cv; 595 } 596 597 private void cursorColumnToContentValues( 598 Cursor cursor, ContentValues values, int index) { 599 switch (cursor.getType(index)) { 600 case Cursor.FIELD_TYPE_NULL: 601 // don't put anything in the content values 602 break; 603 case Cursor.FIELD_TYPE_INTEGER: 604 values.put(ContactQuery.COLUMNS[index], cursor.getLong(index)); 605 break; 606 case Cursor.FIELD_TYPE_STRING: 607 values.put(ContactQuery.COLUMNS[index], cursor.getString(index)); 608 break; 609 case Cursor.FIELD_TYPE_BLOB: 610 values.put(ContactQuery.COLUMNS[index], cursor.getBlob(index)); 611 break; 612 default: 613 throw new IllegalStateException("Invalid or unhandled data type"); 614 } 615 } 616 617 private void loadDirectoryMetaData(Contact result) { 618 long directoryId = result.getDirectoryId(); 619 620 Cursor cursor = getContext().getContentResolver().query( 621 ContentUris.withAppendedId(Directory.CONTENT_URI, directoryId), 622 DirectoryQuery.COLUMNS, null, null, null); 623 if (cursor == null) { 624 return; 625 } 626 try { 627 if (cursor.moveToFirst()) { 628 final String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME); 629 final String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME); 630 final int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID); 631 final String accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE); 632 final String accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME); 633 final int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT); 634 String directoryType = null; 635 if (!TextUtils.isEmpty(packageName)) { 636 PackageManager pm = getContext().getPackageManager(); 637 try { 638 Resources resources = pm.getResourcesForApplication(packageName); 639 directoryType = resources.getString(typeResourceId); 640 } catch (NameNotFoundException e) { 641 Log.w(TAG, "Contact directory resource not found: " 642 + packageName + "." + typeResourceId); 643 } 644 } 645 646 result.setDirectoryMetaData( 647 displayName, directoryType, accountType, accountName, exportSupport); 648 } 649 } finally { 650 cursor.close(); 651 } 652 } 653 654 /** 655 * Loads groups meta-data for all groups associated with all constituent raw contacts' 656 * accounts. 657 */ 658 private void loadGroupMetaData(Contact result) { 659 StringBuilder selection = new StringBuilder(); 660 ArrayList<String> selectionArgs = new ArrayList<String>(); 661 for (RawContact rawContact : result.getRawContacts()) { 662 final String accountName = rawContact.getAccountName(); 663 final String accountType = rawContact.getAccountTypeString(); 664 final String dataSet = rawContact.getDataSet(); 665 if (accountName != null && accountType != null) { 666 if (selection.length() != 0) { 667 selection.append(" OR "); 668 } 669 selection.append( 670 "(" + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?"); 671 selectionArgs.add(accountName); 672 selectionArgs.add(accountType); 673 674 if (dataSet != null) { 675 selection.append(" AND " + Groups.DATA_SET + "=?"); 676 selectionArgs.add(dataSet); 677 } else { 678 selection.append(" AND " + Groups.DATA_SET + " IS NULL"); 679 } 680 selection.append(")"); 681 } 682 } 683 final ImmutableList.Builder<GroupMetaData> groupListBuilder = 684 new ImmutableList.Builder<GroupMetaData>(); 685 final Cursor cursor = getContext().getContentResolver().query(Groups.CONTENT_URI, 686 GroupQuery.COLUMNS, selection.toString(), selectionArgs.toArray(new String[0]), 687 null); 688 try { 689 while (cursor.moveToNext()) { 690 final String accountName = cursor.getString(GroupQuery.ACCOUNT_NAME); 691 final String accountType = cursor.getString(GroupQuery.ACCOUNT_TYPE); 692 final String dataSet = cursor.getString(GroupQuery.DATA_SET); 693 final long groupId = cursor.getLong(GroupQuery.ID); 694 final String title = cursor.getString(GroupQuery.TITLE); 695 final boolean defaultGroup = cursor.isNull(GroupQuery.AUTO_ADD) 696 ? false 697 : cursor.getInt(GroupQuery.AUTO_ADD) != 0; 698 final boolean favorites = cursor.isNull(GroupQuery.FAVORITES) 699 ? false 700 : cursor.getInt(GroupQuery.FAVORITES) != 0; 701 702 groupListBuilder.add(new GroupMetaData( 703 accountName, accountType, dataSet, groupId, title, defaultGroup, 704 favorites)); 705 } 706 } finally { 707 cursor.close(); 708 } 709 result.setGroupMetaData(groupListBuilder.build()); 710 } 711 712 /** 713 * Loads all stream items and stream item photos belonging to this contact. 714 */ 715 private void loadStreamItems(Contact result) { 716 final Cursor cursor = getContext().getContentResolver().query( 717 Contacts.CONTENT_LOOKUP_URI.buildUpon() 718 .appendPath(result.getLookupKey()) 719 .appendPath(Contacts.StreamItems.CONTENT_DIRECTORY).build(), 720 null, null, null, null); 721 final LongSparseArray<StreamItemEntry> streamItemsById = 722 new LongSparseArray<StreamItemEntry>(); 723 final ArrayList<StreamItemEntry> streamItems = new ArrayList<StreamItemEntry>(); 724 try { 725 while (cursor.moveToNext()) { 726 StreamItemEntry streamItem = new StreamItemEntry(cursor); 727 streamItemsById.put(streamItem.getId(), streamItem); 728 streamItems.add(streamItem); 729 } 730 } finally { 731 cursor.close(); 732 } 733 734 // Pre-decode all HTMLs 735 final long start = System.currentTimeMillis(); 736 for (StreamItemEntry streamItem : streamItems) { 737 streamItem.decodeHtml(getContext()); 738 } 739 final long end = System.currentTimeMillis(); 740 if (DEBUG) { 741 Log.d(TAG, "Decoded HTML for " + streamItems.size() + " items, took " 742 + (end - start) + " ms"); 743 } 744 745 // Now retrieve any photo records associated with the stream items. 746 if (!streamItems.isEmpty()) { 747 if (result.isUserProfile()) { 748 // If the stream items we're loading are for the profile, we can't bulk-load the 749 // stream items with a custom selection. 750 for (StreamItemEntry entry : streamItems) { 751 Cursor siCursor = getContext().getContentResolver().query( 752 Uri.withAppendedPath( 753 ContentUris.withAppendedId( 754 StreamItems.CONTENT_URI, entry.getId()), 755 StreamItems.StreamItemPhotos.CONTENT_DIRECTORY), 756 null, null, null, null); 757 try { 758 while (siCursor.moveToNext()) { 759 entry.addPhoto(new StreamItemPhotoEntry(siCursor)); 760 } 761 } finally { 762 siCursor.close(); 763 } 764 } 765 } else { 766 String[] streamItemIdArr = new String[streamItems.size()]; 767 StringBuilder streamItemPhotoSelection = new StringBuilder(); 768 streamItemPhotoSelection.append(StreamItemPhotos.STREAM_ITEM_ID + " IN ("); 769 for (int i = 0; i < streamItems.size(); i++) { 770 if (i > 0) { 771 streamItemPhotoSelection.append(","); 772 } 773 streamItemPhotoSelection.append("?"); 774 streamItemIdArr[i] = String.valueOf(streamItems.get(i).getId()); 775 } 776 streamItemPhotoSelection.append(")"); 777 Cursor sipCursor = getContext().getContentResolver().query( 778 StreamItems.CONTENT_PHOTO_URI, 779 null, streamItemPhotoSelection.toString(), streamItemIdArr, 780 StreamItemPhotos.STREAM_ITEM_ID); 781 try { 782 while (sipCursor.moveToNext()) { 783 long streamItemId = sipCursor.getLong( 784 sipCursor.getColumnIndex(StreamItemPhotos.STREAM_ITEM_ID)); 785 StreamItemEntry streamItem = streamItemsById.get(streamItemId); 786 streamItem.addPhoto(new StreamItemPhotoEntry(sipCursor)); 787 } 788 } finally { 789 sipCursor.close(); 790 } 791 } 792 } 793 794 // Set the sorted stream items on the result. 795 Collections.sort(streamItems); 796 result.setStreamItems(new ImmutableList.Builder<StreamItemEntry>() 797 .addAll(streamItems.iterator()) 798 .build()); 799 } 800 801 /** 802 * Iterates over all data items that represent phone numbers are tries to calculate a formatted 803 * number. This function can safely be called several times as no unformatted data is 804 * overwritten 805 */ 806 private void computeFormattedPhoneNumbers(Contact contactData) { 807 final String countryIso = ContactsUtils.getCurrentCountryIso(getContext()); 808 final ImmutableList<RawContact> rawContacts = contactData.getRawContacts(); 809 final int rawContactCount = rawContacts.size(); 810 for (int rawContactIndex = 0; rawContactIndex < rawContactCount; rawContactIndex++) { 811 final RawContact rawContact = rawContacts.get(rawContactIndex); 812 final List<DataItem> dataItems = rawContact.getDataItems(); 813 final int dataCount = dataItems.size(); 814 for (int dataIndex = 0; dataIndex < dataCount; dataIndex++) { 815 final DataItem dataItem = dataItems.get(dataIndex); 816 if (dataItem instanceof PhoneDataItem) { 817 final PhoneDataItem phoneDataItem = (PhoneDataItem) dataItem; 818 phoneDataItem.computeFormattedPhoneNumber(countryIso); 819 } 820 } 821 } 822 } 823 824 @Override 825 public void deliverResult(Contact result) { 826 unregisterObserver(); 827 828 // The creator isn't interested in any further updates 829 if (isReset() || result == null) { 830 return; 831 } 832 833 mContact = result; 834 835 if (result.isLoaded()) { 836 mLookupUri = result.getLookupUri(); 837 838 if (!result.isDirectoryEntry()) { 839 Log.i(TAG, "Registering content observer for " + mLookupUri); 840 if (mObserver == null) { 841 mObserver = new ForceLoadContentObserver(); 842 } 843 getContext().getContentResolver().registerContentObserver( 844 mLookupUri, true, mObserver); 845 } 846 847 if (mPostViewNotification) { 848 // inform the source of the data that this contact is being looked at 849 postViewNotificationToSyncAdapter(); 850 } 851 } 852 853 super.deliverResult(mContact); 854 } 855 856 /** 857 * Posts a message to the contributing sync adapters that have opted-in, notifying them 858 * that the contact has just been loaded 859 */ 860 private void postViewNotificationToSyncAdapter() { 861 Context context = getContext(); 862 for (RawContact rawContact : mContact.getRawContacts()) { 863 final long rawContactId = rawContact.getId(); 864 if (mNotifiedRawContactIds.contains(rawContactId)) { 865 continue; // Already notified for this raw contact. 866 } 867 mNotifiedRawContactIds.add(rawContactId); 868 final AccountType accountType = rawContact.getAccountType(); 869 final String serviceName = accountType.getViewContactNotifyServiceClassName(); 870 final String servicePackageName = accountType.getViewContactNotifyServicePackageName(); 871 if (!TextUtils.isEmpty(serviceName) && !TextUtils.isEmpty(servicePackageName)) { 872 final Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId); 873 final Intent intent = new Intent(); 874 intent.setClassName(servicePackageName, serviceName); 875 intent.setAction(Intent.ACTION_VIEW); 876 intent.setDataAndType(uri, RawContacts.CONTENT_ITEM_TYPE); 877 try { 878 context.startService(intent); 879 } catch (Exception e) { 880 Log.e(TAG, "Error sending message to source-app", e); 881 } 882 } 883 } 884 } 885 886 private void unregisterObserver() { 887 if (mObserver != null) { 888 getContext().getContentResolver().unregisterContentObserver(mObserver); 889 mObserver = null; 890 } 891 } 892 893 /** 894 * Sets whether to load stream items. Will trigger a reload if the value has changed. 895 * At the moment, this is only used for debugging purposes 896 */ 897 public void setLoadStreamItems(boolean value) { 898 if (mLoadStreamItems != value) { 899 mLoadStreamItems = value; 900 onContentChanged(); 901 } 902 } 903 904 /** 905 * Fully upgrades this ContactLoader to one with all lists fully loaded. When done, the 906 * new result will be delivered 907 */ 908 public void upgradeToFullContact() { 909 // Everything requested already? Nothing to do, so let's bail out 910 if (mLoadGroupMetaData && mLoadInvitableAccountTypes && mLoadStreamItems 911 && mPostViewNotification && mComputeFormattedPhoneNumber) return; 912 913 mLoadGroupMetaData = true; 914 mLoadInvitableAccountTypes = true; 915 mLoadStreamItems = true; 916 mPostViewNotification = true; 917 mComputeFormattedPhoneNumber = true; 918 919 // Cache the current result, so that we only load the "missing" parts of the contact. 920 cacheResult(); 921 922 // Our load parameters have changed, so let's pretend the data has changed. Its the same 923 // thing, essentially. 924 onContentChanged(); 925 } 926 927 public boolean getLoadStreamItems() { 928 return mLoadStreamItems; 929 } 930 931 public Uri getLookupUri() { 932 return mLookupUri; 933 } 934 935 @Override 936 protected void onStartLoading() { 937 if (mContact != null) { 938 deliverResult(mContact); 939 } 940 941 if (takeContentChanged() || mContact == null) { 942 forceLoad(); 943 } 944 } 945 946 @Override 947 protected void onStopLoading() { 948 cancelLoad(); 949 } 950 951 @Override 952 protected void onReset() { 953 super.onReset(); 954 cancelLoad(); 955 unregisterObserver(); 956 mContact = null; 957 } 958 959 /** 960 * Caches the result, which is useful when we switch from activity to activity, using the same 961 * contact. If the next load is for a different contact, the cached result will be dropped 962 */ 963 public void cacheResult() { 964 if (mContact == null || !mContact.isLoaded()) { 965 sCachedResult = null; 966 } else { 967 sCachedResult = mContact; 968 } 969 } 970 } 971