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 */ 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