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
17package com.android.contacts.common.model;
18
19import android.content.ContentValues;
20import android.content.Context;
21import android.net.Uri;
22import android.provider.ContactsContract.CommonDataKinds.Photo;
23import android.provider.ContactsContract.Data;
24import android.provider.ContactsContract.Directory;
25import android.provider.ContactsContract.DisplayNameSources;
26
27import com.android.contacts.common.GroupMetaData;
28import com.android.contacts.common.model.account.AccountType;
29import com.android.contacts.common.util.DataStatus;
30
31import com.google.common.annotations.VisibleForTesting;
32import com.google.common.collect.ImmutableList;
33import com.google.common.collect.ImmutableMap;
34
35import java.util.ArrayList;
36import 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 */
50public 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     */
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
132    public static Contact forError(Uri requestedUri, Exception exception) {
133        return new Contact(requestedUri, Status.ERROR, exception);
134    }
135
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     */
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
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     */
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
222    /* package */ void setPhotoBinaryData(byte[] photoBinaryData) {
223        mPhotoBinaryData = photoBinaryData;
224    }
225
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     */
235    public Uri getLookupUri() {
236        return mLookupUri;
237    }
238
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     */
249    public Uri getUri() {
250        return mUri;
251    }
252
253    /**
254     * Returns the URI for which this {@link ContactLoader) was initially requested.
255     */
256    public Uri getRequestedUri() {
257        return mRequestedUri;
258    }
259
260    /**
261     * Instantiate a new RawContactDeltaList for this contact.
262     */
263    public RawContactDeltaList createRawContactDeltaList() {
264        return RawContactDeltaList.fromIterator(getRawContacts().iterator());
265    }
266
267    /**
268     * Returns the contact ID.
269     */
270    @VisibleForTesting
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     */
282    public boolean isError() {
283        return mStatus == Status.ERROR;
284    }
285
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     */
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     */
304    public boolean isLoaded() {
305        return mStatus == Status.LOADED;
306    }
307
308    public long getNameRawContactId() {
309        return mNameRawContactId;
310    }
311
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     */
320    public boolean isDisplayNameFromOrganization() {
321        return DisplayNameSources.ORGANIZATION == mDisplayNameSource;
322    }
323
324    public long getPhotoId() {
325        return mPhotoId;
326    }
327
328    public String getPhotoUri() {
329        return mPhotoUri;
330    }
331
332    public String getDisplayName() {
333        return mDisplayName;
334    }
335
336    public String getAltDisplayName() {
337        return mAltDisplayName;
338    }
339
340    public String getPhoneticName() {
341        return mPhoneticName;
342    }
343
344    public boolean getStarred() {
345        return mStarred;
346    }
347
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     */
357    public ImmutableList<AccountType> getInvitableAccountTypes() {
358        return mInvitableAccountTypes;
359    }
360
361    public ImmutableList<RawContact> getRawContacts() {
362        return mRawContacts;
363    }
364
365    public ImmutableMap<Long, DataStatus> getStatuses() {
366        return mStatuses;
367    }
368
369    public long getDirectoryId() {
370        return mDirectoryId;
371    }
372
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     */
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     */
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
405    public int getDirectoryExportSupport() {
406        return mDirectoryExportSupport;
407    }
408
409    public String getDirectoryDisplayName() {
410        return mDirectoryDisplayName;
411    }
412
413    public String getDirectoryType() {
414        return mDirectoryType;
415    }
416
417    public String getDirectoryAccountType() {
418        return mDirectoryAccountType;
419    }
420
421    public String getDirectoryAccountName() {
422        return mDirectoryAccountName;
423    }
424
425    public byte[] getPhotoBinaryData() {
426        return mPhotoBinaryData;
427    }
428
429    public byte[] getThumbnailPhotoBinaryData() {
430        return mThumbnailPhotoBinaryData;
431    }
432
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     */
459    public ImmutableList<GroupMetaData> getGroupMetaData() {
460        return mGroups;
461    }
462
463    public boolean isSendToVoicemail() {
464        return mSendToVoicemail;
465    }
466
467    public String getCustomRingtone() {
468        return mCustomRingtone;
469    }
470
471    public boolean isUserProfile() {
472        return mIsUserProfile;
473    }
474
475    @Override
476    public String toString() {
477        return "{requested=" + mRequestedUri + ",lookupkey=" + mLookupKey +
478                ",uri=" + mUri + ",status=" + mStatus + "}";
479    }
480
481    /* package */ void setRawContacts(ImmutableList<RawContact> rawContacts) {
482        mRawContacts = rawContacts;
483    }
484
485    /* package */ void setStatuses(ImmutableMap<Long, DataStatus> statuses) {
486        mStatuses = statuses;
487    }
488
489    /* package */ void setInvitableAccountTypes(ImmutableList<AccountType> accountTypes) {
490        mInvitableAccountTypes = accountTypes;
491    }
492
493    /* package */ void setGroupMetaData(ImmutableList<GroupMetaData> groups) {
494        mGroups = groups;
495    }
496}
497