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