1 /*
2  * Copyright (C) 2012 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.ContentValues;
20 import android.content.Context;
21 import android.net.Uri;
22 import android.provider.ContactsContract.CommonDataKinds.Photo;
23 import android.provider.ContactsContract.Data;
24 import android.provider.ContactsContract.Directory;
25 import android.provider.ContactsContract.DisplayNameSources;
26 
27 import com.android.contacts.group.GroupMetaData;
28 import com.android.contacts.model.account.AccountType;
29 import com.android.contacts.model.account.SimAccountType;
30 import com.android.contacts.util.DataStatus;
31 
32 import com.google.common.annotations.VisibleForTesting;
33 import com.google.common.collect.ImmutableList;
34 import com.google.common.collect.ImmutableMap;
35 
36 import java.util.ArrayList;
37 
38 /**
39  * A Contact represents a single person or logical entity as perceived by the user.  The information
40  * about a contact can come from multiple data sources, which are each represented by a RawContact
41  * object.  Thus, a Contact is associated with a collection of RawContact objects.
42  *
43  * The aggregation of raw contacts into a single contact is performed automatically, and it is
44  * also possible for users to manually split and join raw contacts into various contacts.
45  *
46  * Only the {@link ContactLoader} class can create a Contact object with various flags to allow
47  * partial loading of contact data.  Thus, an instance of this class should be treated as
48  * a read-only object.
49  */
50 public class Contact {
51     private enum Status {
52         /** Contact is successfully loaded */
53         LOADED,
54         /** There was an error loading the contact */
55         ERROR,
56         /** Contact is not found */
57         NOT_FOUND,
58     }
59 
60     private final Uri mRequestedUri;
61     private final Uri mLookupUri;
62     private final Uri mUri;
63     private final long mDirectoryId;
64     private final String mLookupKey;
65     private final long mId;
66     private final long mNameRawContactId;
67     private final int mDisplayNameSource;
68     private final long mPhotoId;
69     private final String mPhotoUri;
70     private final String mDisplayName;
71     private final String mAltDisplayName;
72     private final String mPhoneticName;
73     private final boolean mStarred;
74     private final Integer mPresence;
75     private ImmutableList<RawContact> mRawContacts;
76     private ImmutableMap<Long,DataStatus> mStatuses;
77 
78     private String mDirectoryDisplayName;
79     private String mDirectoryType;
80     private String mDirectoryAccountType;
81     private String mDirectoryAccountName;
82     private int mDirectoryExportSupport;
83 
84     private ImmutableList<GroupMetaData> mGroups;
85 
86     private byte[] mPhotoBinaryData;
87     /**
88      * Small version of the contact photo loaded from a blob instead of from a file. If a large
89      * contact photo is not available yet, then this has the same value as mPhotoBinaryData.
90      */
91     private byte[] mThumbnailPhotoBinaryData;
92     private final boolean mSendToVoicemail;
93     private final String mCustomRingtone;
94     private final boolean mIsUserProfile;
95 
96     private final Contact.Status mStatus;
97     private final Exception mException;
98 
99     /**
100      * Constructor for special results, namely "no contact found" and "error".
101      */
Contact(Uri requestedUri, Contact.Status status, Exception exception)102     private Contact(Uri requestedUri, Contact.Status status, Exception exception) {
103         if (status == Status.ERROR && exception == null) {
104             throw new IllegalArgumentException("ERROR result must have exception");
105         }
106         mStatus = status;
107         mException = exception;
108         mRequestedUri = requestedUri;
109         mLookupUri = null;
110         mUri = null;
111         mDirectoryId = -1;
112         mLookupKey = null;
113         mId = -1;
114         mRawContacts = null;
115         mStatuses = null;
116         mNameRawContactId = -1;
117         mDisplayNameSource = DisplayNameSources.UNDEFINED;
118         mPhotoId = -1;
119         mPhotoUri = null;
120         mDisplayName = null;
121         mAltDisplayName = null;
122         mPhoneticName = null;
123         mStarred = false;
124         mPresence = null;
125         mSendToVoicemail = false;
126         mCustomRingtone = null;
127         mIsUserProfile = false;
128     }
129 
forError(Uri requestedUri, Exception exception)130     public static Contact forError(Uri requestedUri, Exception exception) {
131         return new Contact(requestedUri, Status.ERROR, exception);
132     }
133 
forNotFound(Uri requestedUri)134     public static Contact forNotFound(Uri requestedUri) {
135         return new Contact(requestedUri, Status.NOT_FOUND, null);
136     }
137 
138     /**
139      * Constructor to call when contact was found
140      */
Contact(Uri requestedUri, Uri uri, Uri lookupUri, long directoryId, String lookupKey, long id, long nameRawContactId, int displayNameSource, long photoId, String photoUri, String displayName, String altDisplayName, String phoneticName, boolean starred, Integer presence, boolean sendToVoicemail, String customRingtone, boolean isUserProfile)141     public Contact(Uri requestedUri, Uri uri, Uri lookupUri, long directoryId, String lookupKey,
142             long id, long nameRawContactId, int displayNameSource, long photoId,
143             String photoUri, String displayName, String altDisplayName, String phoneticName,
144             boolean starred, Integer presence, boolean sendToVoicemail, String customRingtone,
145             boolean isUserProfile) {
146         mStatus = Status.LOADED;
147         mException = null;
148         mRequestedUri = requestedUri;
149         mLookupUri = lookupUri;
150         mUri = uri;
151         mDirectoryId = directoryId;
152         mLookupKey = lookupKey;
153         mId = id;
154         mRawContacts = null;
155         mStatuses = null;
156         mNameRawContactId = nameRawContactId;
157         mDisplayNameSource = displayNameSource;
158         mPhotoId = photoId;
159         mPhotoUri = photoUri;
160         mDisplayName = displayName;
161         mAltDisplayName = altDisplayName;
162         mPhoneticName = phoneticName;
163         mStarred = starred;
164         mPresence = presence;
165         mSendToVoicemail = sendToVoicemail;
166         mCustomRingtone = customRingtone;
167         mIsUserProfile = isUserProfile;
168     }
169 
Contact(Uri requestedUri, Contact from)170     public Contact(Uri requestedUri, Contact from) {
171         mRequestedUri = requestedUri;
172 
173         mStatus = from.mStatus;
174         mException = from.mException;
175         mLookupUri = from.mLookupUri;
176         mUri = from.mUri;
177         mDirectoryId = from.mDirectoryId;
178         mLookupKey = from.mLookupKey;
179         mId = from.mId;
180         mNameRawContactId = from.mNameRawContactId;
181         mDisplayNameSource = from.mDisplayNameSource;
182         mPhotoId = from.mPhotoId;
183         mPhotoUri = from.mPhotoUri;
184         mDisplayName = from.mDisplayName;
185         mAltDisplayName = from.mAltDisplayName;
186         mPhoneticName = from.mPhoneticName;
187         mStarred = from.mStarred;
188         mPresence = from.mPresence;
189         mRawContacts = from.mRawContacts;
190         mStatuses = from.mStatuses;
191 
192         mDirectoryDisplayName = from.mDirectoryDisplayName;
193         mDirectoryType = from.mDirectoryType;
194         mDirectoryAccountType = from.mDirectoryAccountType;
195         mDirectoryAccountName = from.mDirectoryAccountName;
196         mDirectoryExportSupport = from.mDirectoryExportSupport;
197 
198         mGroups = from.mGroups;
199 
200         mPhotoBinaryData = from.mPhotoBinaryData;
201         mSendToVoicemail = from.mSendToVoicemail;
202         mCustomRingtone = from.mCustomRingtone;
203         mIsUserProfile = from.mIsUserProfile;
204     }
205 
206     /**
207      * @param exportSupport See {@link Directory#EXPORT_SUPPORT}.
208      */
setDirectoryMetaData(String displayName, String directoryType, String accountType, String accountName, int exportSupport)209     public void setDirectoryMetaData(String displayName, String directoryType,
210             String accountType, String accountName, int exportSupport) {
211         mDirectoryDisplayName = displayName;
212         mDirectoryType = directoryType;
213         mDirectoryAccountType = accountType;
214         mDirectoryAccountName = accountName;
215         mDirectoryExportSupport = exportSupport;
216     }
217 
setPhotoBinaryData(byte[] photoBinaryData)218     /* package */ void setPhotoBinaryData(byte[] photoBinaryData) {
219         mPhotoBinaryData = photoBinaryData;
220     }
221 
setThumbnailPhotoBinaryData(byte[] photoBinaryData)222     /* package */ void setThumbnailPhotoBinaryData(byte[] photoBinaryData) {
223         mThumbnailPhotoBinaryData = photoBinaryData;
224     }
225 
226     /**
227      * Returns the URI for the contact that contains both the lookup key and the ID. This is
228      * the best URI to reference a contact.
229      * For directory contacts, this is the same a the URI as returned by {@link #getUri()}
230      */
getLookupUri()231     public Uri getLookupUri() {
232         return mLookupUri;
233     }
234 
getLookupKey()235     public String getLookupKey() {
236         return mLookupKey;
237     }
238 
239     /**
240      * Returns the contact Uri that was passed to the provider to make the query. This is
241      * the same as the requested Uri, unless the requested Uri doesn't specify a Contact:
242      * If it either references a Raw-Contact or a Person (a pre-Eclair style Uri), this Uri will
243      * always reference the full aggregate contact.
244      */
getUri()245     public Uri getUri() {
246         return mUri;
247     }
248 
249     /**
250      * Returns the URI for which this {@link ContactLoader) was initially requested.
251      */
getRequestedUri()252     public Uri getRequestedUri() {
253         return mRequestedUri;
254     }
255 
256     /**
257      * Instantiate a new RawContactDeltaList for this contact.
258      */
createRawContactDeltaList()259     public RawContactDeltaList createRawContactDeltaList() {
260         return RawContactDeltaList.fromIterator(getRawContacts().iterator());
261     }
262 
263     /**
264      * Returns the contact ID.
265      */
266     @VisibleForTesting
getId()267     public long getId() {
268         return mId;
269     }
270 
271     /**
272      * @return true when an exception happened during loading, in which case
273      *     {@link #getException} returns the actual exception object.
274      *     Note {@link #isNotFound()} and {@link #isError()} are mutually exclusive; If
275      *     {@link #isError()} is {@code true}, {@link #isNotFound()} is always {@code false},
276      *     and vice versa.
277      */
isError()278     public boolean isError() {
279         return mStatus == Status.ERROR;
280     }
281 
getException()282     public Exception getException() {
283         return mException;
284     }
285 
286     /**
287      * @return true when the specified contact is not found.
288      *     Note {@link #isNotFound()} and {@link #isError()} are mutually exclusive; If
289      *     {@link #isError()} is {@code true}, {@link #isNotFound()} is always {@code false},
290      *     and vice versa.
291      */
isNotFound()292     public boolean isNotFound() {
293         return mStatus == Status.NOT_FOUND;
294     }
295 
296     /**
297      * @return true if the specified contact is successfully loaded.
298      *     i.e. neither {@link #isError()} nor {@link #isNotFound()}.
299      */
isLoaded()300     public boolean isLoaded() {
301         return mStatus == Status.LOADED;
302     }
303 
getNameRawContactId()304     public long getNameRawContactId() {
305         return mNameRawContactId;
306     }
307 
getDisplayNameSource()308     public int getDisplayNameSource() {
309         return mDisplayNameSource;
310     }
311 
312     /**
313      * Used by various classes to determine whether or not this contact should be displayed as
314      * a business rather than a person.
315      */
isDisplayNameFromOrganization()316     public boolean isDisplayNameFromOrganization() {
317         return DisplayNameSources.ORGANIZATION == mDisplayNameSource;
318     }
319 
getPhotoId()320     public long getPhotoId() {
321         return mPhotoId;
322     }
323 
getPhotoUri()324     public String getPhotoUri() {
325         return mPhotoUri;
326     }
327 
getDisplayName()328     public String getDisplayName() {
329         return mDisplayName;
330     }
331 
getAltDisplayName()332     public String getAltDisplayName() {
333         return mAltDisplayName;
334     }
335 
getPhoneticName()336     public String getPhoneticName() {
337         return mPhoneticName;
338     }
339 
getStarred()340     public boolean getStarred() {
341         return mStarred;
342     }
343 
getPresence()344     public Integer getPresence() {
345         return mPresence;
346     }
347 
getRawContacts()348     public ImmutableList<RawContact> getRawContacts() {
349         return mRawContacts;
350     }
351 
getStatuses()352     public ImmutableMap<Long, DataStatus> getStatuses() {
353         return mStatuses;
354     }
355 
getDirectoryId()356     public long getDirectoryId() {
357         return mDirectoryId;
358     }
359 
isDirectoryEntry()360     public boolean isDirectoryEntry() {
361         return mDirectoryId != -1 && mDirectoryId != Directory.DEFAULT
362                 && mDirectoryId != Directory.LOCAL_INVISIBLE;
363     }
364 
365     /**
366      * @return true if this is a contact (not group, etc.) with at least one
367      *         writable raw-contact, and false otherwise.
368      */
isWritableContact(final Context context)369     public boolean isWritableContact(final Context context) {
370         return getFirstWritableRawContactId(context) != -1;
371     }
372 
373     /**
374      * Return the ID of the first raw-contact in the contact data that belongs to a
375      * contact-writable account, or -1 if no such entity exists.
376      */
getFirstWritableRawContactId(final Context context)377     public long getFirstWritableRawContactId(final Context context) {
378         // Directory entries are non-writable
379         if (isDirectoryEntry()) return -1;
380 
381         // Iterate through raw-contacts; if we find a writable on, return its ID.
382         for (RawContact rawContact : getRawContacts()) {
383             AccountType accountType = rawContact.getAccountType(context);
384             if (accountType != null && accountType.areContactsWritable()) {
385                 return rawContact.getId();
386             }
387         }
388         // No writable raw-contact was found.
389         return -1;
390     }
391 
getDirectoryExportSupport()392     public int getDirectoryExportSupport() {
393         return mDirectoryExportSupport;
394     }
395 
getDirectoryDisplayName()396     public String getDirectoryDisplayName() {
397         return mDirectoryDisplayName;
398     }
399 
getDirectoryType()400     public String getDirectoryType() {
401         return mDirectoryType;
402     }
403 
getDirectoryAccountType()404     public String getDirectoryAccountType() {
405         return mDirectoryAccountType;
406     }
407 
getDirectoryAccountName()408     public String getDirectoryAccountName() {
409         return mDirectoryAccountName;
410     }
411 
getPhotoBinaryData()412     public byte[] getPhotoBinaryData() {
413         return mPhotoBinaryData;
414     }
415 
getThumbnailPhotoBinaryData()416     public byte[] getThumbnailPhotoBinaryData() {
417         return mThumbnailPhotoBinaryData;
418     }
419 
getContentValues()420     public ArrayList<ContentValues> getContentValues() {
421         if (mRawContacts.size() != 1) {
422             throw new IllegalStateException(
423                     "Cannot extract content values from an aggregated contact");
424         }
425 
426         RawContact rawContact = mRawContacts.get(0);
427         ArrayList<ContentValues> result = rawContact.getContentValues();
428 
429         // If the photo was loaded using the URI, create an entry for the photo
430         // binary data.
431         if (mPhotoId == 0 && mPhotoBinaryData != null) {
432             ContentValues photo = new ContentValues();
433             photo.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
434             photo.put(Photo.PHOTO, mPhotoBinaryData);
435             result.add(photo);
436         }
437 
438         return result;
439     }
440 
441     /**
442      * This can return non-null group meta-data only if the {@link ContactLoader} was configured to
443      * load group metadata in its constructor.
444      * @return
445      */
getGroupMetaData()446     public ImmutableList<GroupMetaData> getGroupMetaData() {
447         return mGroups;
448     }
449 
isSendToVoicemail()450     public boolean isSendToVoicemail() {
451         return mSendToVoicemail;
452     }
453 
getCustomRingtone()454     public String getCustomRingtone() {
455         return mCustomRingtone;
456     }
457 
isUserProfile()458     public boolean isUserProfile() {
459         return mIsUserProfile;
460     }
461 
isMultipleRawContacts()462     public boolean isMultipleRawContacts() {
463         return mRawContacts.size() > 1;
464     }
465 
466     /**
467      * @return true if all the raw contacts are from SIM accounts, and false otherwise.
468      */
areAllRawContactsSimAccounts(final Context context)469     public boolean areAllRawContactsSimAccounts(final Context context) {
470         if (getRawContacts() == null) return false;
471 
472         for (RawContact rawContact : getRawContacts()) {
473             final AccountType accountType = rawContact.getAccountType(context);
474             if (!(accountType instanceof SimAccountType)) return false;
475         }
476         return true;
477     }
478 
479     @Override
toString()480     public String toString() {
481         return "{requested=" + mRequestedUri + ",lookupkey=" + mLookupKey +
482                 ",uri=" + mUri + ",status=" + mStatus + "}";
483     }
484 
setRawContacts(ImmutableList<RawContact> rawContacts)485     /* package */ void setRawContacts(ImmutableList<RawContact> rawContacts) {
486         mRawContacts = rawContacts;
487     }
488 
setStatuses(ImmutableMap<Long, DataStatus> statuses)489     /* package */ void setStatuses(ImmutableMap<Long, DataStatus> statuses) {
490         mStatuses = statuses;
491     }
492 
setGroupMetaData(ImmutableList<GroupMetaData> groups)493     /* package */ void setGroupMetaData(ImmutableList<GroupMetaData> groups) {
494         mGroups = groups;
495     }
496 }
497