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