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