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