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