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