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