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