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.text.TextUtils; 39 import android.util.Log; 40 41 import com.android.contacts.GroupMetaData; 42 import com.android.contacts.common.GeoUtil; 43 import com.android.contacts.common.model.AccountTypeManager; 44 import com.android.contacts.common.model.account.AccountType; 45 import com.android.contacts.common.model.account.AccountTypeWithDataSet; 46 import com.android.contacts.common.util.Constants; 47 import com.android.contacts.common.util.UriUtils; 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.google.common.collect.ImmutableList; 54 import com.google.common.collect.ImmutableMap; 55 import com.google.common.collect.Maps; 56 import com.google.common.collect.Sets; 57 58 import org.json.JSONArray; 59 import org.json.JSONException; 60 import org.json.JSONObject; 61 62 import java.io.ByteArrayOutputStream; 63 import java.io.IOException; 64 import java.io.InputStream; 65 import java.net.URL; 66 import java.util.ArrayList; 67 import java.util.Iterator; 68 import java.util.List; 69 import java.util.Map; 70 import java.util.Set; 71 72 /** 73 * Loads a single Contact and all it constituent RawContacts. 74 */ 75 public class ContactLoader extends AsyncTaskLoader<Contact> { 76 77 private static final String TAG = ContactLoader.class.getSimpleName(); 78 79 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 80 81 /** A short-lived cache that can be set by {@link #cacheResult()} */ 82 private static Contact sCachedResult = null; 83 84 private final Uri mRequestedUri; 85 private Uri mLookupUri; 86 private boolean mLoadGroupMetaData; 87 private boolean mLoadInvitableAccountTypes; 88 private boolean mPostViewNotification; 89 private boolean mComputeFormattedPhoneNumber; 90 private Contact mContact; 91 private ForceLoadContentObserver mObserver; 92 private final Set<Long> mNotifiedRawContactIds = Sets.newHashSet(); 93 94 public ContactLoader(Context context, Uri lookupUri, boolean postViewNotification) { 95 this(context, lookupUri, false, false, postViewNotification, false); 96 } 97 98 public ContactLoader(Context context, Uri lookupUri, boolean loadGroupMetaData, 99 boolean loadInvitableAccountTypes, 100 boolean postViewNotification, boolean computeFormattedPhoneNumber) { 101 super(context); 102 mLookupUri = lookupUri; 103 mRequestedUri = lookupUri; 104 mLoadGroupMetaData = loadGroupMetaData; 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 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 if (uriCurrentFormat.getLastPathSegment().equals(Constants.LOOKUP_URI_ENCODED)) { 322 result = loadEncodedContactEntity(uriCurrentFormat); 323 } else { 324 result = loadContactEntity(resolver, uriCurrentFormat); 325 } 326 resultIsCached = false; 327 } 328 if (result.isLoaded()) { 329 if (result.isDirectoryEntry()) { 330 if (!resultIsCached) { 331 loadDirectoryMetaData(result); 332 } 333 } else if (mLoadGroupMetaData) { 334 if (result.getGroupMetaData() == null) { 335 loadGroupMetaData(result); 336 } 337 } 338 if (mComputeFormattedPhoneNumber) { 339 computeFormattedPhoneNumbers(result); 340 } 341 if (!resultIsCached) loadPhotoBinaryData(result); 342 343 // Note ME profile should never have "Add connection" 344 if (mLoadInvitableAccountTypes && result.getInvitableAccountTypes() == null) { 345 loadInvitableAccountTypes(result); 346 } 347 } 348 return result; 349 } catch (Exception e) { 350 Log.e(TAG, "Error loading the contact: " + mLookupUri, e); 351 return Contact.forError(mRequestedUri, e); 352 } 353 } 354 355 private Contact loadEncodedContactEntity(Uri uri) throws JSONException { 356 final String jsonString = uri.getEncodedFragment(); 357 final JSONObject json = new JSONObject(jsonString); 358 359 final long directoryId = 360 Long.valueOf(uri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY)); 361 362 final String displayName = json.getString(Contacts.DISPLAY_NAME); 363 final String altDisplayName = json.optString( 364 Contacts.DISPLAY_NAME_ALTERNATIVE, displayName); 365 final int displayNameSource = json.getInt(Contacts.DISPLAY_NAME_SOURCE); 366 final String photoUri = json.optString(Contacts.PHOTO_URI, null); 367 final Contact contact = new Contact( 368 uri, uri, 369 mLookupUri, 370 directoryId, 371 null /* lookupKey */, 372 -1 /* id */, 373 -1 /* nameRawContactId */, 374 displayNameSource, 375 0 /* photoId */, 376 photoUri, 377 displayName, 378 altDisplayName, 379 null /* phoneticName */, 380 false /* starred */, 381 null /* presence */, 382 false /* sendToVoicemail */, 383 null /* customRingtone */, 384 false /* isUserProfile */); 385 386 contact.setStatuses(new ImmutableMap.Builder<Long, DataStatus>().build()); 387 388 final String accountName = json.optString(RawContacts.ACCOUNT_NAME, null); 389 final String directoryName = uri.getQueryParameter(Directory.DISPLAY_NAME); 390 if (accountName != null) { 391 final String accountType = json.getString(RawContacts.ACCOUNT_TYPE); 392 contact.setDirectoryMetaData(directoryName, null, accountName, accountType, 393 json.optInt(Directory.EXPORT_SUPPORT, 394 Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY)); 395 } else { 396 contact.setDirectoryMetaData(directoryName, null, null, null, 397 json.optInt(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_ANY_ACCOUNT)); 398 } 399 400 final ContentValues values = new ContentValues(); 401 values.put(Data._ID, -1); 402 values.put(Data.CONTACT_ID, -1); 403 final RawContact rawContact = new RawContact(values); 404 405 final JSONObject items = json.getJSONObject(Contacts.CONTENT_ITEM_TYPE); 406 final Iterator keys = items.keys(); 407 while (keys.hasNext()) { 408 final String mimetype = (String) keys.next(); 409 410 // Could be single object or array. 411 final JSONObject obj = items.optJSONObject(mimetype); 412 if (obj == null) { 413 final JSONArray array = items.getJSONArray(mimetype); 414 for (int i = 0; i < array.length(); i++) { 415 final JSONObject item = array.getJSONObject(i); 416 processOneRecord(rawContact, item, mimetype); 417 } 418 } else { 419 processOneRecord(rawContact, obj, mimetype); 420 } 421 } 422 423 contact.setRawContacts(new ImmutableList.Builder<RawContact>() 424 .add(rawContact) 425 .build()); 426 return contact; 427 } 428 429 private void processOneRecord(RawContact rawContact, JSONObject item, String mimetype) 430 throws JSONException { 431 final ContentValues itemValues = new ContentValues(); 432 itemValues.put(Data.MIMETYPE, mimetype); 433 itemValues.put(Data._ID, -1); 434 435 final Iterator iterator = item.keys(); 436 while (iterator.hasNext()) { 437 String name = (String) iterator.next(); 438 final Object o = item.get(name); 439 if (o instanceof String) { 440 itemValues.put(name, (String) o); 441 } else if (o instanceof Integer) { 442 itemValues.put(name, (Integer) o); 443 } 444 } 445 rawContact.addDataItemValues(itemValues); 446 } 447 448 private Contact loadContactEntity(ContentResolver resolver, Uri contactUri) { 449 Uri entityUri = Uri.withAppendedPath(contactUri, Contacts.Entity.CONTENT_DIRECTORY); 450 Cursor cursor = resolver.query(entityUri, ContactQuery.COLUMNS, null, null, 451 Contacts.Entity.RAW_CONTACT_ID); 452 if (cursor == null) { 453 Log.e(TAG, "No cursor returned in loadContactEntity"); 454 return Contact.forNotFound(mRequestedUri); 455 } 456 457 try { 458 if (!cursor.moveToFirst()) { 459 cursor.close(); 460 return Contact.forNotFound(mRequestedUri); 461 } 462 463 // Create the loaded contact starting with the header data. 464 Contact contact = loadContactHeaderData(cursor, contactUri); 465 466 // Fill in the raw contacts, which is wrapped in an Entity and any 467 // status data. Initially, result has empty entities and statuses. 468 long currentRawContactId = -1; 469 RawContact rawContact = null; 470 ImmutableList.Builder<RawContact> rawContactsBuilder = 471 new ImmutableList.Builder<RawContact>(); 472 ImmutableMap.Builder<Long, DataStatus> statusesBuilder = 473 new ImmutableMap.Builder<Long, DataStatus>(); 474 do { 475 long rawContactId = cursor.getLong(ContactQuery.RAW_CONTACT_ID); 476 if (rawContactId != currentRawContactId) { 477 // First time to see this raw contact id, so create a new entity, and 478 // add it to the result's entities. 479 currentRawContactId = rawContactId; 480 rawContact = new RawContact(loadRawContactValues(cursor)); 481 rawContactsBuilder.add(rawContact); 482 } 483 if (!cursor.isNull(ContactQuery.DATA_ID)) { 484 ContentValues data = loadDataValues(cursor); 485 rawContact.addDataItemValues(data); 486 487 if (!cursor.isNull(ContactQuery.PRESENCE) 488 || !cursor.isNull(ContactQuery.STATUS)) { 489 final DataStatus status = new DataStatus(cursor); 490 final long dataId = cursor.getLong(ContactQuery.DATA_ID); 491 statusesBuilder.put(dataId, status); 492 } 493 } 494 } while (cursor.moveToNext()); 495 496 contact.setRawContacts(rawContactsBuilder.build()); 497 contact.setStatuses(statusesBuilder.build()); 498 499 return contact; 500 } finally { 501 cursor.close(); 502 } 503 } 504 505 /** 506 * Looks for the photo data item in entities. If found, creates a new Bitmap instance. If 507 * not found, returns null 508 */ 509 private void loadPhotoBinaryData(Contact contactData) { 510 // If we have a photo URI, try loading that first. 511 String photoUri = contactData.getPhotoUri(); 512 if (photoUri != null) { 513 try { 514 final InputStream inputStream; 515 final AssetFileDescriptor fd; 516 final Uri uri = Uri.parse(photoUri); 517 final String scheme = uri.getScheme(); 518 if ("http".equals(scheme) || "https".equals(scheme)) { 519 // Support HTTP urls that might come from extended directories 520 inputStream = new URL(photoUri).openStream(); 521 fd = null; 522 } else { 523 fd = getContext().getContentResolver().openAssetFileDescriptor(uri, "r"); 524 inputStream = fd.createInputStream(); 525 } 526 byte[] buffer = new byte[16 * 1024]; 527 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 528 try { 529 int size; 530 while ((size = inputStream.read(buffer)) != -1) { 531 baos.write(buffer, 0, size); 532 } 533 contactData.setPhotoBinaryData(baos.toByteArray()); 534 } finally { 535 inputStream.close(); 536 if (fd != null) { 537 fd.close(); 538 } 539 } 540 return; 541 } catch (IOException ioe) { 542 // Just fall back to the case below. 543 } 544 } 545 546 // If we couldn't load from a file, fall back to the data blob. 547 final long photoId = contactData.getPhotoId(); 548 if (photoId <= 0) { 549 // No photo ID 550 return; 551 } 552 553 for (RawContact rawContact : contactData.getRawContacts()) { 554 for (DataItem dataItem : rawContact.getDataItems()) { 555 if (dataItem.getId() == photoId) { 556 if (!(dataItem instanceof PhotoDataItem)) { 557 break; 558 } 559 560 final PhotoDataItem photo = (PhotoDataItem) dataItem; 561 contactData.setPhotoBinaryData(photo.getPhoto()); 562 break; 563 } 564 } 565 } 566 } 567 568 /** 569 * Sets the "invitable" account types to {@link Contact#mInvitableAccountTypes}. 570 */ 571 private void loadInvitableAccountTypes(Contact contactData) { 572 final ImmutableList.Builder<AccountType> resultListBuilder = 573 new ImmutableList.Builder<AccountType>(); 574 if (!contactData.isUserProfile()) { 575 Map<AccountTypeWithDataSet, AccountType> invitables = 576 AccountTypeManager.getInstance(getContext()).getUsableInvitableAccountTypes(); 577 if (!invitables.isEmpty()) { 578 final Map<AccountTypeWithDataSet, AccountType> resultMap = 579 Maps.newHashMap(invitables); 580 581 // Remove the ones that already have a raw contact in the current contact 582 for (RawContact rawContact : contactData.getRawContacts()) { 583 final AccountTypeWithDataSet type = AccountTypeWithDataSet.get( 584 rawContact.getAccountTypeString(), 585 rawContact.getDataSet()); 586 resultMap.remove(type); 587 } 588 589 resultListBuilder.addAll(resultMap.values()); 590 } 591 } 592 593 // Set to mInvitableAccountTypes 594 contactData.setInvitableAccountTypes(resultListBuilder.build()); 595 } 596 597 /** 598 * Extracts Contact level columns from the cursor. 599 */ 600 private Contact loadContactHeaderData(final Cursor cursor, Uri contactUri) { 601 final String directoryParameter = 602 contactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY); 603 final long directoryId = directoryParameter == null 604 ? Directory.DEFAULT 605 : Long.parseLong(directoryParameter); 606 final long contactId = cursor.getLong(ContactQuery.CONTACT_ID); 607 final String lookupKey = cursor.getString(ContactQuery.LOOKUP_KEY); 608 final long nameRawContactId = cursor.getLong(ContactQuery.NAME_RAW_CONTACT_ID); 609 final int displayNameSource = cursor.getInt(ContactQuery.DISPLAY_NAME_SOURCE); 610 final String displayName = cursor.getString(ContactQuery.DISPLAY_NAME); 611 final String altDisplayName = cursor.getString(ContactQuery.ALT_DISPLAY_NAME); 612 final String phoneticName = cursor.getString(ContactQuery.PHONETIC_NAME); 613 final long photoId = cursor.getLong(ContactQuery.PHOTO_ID); 614 final String photoUri = cursor.getString(ContactQuery.PHOTO_URI); 615 final boolean starred = cursor.getInt(ContactQuery.STARRED) != 0; 616 final Integer presence = cursor.isNull(ContactQuery.CONTACT_PRESENCE) 617 ? null 618 : cursor.getInt(ContactQuery.CONTACT_PRESENCE); 619 final boolean sendToVoicemail = cursor.getInt(ContactQuery.SEND_TO_VOICEMAIL) == 1; 620 final String customRingtone = cursor.getString(ContactQuery.CUSTOM_RINGTONE); 621 final boolean isUserProfile = cursor.getInt(ContactQuery.IS_USER_PROFILE) == 1; 622 623 Uri lookupUri; 624 if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) { 625 lookupUri = ContentUris.withAppendedId( 626 Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), contactId); 627 } else { 628 lookupUri = contactUri; 629 } 630 631 return new Contact(mRequestedUri, contactUri, lookupUri, directoryId, lookupKey, 632 contactId, nameRawContactId, displayNameSource, photoId, photoUri, displayName, 633 altDisplayName, phoneticName, starred, presence, sendToVoicemail, 634 customRingtone, isUserProfile); 635 } 636 637 /** 638 * Extracts RawContact level columns from the cursor. 639 */ 640 private ContentValues loadRawContactValues(Cursor cursor) { 641 ContentValues cv = new ContentValues(); 642 643 cv.put(RawContacts._ID, cursor.getLong(ContactQuery.RAW_CONTACT_ID)); 644 645 cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_NAME); 646 cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE); 647 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SET); 648 cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE_AND_DATA_SET); 649 cursorColumnToContentValues(cursor, cv, ContactQuery.DIRTY); 650 cursorColumnToContentValues(cursor, cv, ContactQuery.VERSION); 651 cursorColumnToContentValues(cursor, cv, ContactQuery.SOURCE_ID); 652 cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC1); 653 cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC2); 654 cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC3); 655 cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC4); 656 cursorColumnToContentValues(cursor, cv, ContactQuery.DELETED); 657 cursorColumnToContentValues(cursor, cv, ContactQuery.CONTACT_ID); 658 cursorColumnToContentValues(cursor, cv, ContactQuery.STARRED); 659 cursorColumnToContentValues(cursor, cv, ContactQuery.NAME_VERIFIED); 660 661 return cv; 662 } 663 664 /** 665 * Extracts Data level columns from the cursor. 666 */ 667 private ContentValues loadDataValues(Cursor cursor) { 668 ContentValues cv = new ContentValues(); 669 670 cv.put(Data._ID, cursor.getLong(ContactQuery.DATA_ID)); 671 672 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA1); 673 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA2); 674 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA3); 675 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA4); 676 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA5); 677 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA6); 678 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA7); 679 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA8); 680 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA9); 681 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA10); 682 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA11); 683 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA12); 684 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA13); 685 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA14); 686 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA15); 687 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC1); 688 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC2); 689 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC3); 690 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC4); 691 cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_VERSION); 692 cursorColumnToContentValues(cursor, cv, ContactQuery.IS_PRIMARY); 693 cursorColumnToContentValues(cursor, cv, ContactQuery.IS_SUPERPRIMARY); 694 cursorColumnToContentValues(cursor, cv, ContactQuery.MIMETYPE); 695 cursorColumnToContentValues(cursor, cv, ContactQuery.RES_PACKAGE); 696 cursorColumnToContentValues(cursor, cv, ContactQuery.GROUP_SOURCE_ID); 697 cursorColumnToContentValues(cursor, cv, ContactQuery.CHAT_CAPABILITY); 698 699 return cv; 700 } 701 702 private void cursorColumnToContentValues( 703 Cursor cursor, ContentValues values, int index) { 704 switch (cursor.getType(index)) { 705 case Cursor.FIELD_TYPE_NULL: 706 // don't put anything in the content values 707 break; 708 case Cursor.FIELD_TYPE_INTEGER: 709 values.put(ContactQuery.COLUMNS[index], cursor.getLong(index)); 710 break; 711 case Cursor.FIELD_TYPE_STRING: 712 values.put(ContactQuery.COLUMNS[index], cursor.getString(index)); 713 break; 714 case Cursor.FIELD_TYPE_BLOB: 715 values.put(ContactQuery.COLUMNS[index], cursor.getBlob(index)); 716 break; 717 default: 718 throw new IllegalStateException("Invalid or unhandled data type"); 719 } 720 } 721 722 private void loadDirectoryMetaData(Contact result) { 723 long directoryId = result.getDirectoryId(); 724 725 Cursor cursor = getContext().getContentResolver().query( 726 ContentUris.withAppendedId(Directory.CONTENT_URI, directoryId), 727 DirectoryQuery.COLUMNS, null, null, null); 728 if (cursor == null) { 729 return; 730 } 731 try { 732 if (cursor.moveToFirst()) { 733 final String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME); 734 final String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME); 735 final int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID); 736 final String accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE); 737 final String accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME); 738 final int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT); 739 String directoryType = null; 740 if (!TextUtils.isEmpty(packageName)) { 741 PackageManager pm = getContext().getPackageManager(); 742 try { 743 Resources resources = pm.getResourcesForApplication(packageName); 744 directoryType = resources.getString(typeResourceId); 745 } catch (NameNotFoundException e) { 746 Log.w(TAG, "Contact directory resource not found: " 747 + packageName + "." + typeResourceId); 748 } 749 } 750 751 result.setDirectoryMetaData( 752 displayName, directoryType, accountType, accountName, exportSupport); 753 } 754 } finally { 755 cursor.close(); 756 } 757 } 758 759 /** 760 * Loads groups meta-data for all groups associated with all constituent raw contacts' 761 * accounts. 762 */ 763 private void loadGroupMetaData(Contact result) { 764 StringBuilder selection = new StringBuilder(); 765 ArrayList<String> selectionArgs = new ArrayList<String>(); 766 for (RawContact rawContact : result.getRawContacts()) { 767 final String accountName = rawContact.getAccountName(); 768 final String accountType = rawContact.getAccountTypeString(); 769 final String dataSet = rawContact.getDataSet(); 770 if (accountName != null && accountType != null) { 771 if (selection.length() != 0) { 772 selection.append(" OR "); 773 } 774 selection.append( 775 "(" + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?"); 776 selectionArgs.add(accountName); 777 selectionArgs.add(accountType); 778 779 if (dataSet != null) { 780 selection.append(" AND " + Groups.DATA_SET + "=?"); 781 selectionArgs.add(dataSet); 782 } else { 783 selection.append(" AND " + Groups.DATA_SET + " IS NULL"); 784 } 785 selection.append(")"); 786 } 787 } 788 final ImmutableList.Builder<GroupMetaData> groupListBuilder = 789 new ImmutableList.Builder<GroupMetaData>(); 790 final Cursor cursor = getContext().getContentResolver().query(Groups.CONTENT_URI, 791 GroupQuery.COLUMNS, selection.toString(), selectionArgs.toArray(new String[0]), 792 null); 793 try { 794 while (cursor.moveToNext()) { 795 final String accountName = cursor.getString(GroupQuery.ACCOUNT_NAME); 796 final String accountType = cursor.getString(GroupQuery.ACCOUNT_TYPE); 797 final String dataSet = cursor.getString(GroupQuery.DATA_SET); 798 final long groupId = cursor.getLong(GroupQuery.ID); 799 final String title = cursor.getString(GroupQuery.TITLE); 800 final boolean defaultGroup = cursor.isNull(GroupQuery.AUTO_ADD) 801 ? false 802 : cursor.getInt(GroupQuery.AUTO_ADD) != 0; 803 final boolean favorites = cursor.isNull(GroupQuery.FAVORITES) 804 ? false 805 : cursor.getInt(GroupQuery.FAVORITES) != 0; 806 807 groupListBuilder.add(new GroupMetaData( 808 accountName, accountType, dataSet, groupId, title, defaultGroup, 809 favorites)); 810 } 811 } finally { 812 cursor.close(); 813 } 814 result.setGroupMetaData(groupListBuilder.build()); 815 } 816 817 /** 818 * Iterates over all data items that represent phone numbers are tries to calculate a formatted 819 * number. This function can safely be called several times as no unformatted data is 820 * overwritten 821 */ 822 private void computeFormattedPhoneNumbers(Contact contactData) { 823 final String countryIso = GeoUtil.getCurrentCountryIso(getContext()); 824 final ImmutableList<RawContact> rawContacts = contactData.getRawContacts(); 825 final int rawContactCount = rawContacts.size(); 826 for (int rawContactIndex = 0; rawContactIndex < rawContactCount; rawContactIndex++) { 827 final RawContact rawContact = rawContacts.get(rawContactIndex); 828 final List<DataItem> dataItems = rawContact.getDataItems(); 829 final int dataCount = dataItems.size(); 830 for (int dataIndex = 0; dataIndex < dataCount; dataIndex++) { 831 final DataItem dataItem = dataItems.get(dataIndex); 832 if (dataItem instanceof PhoneDataItem) { 833 final PhoneDataItem phoneDataItem = (PhoneDataItem) dataItem; 834 phoneDataItem.computeFormattedPhoneNumber(countryIso); 835 } 836 } 837 } 838 } 839 840 @Override 841 public void deliverResult(Contact result) { 842 unregisterObserver(); 843 844 // The creator isn't interested in any further updates 845 if (isReset() || result == null) { 846 return; 847 } 848 849 mContact = result; 850 851 if (result.isLoaded()) { 852 mLookupUri = result.getLookupUri(); 853 854 if (!result.isDirectoryEntry()) { 855 Log.i(TAG, "Registering content observer for " + mLookupUri); 856 if (mObserver == null) { 857 mObserver = new ForceLoadContentObserver(); 858 } 859 getContext().getContentResolver().registerContentObserver( 860 mLookupUri, true, mObserver); 861 } 862 863 if (mPostViewNotification) { 864 // inform the source of the data that this contact is being looked at 865 postViewNotificationToSyncAdapter(); 866 } 867 } 868 869 super.deliverResult(mContact); 870 } 871 872 /** 873 * Posts a message to the contributing sync adapters that have opted-in, notifying them 874 * that the contact has just been loaded 875 */ 876 private void postViewNotificationToSyncAdapter() { 877 Context context = getContext(); 878 for (RawContact rawContact : mContact.getRawContacts()) { 879 final long rawContactId = rawContact.getId(); 880 if (mNotifiedRawContactIds.contains(rawContactId)) { 881 continue; // Already notified for this raw contact. 882 } 883 mNotifiedRawContactIds.add(rawContactId); 884 final AccountType accountType = rawContact.getAccountType(context); 885 final String serviceName = accountType.getViewContactNotifyServiceClassName(); 886 final String servicePackageName = accountType.getViewContactNotifyServicePackageName(); 887 if (!TextUtils.isEmpty(serviceName) && !TextUtils.isEmpty(servicePackageName)) { 888 final Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId); 889 final Intent intent = new Intent(); 890 intent.setClassName(servicePackageName, serviceName); 891 intent.setAction(Intent.ACTION_VIEW); 892 intent.setDataAndType(uri, RawContacts.CONTENT_ITEM_TYPE); 893 try { 894 context.startService(intent); 895 } catch (Exception e) { 896 Log.e(TAG, "Error sending message to source-app", e); 897 } 898 } 899 } 900 } 901 902 private void unregisterObserver() { 903 if (mObserver != null) { 904 getContext().getContentResolver().unregisterContentObserver(mObserver); 905 mObserver = null; 906 } 907 } 908 909 /** 910 * Fully upgrades this ContactLoader to one with all lists fully loaded. When done, the 911 * new result will be delivered 912 */ 913 public void upgradeToFullContact() { 914 // Everything requested already? Nothing to do, so let's bail out 915 if (mLoadGroupMetaData && mLoadInvitableAccountTypes 916 && mPostViewNotification && mComputeFormattedPhoneNumber) return; 917 918 mLoadGroupMetaData = true; 919 mLoadInvitableAccountTypes = true; 920 mPostViewNotification = true; 921 mComputeFormattedPhoneNumber = true; 922 923 // Cache the current result, so that we only load the "missing" parts of the contact. 924 cacheResult(); 925 926 // Our load parameters have changed, so let's pretend the data has changed. Its the same 927 // thing, essentially. 928 onContentChanged(); 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