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