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