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