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