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.dialer.common.LogUtil; 48 import com.android.dialer.location.GeoUtil; 49 import com.android.dialer.util.PermissionsUtil; 50 import com.android.dialer.util.UriUtils; 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.v(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 526 return cv; 527 } 528 cursorColumnToContentValues(Cursor cursor, ContentValues values, int index)529 private void cursorColumnToContentValues(Cursor cursor, ContentValues values, int index) { 530 switch (cursor.getType(index)) { 531 case Cursor.FIELD_TYPE_NULL: 532 // don't put anything in the content values 533 break; 534 case Cursor.FIELD_TYPE_INTEGER: 535 values.put(ContactQuery.COLUMNS[index], cursor.getLong(index)); 536 break; 537 case Cursor.FIELD_TYPE_STRING: 538 values.put(ContactQuery.COLUMNS[index], cursor.getString(index)); 539 break; 540 case Cursor.FIELD_TYPE_BLOB: 541 values.put(ContactQuery.COLUMNS[index], cursor.getBlob(index)); 542 break; 543 default: 544 throw new IllegalStateException("Invalid or unhandled data type"); 545 } 546 } 547 loadDirectoryMetaData(Contact result)548 private void loadDirectoryMetaData(Contact result) { 549 long directoryId = result.getDirectoryId(); 550 551 Cursor cursor = 552 getContext() 553 .getContentResolver() 554 .query( 555 ContentUris.withAppendedId(Directory.CONTENT_URI, directoryId), 556 DirectoryQuery.COLUMNS, 557 null, 558 null, 559 null); 560 if (cursor == null) { 561 return; 562 } 563 try { 564 if (cursor.moveToFirst()) { 565 final String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME); 566 final String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME); 567 final int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID); 568 final String accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE); 569 final String accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME); 570 final int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT); 571 String directoryType = null; 572 if (!TextUtils.isEmpty(packageName)) { 573 PackageManager pm = getContext().getPackageManager(); 574 try { 575 Resources resources = pm.getResourcesForApplication(packageName); 576 directoryType = resources.getString(typeResourceId); 577 } catch (NameNotFoundException e) { 578 LogUtil.w( 579 TAG, "Contact directory resource not found: " + packageName + "." + typeResourceId); 580 } 581 } 582 583 result.setDirectoryMetaData( 584 displayName, directoryType, accountType, accountName, exportSupport); 585 } 586 } finally { 587 cursor.close(); 588 } 589 } 590 591 /** 592 * Loads groups meta-data for all groups associated with all constituent raw contacts' accounts. 593 */ loadGroupMetaData(Contact result)594 private void loadGroupMetaData(Contact result) { 595 StringBuilder selection = new StringBuilder(); 596 ArrayList<String> selectionArgs = new ArrayList<String>(); 597 final HashSet<AccountKey> accountsSeen = new HashSet<>(); 598 for (RawContact rawContact : result.getRawContacts()) { 599 final String accountName = rawContact.getAccountName(); 600 final String accountType = rawContact.getAccountTypeString(); 601 final String dataSet = rawContact.getDataSet(); 602 final AccountKey accountKey = new AccountKey(accountName, accountType, dataSet); 603 if (accountName != null && accountType != null && !accountsSeen.contains(accountKey)) { 604 accountsSeen.add(accountKey); 605 if (selection.length() != 0) { 606 selection.append(" OR "); 607 } 608 selection.append("(" + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?"); 609 selectionArgs.add(accountName); 610 selectionArgs.add(accountType); 611 612 if (dataSet != null) { 613 selection.append(" AND " + Groups.DATA_SET + "=?"); 614 selectionArgs.add(dataSet); 615 } else { 616 selection.append(" AND " + Groups.DATA_SET + " IS NULL"); 617 } 618 selection.append(")"); 619 } 620 } 621 final ImmutableList.Builder<GroupMetaData> groupListBuilder = 622 new ImmutableList.Builder<GroupMetaData>(); 623 final Cursor cursor = 624 getContext() 625 .getContentResolver() 626 .query( 627 Groups.CONTENT_URI, 628 GroupQuery.COLUMNS, 629 selection.toString(), 630 selectionArgs.toArray(new String[0]), 631 null); 632 if (cursor != null) { 633 try { 634 while (cursor.moveToNext()) { 635 final String accountName = cursor.getString(GroupQuery.ACCOUNT_NAME); 636 final String accountType = cursor.getString(GroupQuery.ACCOUNT_TYPE); 637 final String dataSet = cursor.getString(GroupQuery.DATA_SET); 638 final long groupId = cursor.getLong(GroupQuery.ID); 639 final String title = cursor.getString(GroupQuery.TITLE); 640 final boolean defaultGroup = 641 !cursor.isNull(GroupQuery.AUTO_ADD) && cursor.getInt(GroupQuery.AUTO_ADD) != 0; 642 final boolean favorites = 643 !cursor.isNull(GroupQuery.FAVORITES) && cursor.getInt(GroupQuery.FAVORITES) != 0; 644 645 groupListBuilder.add( 646 new GroupMetaData( 647 accountName, accountType, dataSet, groupId, title, defaultGroup, favorites)); 648 } 649 } finally { 650 cursor.close(); 651 } 652 } 653 result.setGroupMetaData(groupListBuilder.build()); 654 } 655 656 /** 657 * Iterates over all data items that represent phone numbers are tries to calculate a formatted 658 * number. This function can safely be called several times as no unformatted data is overwritten 659 */ computeFormattedPhoneNumbers(Contact contactData)660 private void computeFormattedPhoneNumbers(Contact contactData) { 661 final String countryIso = GeoUtil.getCurrentCountryIso(getContext()); 662 final ImmutableList<RawContact> rawContacts = contactData.getRawContacts(); 663 final int rawContactCount = rawContacts.size(); 664 for (int rawContactIndex = 0; rawContactIndex < rawContactCount; rawContactIndex++) { 665 final RawContact rawContact = rawContacts.get(rawContactIndex); 666 final List<DataItem> dataItems = rawContact.getDataItems(); 667 final int dataCount = dataItems.size(); 668 for (int dataIndex = 0; dataIndex < dataCount; dataIndex++) { 669 final DataItem dataItem = dataItems.get(dataIndex); 670 if (dataItem instanceof PhoneDataItem) { 671 final PhoneDataItem phoneDataItem = (PhoneDataItem) dataItem; 672 phoneDataItem.computeFormattedPhoneNumber(getContext(), countryIso); 673 } 674 } 675 } 676 } 677 678 @Override deliverResult(Contact result)679 public void deliverResult(Contact result) { 680 unregisterObserver(); 681 682 // The creator isn't interested in any further updates 683 if (isReset() || result == null) { 684 return; 685 } 686 687 mContact = result; 688 689 if (result.isLoaded()) { 690 mLookupUri = result.getLookupUri(); 691 692 if (!result.isDirectoryEntry()) { 693 if (mObserver == null) { 694 mObserver = new ForceLoadContentObserver(); 695 } 696 697 if (PermissionsUtil.hasContactsReadPermissions(getContext())) { 698 getContext().getContentResolver().registerContentObserver(mLookupUri, true, mObserver); 699 } else { 700 LogUtil.w("ContactLoader.deliverResult", "contacts permission not available"); 701 } 702 } 703 704 if (mPostViewNotification) { 705 // inform the source of the data that this contact is being looked at 706 postViewNotificationToSyncAdapter(); 707 } 708 } 709 710 super.deliverResult(mContact); 711 } 712 713 /** 714 * Posts a message to the contributing sync adapters that have opted-in, notifying them that the 715 * contact has just been loaded 716 */ postViewNotificationToSyncAdapter()717 private void postViewNotificationToSyncAdapter() { 718 Context context = getContext(); 719 for (RawContact rawContact : mContact.getRawContacts()) { 720 final long rawContactId = rawContact.getId(); 721 if (mNotifiedRawContactIds.contains(rawContactId)) { 722 continue; // Already notified for this raw contact. 723 } 724 mNotifiedRawContactIds.add(rawContactId); 725 final AccountType accountType = rawContact.getAccountType(context); 726 final String serviceName = accountType.getViewContactNotifyServiceClassName(); 727 final String servicePackageName = accountType.getViewContactNotifyServicePackageName(); 728 if (!TextUtils.isEmpty(serviceName) && !TextUtils.isEmpty(servicePackageName)) { 729 final Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId); 730 final Intent intent = new Intent(); 731 intent.setClassName(servicePackageName, serviceName); 732 intent.setAction(Intent.ACTION_VIEW); 733 intent.setDataAndType(uri, RawContacts.CONTENT_ITEM_TYPE); 734 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 735 try { 736 context.startService(intent); 737 } catch (Exception e) { 738 LogUtil.e(TAG, "Error sending message to source-app", e); 739 } 740 } 741 } 742 } 743 unregisterObserver()744 private void unregisterObserver() { 745 if (mObserver != null) { 746 getContext().getContentResolver().unregisterContentObserver(mObserver); 747 mObserver = null; 748 } 749 } 750 getLookupUri()751 public Uri getLookupUri() { 752 return mLookupUri; 753 } 754 setLookupUri(Uri lookupUri)755 public void setLookupUri(Uri lookupUri) { 756 mLookupUri = lookupUri; 757 } 758 759 @Override onStartLoading()760 protected void onStartLoading() { 761 if (mContact != null) { 762 deliverResult(mContact); 763 } 764 765 if (takeContentChanged() || mContact == null) { 766 forceLoad(); 767 } 768 } 769 770 @Override onStopLoading()771 protected void onStopLoading() { 772 cancelLoad(); 773 } 774 775 @Override onReset()776 protected void onReset() { 777 super.onReset(); 778 cancelLoad(); 779 unregisterObserver(); 780 mContact = null; 781 } 782 783 /** 784 * Projection used for the query that loads all data for the entire contact (except for social 785 * stream items). 786 */ 787 private static class ContactQuery { 788 789 public static final int NAME_RAW_CONTACT_ID = 0; 790 public static final int DISPLAY_NAME_SOURCE = 1; 791 public static final int LOOKUP_KEY = 2; 792 public static final int DISPLAY_NAME = 3; 793 public static final int ALT_DISPLAY_NAME = 4; 794 public static final int PHONETIC_NAME = 5; 795 public static final int PHOTO_ID = 6; 796 public static final int STARRED = 7; 797 public static final int CONTACT_PRESENCE = 8; 798 public static final int CONTACT_STATUS = 9; 799 public static final int CONTACT_STATUS_TIMESTAMP = 10; 800 public static final int CONTACT_STATUS_RES_PACKAGE = 11; 801 public static final int CONTACT_STATUS_LABEL = 12; 802 public static final int CONTACT_ID = 13; 803 public static final int RAW_CONTACT_ID = 14; 804 public static final int ACCOUNT_NAME = 15; 805 public static final int ACCOUNT_TYPE = 16; 806 public static final int DATA_SET = 17; 807 public static final int DIRTY = 18; 808 public static final int VERSION = 19; 809 public static final int SOURCE_ID = 20; 810 public static final int SYNC1 = 21; 811 public static final int SYNC2 = 22; 812 public static final int SYNC3 = 23; 813 public static final int SYNC4 = 24; 814 public static final int DELETED = 25; 815 public static final int DATA_ID = 26; 816 public static final int DATA1 = 27; 817 public static final int DATA2 = 28; 818 public static final int DATA3 = 29; 819 public static final int DATA4 = 30; 820 public static final int DATA5 = 31; 821 public static final int DATA6 = 32; 822 public static final int DATA7 = 33; 823 public static final int DATA8 = 34; 824 public static final int DATA9 = 35; 825 public static final int DATA10 = 36; 826 public static final int DATA11 = 37; 827 public static final int DATA12 = 38; 828 public static final int DATA13 = 39; 829 public static final int DATA14 = 40; 830 public static final int DATA15 = 41; 831 public static final int DATA_SYNC1 = 42; 832 public static final int DATA_SYNC2 = 43; 833 public static final int DATA_SYNC3 = 44; 834 public static final int DATA_SYNC4 = 45; 835 public static final int DATA_VERSION = 46; 836 public static final int IS_PRIMARY = 47; 837 public static final int IS_SUPERPRIMARY = 48; 838 public static final int MIMETYPE = 49; 839 public static final int GROUP_SOURCE_ID = 50; 840 public static final int PRESENCE = 51; 841 public static final int CHAT_CAPABILITY = 52; 842 public static final int STATUS = 53; 843 public static final int STATUS_RES_PACKAGE = 54; 844 public static final int STATUS_ICON = 55; 845 public static final int STATUS_LABEL = 56; 846 public static final int STATUS_TIMESTAMP = 57; 847 public static final int PHOTO_URI = 58; 848 public static final int SEND_TO_VOICEMAIL = 59; 849 public static final int CUSTOM_RINGTONE = 60; 850 public static final int IS_USER_PROFILE = 61; 851 public static final int TIMES_USED = 62; 852 public static final int LAST_TIME_USED = 63; 853 public static final int CARRIER_PRESENCE = 64; 854 static final String[] COLUMNS_INTERNAL = 855 new String[] { 856 Contacts.NAME_RAW_CONTACT_ID, 857 Contacts.DISPLAY_NAME_SOURCE, 858 Contacts.LOOKUP_KEY, 859 Contacts.DISPLAY_NAME, 860 Contacts.DISPLAY_NAME_ALTERNATIVE, 861 Contacts.PHONETIC_NAME, 862 Contacts.PHOTO_ID, 863 Contacts.STARRED, 864 Contacts.CONTACT_PRESENCE, 865 Contacts.CONTACT_STATUS, 866 Contacts.CONTACT_STATUS_TIMESTAMP, 867 Contacts.CONTACT_STATUS_RES_PACKAGE, 868 Contacts.CONTACT_STATUS_LABEL, 869 Contacts.Entity.CONTACT_ID, 870 Contacts.Entity.RAW_CONTACT_ID, 871 RawContacts.ACCOUNT_NAME, 872 RawContacts.ACCOUNT_TYPE, 873 RawContacts.DATA_SET, 874 RawContacts.DIRTY, 875 RawContacts.VERSION, 876 RawContacts.SOURCE_ID, 877 RawContacts.SYNC1, 878 RawContacts.SYNC2, 879 RawContacts.SYNC3, 880 RawContacts.SYNC4, 881 RawContacts.DELETED, 882 Contacts.Entity.DATA_ID, 883 Data.DATA1, 884 Data.DATA2, 885 Data.DATA3, 886 Data.DATA4, 887 Data.DATA5, 888 Data.DATA6, 889 Data.DATA7, 890 Data.DATA8, 891 Data.DATA9, 892 Data.DATA10, 893 Data.DATA11, 894 Data.DATA12, 895 Data.DATA13, 896 Data.DATA14, 897 Data.DATA15, 898 Data.SYNC1, 899 Data.SYNC2, 900 Data.SYNC3, 901 Data.SYNC4, 902 Data.DATA_VERSION, 903 Data.IS_PRIMARY, 904 Data.IS_SUPER_PRIMARY, 905 Data.MIMETYPE, 906 GroupMembership.GROUP_SOURCE_ID, 907 Data.PRESENCE, 908 Data.CHAT_CAPABILITY, 909 Data.STATUS, 910 Data.STATUS_RES_PACKAGE, 911 Data.STATUS_ICON, 912 Data.STATUS_LABEL, 913 Data.STATUS_TIMESTAMP, 914 Contacts.PHOTO_URI, 915 Contacts.SEND_TO_VOICEMAIL, 916 Contacts.CUSTOM_RINGTONE, 917 Contacts.IS_USER_PROFILE, 918 Data.TIMES_USED, 919 Data.LAST_TIME_USED 920 }; 921 static final String[] COLUMNS; 922 923 static { 924 List<String> projectionList = Lists.newArrayList(COLUMNS_INTERNAL); 925 projectionList.add(Data.CARRIER_PRESENCE); 926 COLUMNS = projectionList.toArray(new String[projectionList.size()]); 927 } 928 } 929 930 /** Projection used for the query that loads all data for the entire contact. */ 931 private static class DirectoryQuery { 932 933 public static final int DISPLAY_NAME = 0; 934 public static final int PACKAGE_NAME = 1; 935 public static final int TYPE_RESOURCE_ID = 2; 936 public static final int ACCOUNT_TYPE = 3; 937 public static final int ACCOUNT_NAME = 4; 938 public static final int EXPORT_SUPPORT = 5; 939 static final String[] COLUMNS = 940 new String[] { 941 Directory.DISPLAY_NAME, 942 Directory.PACKAGE_NAME, 943 Directory.TYPE_RESOURCE_ID, 944 Directory.ACCOUNT_TYPE, 945 Directory.ACCOUNT_NAME, 946 Directory.EXPORT_SUPPORT, 947 }; 948 } 949 950 private static class GroupQuery { 951 952 public static final int ACCOUNT_NAME = 0; 953 public static final int ACCOUNT_TYPE = 1; 954 public static final int DATA_SET = 2; 955 public static final int ID = 3; 956 public static final int TITLE = 4; 957 public static final int AUTO_ADD = 5; 958 public static final int FAVORITES = 6; 959 static final String[] COLUMNS = 960 new String[] { 961 Groups.ACCOUNT_NAME, 962 Groups.ACCOUNT_TYPE, 963 Groups.DATA_SET, 964 Groups._ID, 965 Groups.TITLE, 966 Groups.AUTO_ADD, 967 Groups.FAVORITES, 968 }; 969 } 970 971 private static class AccountKey { 972 973 private final String mAccountName; 974 private final String mAccountType; 975 private final String mDataSet; 976 AccountKey(String accountName, String accountType, String dataSet)977 public AccountKey(String accountName, String accountType, String dataSet) { 978 mAccountName = accountName; 979 mAccountType = accountType; 980 mDataSet = dataSet; 981 } 982 983 @Override hashCode()984 public int hashCode() { 985 return Objects.hash(mAccountName, mAccountType, mDataSet); 986 } 987 988 @Override equals(Object obj)989 public boolean equals(Object obj) { 990 if (!(obj instanceof AccountKey)) { 991 return false; 992 } 993 final AccountKey other = (AccountKey) obj; 994 return Objects.equals(mAccountName, other.mAccountName) 995 && Objects.equals(mAccountType, other.mAccountType) 996 && Objects.equals(mDataSet, other.mDataSet); 997 } 998 } 999 } 1000