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