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