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