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.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.GroupMetaData; 28 import com.android.contacts.common.model.account.AccountType; 29 import com.android.contacts.util.DataStatus; 30 import com.android.contacts.util.StreamItemEntry; 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 public long getPhotoId() { 308 return mPhotoId; 309 } 310 311 public String getPhotoUri() { 312 return mPhotoUri; 313 } 314 315 public String getDisplayName() { 316 return mDisplayName; 317 } 318 319 public String getAltDisplayName() { 320 return mAltDisplayName; 321 } 322 323 public String getPhoneticName() { 324 return mPhoneticName; 325 } 326 327 public boolean getStarred() { 328 return mStarred; 329 } 330 331 public Integer getPresence() { 332 return mPresence; 333 } 334 335 /** 336 * This can return non-null invitable account types only if the {@link ContactLoader} was 337 * configured to load invitable account types in its constructor. 338 * @return 339 */ 340 public ImmutableList<AccountType> getInvitableAccountTypes() { 341 return mInvitableAccountTypes; 342 } 343 344 public ImmutableList<RawContact> getRawContacts() { 345 return mRawContacts; 346 } 347 348 /** 349 * This can return non-null stream items only if the {@link ContactLoader} was 350 * configured to load stream items in its constructor. 351 * @return 352 */ 353 public ImmutableList<StreamItemEntry> getStreamItems() { 354 return ImmutableList.of(); 355 } 356 357 public ImmutableMap<Long, DataStatus> getStatuses() { 358 return mStatuses; 359 } 360 361 public long getDirectoryId() { 362 return mDirectoryId; 363 } 364 365 public boolean isDirectoryEntry() { 366 return mDirectoryId != -1 && mDirectoryId != Directory.DEFAULT 367 && mDirectoryId != Directory.LOCAL_INVISIBLE; 368 } 369 370 /** 371 * @return true if this is a contact (not group, etc.) with at least one 372 * writable raw-contact, and false otherwise. 373 */ 374 public boolean isWritableContact(final Context context) { 375 return getFirstWritableRawContactId(context) != -1; 376 } 377 378 /** 379 * Return the ID of the first raw-contact in the contact data that belongs to a 380 * contact-writable account, or -1 if no such entity exists. 381 */ 382 public long getFirstWritableRawContactId(final Context context) { 383 // Directory entries are non-writable 384 if (isDirectoryEntry()) return -1; 385 386 // Iterate through raw-contacts; if we find a writable on, return its ID. 387 for (RawContact rawContact : getRawContacts()) { 388 AccountType accountType = rawContact.getAccountType(context); 389 if (accountType != null && accountType.areContactsWritable()) { 390 return rawContact.getId(); 391 } 392 } 393 // No writable raw-contact was found. 394 return -1; 395 } 396 397 public int getDirectoryExportSupport() { 398 return mDirectoryExportSupport; 399 } 400 401 public String getDirectoryDisplayName() { 402 return mDirectoryDisplayName; 403 } 404 405 public String getDirectoryType() { 406 return mDirectoryType; 407 } 408 409 public String getDirectoryAccountType() { 410 return mDirectoryAccountType; 411 } 412 413 public String getDirectoryAccountName() { 414 return mDirectoryAccountName; 415 } 416 417 public byte[] getPhotoBinaryData() { 418 return mPhotoBinaryData; 419 } 420 421 public ArrayList<ContentValues> getContentValues() { 422 if (mRawContacts.size() != 1) { 423 throw new IllegalStateException( 424 "Cannot extract content values from an aggregated contact"); 425 } 426 427 RawContact rawContact = mRawContacts.get(0); 428 ArrayList<ContentValues> result = rawContact.getContentValues(); 429 430 // If the photo was loaded using the URI, create an entry for the photo 431 // binary data. 432 if (mPhotoId == 0 && mPhotoBinaryData != null) { 433 ContentValues photo = new ContentValues(); 434 photo.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE); 435 photo.put(Photo.PHOTO, mPhotoBinaryData); 436 result.add(photo); 437 } 438 439 return result; 440 } 441 442 /** 443 * This can return non-null group meta-data only if the {@link ContactLoader} was configured to 444 * load group metadata in its constructor. 445 * @return 446 */ 447 public ImmutableList<GroupMetaData> getGroupMetaData() { 448 return mGroups; 449 } 450 451 public boolean isSendToVoicemail() { 452 return mSendToVoicemail; 453 } 454 455 public String getCustomRingtone() { 456 return mCustomRingtone; 457 } 458 459 public boolean isUserProfile() { 460 return mIsUserProfile; 461 } 462 463 @Override 464 public String toString() { 465 return "{requested=" + mRequestedUri + ",lookupkey=" + mLookupKey + 466 ",uri=" + mUri + ",status=" + mStatus + "}"; 467 } 468 469 /* package */ void setRawContacts(ImmutableList<RawContact> rawContacts) { 470 mRawContacts = rawContacts; 471 } 472 473 /* package */ void setStatuses(ImmutableMap<Long, DataStatus> statuses) { 474 mStatuses = statuses; 475 } 476 477 /* package */ void setInvitableAccountTypes(ImmutableList<AccountType> accountTypes) { 478 mInvitableAccountTypes = accountTypes; 479 } 480 481 /* package */ void setGroupMetaData(ImmutableList<GroupMetaData> groups) { 482 mGroups = groups; 483 } 484 } 485