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