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