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.common.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.common.GeoUtil; 42 import com.android.contacts.common.GroupMetaData; 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.ContactLoaderUtils; 48 import com.android.contacts.common.util.DataStatus; 49 import com.android.contacts.common.util.UriUtils; 50 import com.android.contacts.common.model.dataitem.DataItem; 51 import com.android.contacts.common.model.dataitem.PhoneDataItem; 52 import com.android.contacts.common.model.dataitem.PhotoDataItem; 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 if (cursor != null) { 794 try { 795 while (cursor.moveToNext()) { 796 final String accountName = cursor.getString(GroupQuery.ACCOUNT_NAME); 797 final String accountType = cursor.getString(GroupQuery.ACCOUNT_TYPE); 798 final String dataSet = cursor.getString(GroupQuery.DATA_SET); 799 final long groupId = cursor.getLong(GroupQuery.ID); 800 final String title = cursor.getString(GroupQuery.TITLE); 801 final boolean defaultGroup = cursor.isNull(GroupQuery.AUTO_ADD) 802 ? false 803 : cursor.getInt(GroupQuery.AUTO_ADD) != 0; 804 final boolean favorites = cursor.isNull(GroupQuery.FAVORITES) 805 ? false 806 : cursor.getInt(GroupQuery.FAVORITES) != 0; 807 808 groupListBuilder.add(new GroupMetaData( 809 accountName, accountType, dataSet, groupId, title, defaultGroup, 810 favorites)); 811 } 812 } finally { 813 cursor.close(); 814 } 815 } 816 result.setGroupMetaData(groupListBuilder.build()); 817 } 818 819 /** 820 * Iterates over all data items that represent phone numbers are tries to calculate a formatted 821 * number. This function can safely be called several times as no unformatted data is 822 * overwritten 823 */ 824 private void computeFormattedPhoneNumbers(Contact contactData) { 825 final String countryIso = GeoUtil.getCurrentCountryIso(getContext()); 826 final ImmutableList<RawContact> rawContacts = contactData.getRawContacts(); 827 final int rawContactCount = rawContacts.size(); 828 for (int rawContactIndex = 0; rawContactIndex < rawContactCount; rawContactIndex++) { 829 final RawContact rawContact = rawContacts.get(rawContactIndex); 830 final List<DataItem> dataItems = rawContact.getDataItems(); 831 final int dataCount = dataItems.size(); 832 for (int dataIndex = 0; dataIndex < dataCount; dataIndex++) { 833 final DataItem dataItem = dataItems.get(dataIndex); 834 if (dataItem instanceof PhoneDataItem) { 835 final PhoneDataItem phoneDataItem = (PhoneDataItem) dataItem; 836 phoneDataItem.computeFormattedPhoneNumber(countryIso); 837 } 838 } 839 } 840 } 841 842 @Override 843 public void deliverResult(Contact result) { 844 unregisterObserver(); 845 846 // The creator isn't interested in any further updates 847 if (isReset() || result == null) { 848 return; 849 } 850 851 mContact = result; 852 853 if (result.isLoaded()) { 854 mLookupUri = result.getLookupUri(); 855 856 if (!result.isDirectoryEntry()) { 857 Log.i(TAG, "Registering content observer for " + mLookupUri); 858 if (mObserver == null) { 859 mObserver = new ForceLoadContentObserver(); 860 } 861 getContext().getContentResolver().registerContentObserver( 862 mLookupUri, true, mObserver); 863 } 864 865 if (mPostViewNotification) { 866 // inform the source of the data that this contact is being looked at 867 postViewNotificationToSyncAdapter(); 868 } 869 } 870 871 super.deliverResult(mContact); 872 } 873 874 /** 875 * Posts a message to the contributing sync adapters that have opted-in, notifying them 876 * that the contact has just been loaded 877 */ 878 private void postViewNotificationToSyncAdapter() { 879 Context context = getContext(); 880 for (RawContact rawContact : mContact.getRawContacts()) { 881 final long rawContactId = rawContact.getId(); 882 if (mNotifiedRawContactIds.contains(rawContactId)) { 883 continue; // Already notified for this raw contact. 884 } 885 mNotifiedRawContactIds.add(rawContactId); 886 final AccountType accountType = rawContact.getAccountType(context); 887 final String serviceName = accountType.getViewContactNotifyServiceClassName(); 888 final String servicePackageName = accountType.getViewContactNotifyServicePackageName(); 889 if (!TextUtils.isEmpty(serviceName) && !TextUtils.isEmpty(servicePackageName)) { 890 final Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId); 891 final Intent intent = new Intent(); 892 intent.setClassName(servicePackageName, serviceName); 893 intent.setAction(Intent.ACTION_VIEW); 894 intent.setDataAndType(uri, RawContacts.CONTENT_ITEM_TYPE); 895 try { 896 context.startService(intent); 897 } catch (Exception e) { 898 Log.e(TAG, "Error sending message to source-app", e); 899 } 900 } 901 } 902 } 903 904 private void unregisterObserver() { 905 if (mObserver != null) { 906 getContext().getContentResolver().unregisterContentObserver(mObserver); 907 mObserver = null; 908 } 909 } 910 911 /** 912 * Fully upgrades this ContactLoader to one with all lists fully loaded. When done, the 913 * new result will be delivered 914 */ 915 public void upgradeToFullContact() { 916 // Everything requested already? Nothing to do, so let's bail out 917 if (mLoadGroupMetaData && mLoadInvitableAccountTypes 918 && mPostViewNotification && mComputeFormattedPhoneNumber) return; 919 920 mLoadGroupMetaData = true; 921 mLoadInvitableAccountTypes = true; 922 mPostViewNotification = true; 923 mComputeFormattedPhoneNumber = true; 924 925 // Cache the current result, so that we only load the "missing" parts of the contact. 926 cacheResult(); 927 928 // Our load parameters have changed, so let's pretend the data has changed. Its the same 929 // thing, essentially. 930 onContentChanged(); 931 } 932 933 public Uri getLookupUri() { 934 return mLookupUri; 935 } 936 937 @Override 938 protected void onStartLoading() { 939 if (mContact != null) { 940 deliverResult(mContact); 941 } 942 943 if (takeContentChanged() || mContact == null) { 944 forceLoad(); 945 } 946 } 947 948 @Override 949 protected void onStopLoading() { 950 cancelLoad(); 951 } 952 953 @Override 954 protected void onReset() { 955 super.onReset(); 956 cancelLoad(); 957 unregisterObserver(); 958 mContact = null; 959 } 960 961 /** 962 * Caches the result, which is useful when we switch from activity to activity, using the same 963 * contact. If the next load is for a different contact, the cached result will be dropped 964 */ 965 public void cacheResult() { 966 if (mContact == null || !mContact.isLoaded()) { 967 sCachedResult = null; 968 } else { 969 sCachedResult = mContact; 970 } 971 } 972 } 973