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