1 /* 2 * Copyright (C) 2009 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.ContentProviderOperation; 20 import android.content.ContentProviderOperation.Builder; 21 import android.content.ContentValues; 22 import android.content.Context; 23 import android.net.Uri; 24 import android.os.Parcel; 25 import android.os.Parcelable; 26 import android.provider.BaseColumns; 27 import android.provider.ContactsContract.Data; 28 import android.provider.ContactsContract.Profile; 29 import android.provider.ContactsContract.RawContacts; 30 import android.util.Log; 31 32 import com.android.contacts.common.model.AccountTypeManager; 33 import com.android.contacts.common.model.ValuesDelta; 34 import com.android.contacts.common.model.account.AccountType; 35 import com.android.contacts.common.testing.NeededForTesting; 36 import com.google.common.collect.Lists; 37 import com.google.common.collect.Maps; 38 39 import java.util.ArrayList; 40 import java.util.HashMap; 41 42 /** 43 * Contains a {@link RawContact} and records any modifications separately so the 44 * original {@link RawContact} can be swapped out with a newer version and the 45 * changes still cleanly applied. 46 * <p> 47 * One benefit of this approach is that we can build changes entirely on an 48 * empty {@link RawContact}, which then becomes an insert {@link RawContacts} case. 49 * <p> 50 * When applying modifications over an {@link RawContact}, we try finding the 51 * original {@link Data#_ID} rows where the modifications took place. If those 52 * rows are missing from the new {@link RawContact}, we know the original data must 53 * be deleted, but to preserve the user modifications we treat as an insert. 54 */ 55 public class RawContactDelta implements Parcelable { 56 // TODO: optimize by using contentvalues pool, since we allocate so many of them 57 58 private static final String TAG = "EntityDelta"; 59 private static final boolean LOGV = false; 60 61 /** 62 * Direct values from {@link Entity#getEntityValues()}. 63 */ 64 private ValuesDelta mValues; 65 66 /** 67 * URI used for contacts queries, by default it is set to query raw contacts. 68 * It can be set to query the profile's raw contact(s). 69 */ 70 private Uri mContactsQueryUri = RawContacts.CONTENT_URI; 71 72 /** 73 * Internal map of children values from {@link Entity#getSubValues()}, which 74 * we store here sorted into {@link Data#MIMETYPE} bins. 75 */ 76 private final HashMap<String, ArrayList<ValuesDelta>> mEntries = Maps.newHashMap(); 77 RawContactDelta()78 public RawContactDelta() { 79 } 80 RawContactDelta(ValuesDelta values)81 public RawContactDelta(ValuesDelta values) { 82 mValues = values; 83 } 84 85 /** 86 * Build an {@link RawContactDelta} using the given {@link RawContact} as a 87 * starting point; the "before" snapshot. 88 */ fromBefore(RawContact before)89 public static RawContactDelta fromBefore(RawContact before) { 90 final RawContactDelta rawContactDelta = new RawContactDelta(); 91 rawContactDelta.mValues = ValuesDelta.fromBefore(before.getValues()); 92 rawContactDelta.mValues.setIdColumn(RawContacts._ID); 93 for (final ContentValues values : before.getContentValues()) { 94 rawContactDelta.addEntry(ValuesDelta.fromBefore(values)); 95 } 96 return rawContactDelta; 97 } 98 99 /** 100 * Merge the "after" values from the given {@link RawContactDelta} onto the 101 * "before" state represented by this {@link RawContactDelta}, discarding any 102 * existing "after" states. This is typically used when re-parenting changes 103 * onto an updated {@link Entity}. 104 */ mergeAfter(RawContactDelta local, RawContactDelta remote)105 public static RawContactDelta mergeAfter(RawContactDelta local, RawContactDelta remote) { 106 // Bail early if trying to merge delete with missing local 107 final ValuesDelta remoteValues = remote.mValues; 108 if (local == null && (remoteValues.isDelete() || remoteValues.isTransient())) return null; 109 110 // Create local version if none exists yet 111 if (local == null) local = new RawContactDelta(); 112 113 if (LOGV) { 114 final Long localVersion = (local.mValues == null) ? null : local.mValues 115 .getAsLong(RawContacts.VERSION); 116 final Long remoteVersion = remote.mValues.getAsLong(RawContacts.VERSION); 117 Log.d(TAG, "Re-parenting from original version " + remoteVersion + " to " 118 + localVersion); 119 } 120 121 // Create values if needed, and merge "after" changes 122 local.mValues = ValuesDelta.mergeAfter(local.mValues, remote.mValues); 123 124 // Find matching local entry for each remote values, or create 125 for (ArrayList<ValuesDelta> mimeEntries : remote.mEntries.values()) { 126 for (ValuesDelta remoteEntry : mimeEntries) { 127 final Long childId = remoteEntry.getId(); 128 129 // Find or create local match and merge 130 final ValuesDelta localEntry = local.getEntry(childId); 131 final ValuesDelta merged = ValuesDelta.mergeAfter(localEntry, remoteEntry); 132 133 if (localEntry == null && merged != null) { 134 // No local entry before, so insert 135 local.addEntry(merged); 136 } 137 } 138 } 139 140 return local; 141 } 142 getValues()143 public ValuesDelta getValues() { 144 return mValues; 145 } 146 isContactInsert()147 public boolean isContactInsert() { 148 return mValues.isInsert(); 149 } 150 151 /** 152 * Get the {@link ValuesDelta} child marked as {@link Data#IS_PRIMARY}, 153 * which may return null when no entry exists. 154 */ getPrimaryEntry(String mimeType)155 public ValuesDelta getPrimaryEntry(String mimeType) { 156 final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false); 157 if (mimeEntries == null) return null; 158 159 for (ValuesDelta entry : mimeEntries) { 160 if (entry.isPrimary()) { 161 return entry; 162 } 163 } 164 165 // When no direct primary, return something 166 return mimeEntries.size() > 0 ? mimeEntries.get(0) : null; 167 } 168 169 /** 170 * calls {@link #getSuperPrimaryEntry(String, boolean)} with true 171 * @see #getSuperPrimaryEntry(String, boolean) 172 */ getSuperPrimaryEntry(String mimeType)173 public ValuesDelta getSuperPrimaryEntry(String mimeType) { 174 return getSuperPrimaryEntry(mimeType, true); 175 } 176 177 /** 178 * Returns the super-primary entry for the given mime type 179 * @param forceSelection if true, will try to return some value even if a super-primary 180 * doesn't exist (may be a primary, or just a random item 181 * @return 182 */ 183 @NeededForTesting getSuperPrimaryEntry(String mimeType, boolean forceSelection)184 public ValuesDelta getSuperPrimaryEntry(String mimeType, boolean forceSelection) { 185 final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false); 186 if (mimeEntries == null) return null; 187 188 ValuesDelta primary = null; 189 for (ValuesDelta entry : mimeEntries) { 190 if (entry.isSuperPrimary()) { 191 return entry; 192 } else if (entry.isPrimary()) { 193 primary = entry; 194 } 195 } 196 197 if (!forceSelection) { 198 return null; 199 } 200 201 // When no direct super primary, return something 202 if (primary != null) { 203 return primary; 204 } 205 return mimeEntries.size() > 0 ? mimeEntries.get(0) : null; 206 } 207 208 /** 209 * Return the AccountType that this raw-contact belongs to. 210 */ getRawContactAccountType(Context context)211 public AccountType getRawContactAccountType(Context context) { 212 ContentValues entityValues = getValues().getCompleteValues(); 213 String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE); 214 String dataSet = entityValues.getAsString(RawContacts.DATA_SET); 215 return AccountTypeManager.getInstance(context).getAccountType(type, dataSet); 216 } 217 getRawContactId()218 public Long getRawContactId() { 219 return getValues().getAsLong(RawContacts._ID); 220 } 221 getAccountName()222 public String getAccountName() { 223 return getValues().getAsString(RawContacts.ACCOUNT_NAME); 224 } 225 getAccountType()226 public String getAccountType() { 227 return getValues().getAsString(RawContacts.ACCOUNT_TYPE); 228 } 229 getDataSet()230 public String getDataSet() { 231 return getValues().getAsString(RawContacts.DATA_SET); 232 } 233 getAccountType(AccountTypeManager manager)234 public AccountType getAccountType(AccountTypeManager manager) { 235 return manager.getAccountType(getAccountType(), getDataSet()); 236 } 237 isVisible()238 public boolean isVisible() { 239 return getValues().isVisible(); 240 } 241 242 /** 243 * Return the list of child {@link ValuesDelta} from our optimized map, 244 * creating the list if requested. 245 */ getMimeEntries(String mimeType, boolean lazyCreate)246 private ArrayList<ValuesDelta> getMimeEntries(String mimeType, boolean lazyCreate) { 247 ArrayList<ValuesDelta> mimeEntries = mEntries.get(mimeType); 248 if (mimeEntries == null && lazyCreate) { 249 mimeEntries = Lists.newArrayList(); 250 mEntries.put(mimeType, mimeEntries); 251 } 252 return mimeEntries; 253 } 254 getMimeEntries(String mimeType)255 public ArrayList<ValuesDelta> getMimeEntries(String mimeType) { 256 return getMimeEntries(mimeType, false); 257 } 258 getMimeEntriesCount(String mimeType, boolean onlyVisible)259 public int getMimeEntriesCount(String mimeType, boolean onlyVisible) { 260 final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType); 261 if (mimeEntries == null) return 0; 262 263 int count = 0; 264 for (ValuesDelta child : mimeEntries) { 265 // Skip deleted items when requesting only visible 266 if (onlyVisible && !child.isVisible()) continue; 267 count++; 268 } 269 return count; 270 } 271 hasMimeEntries(String mimeType)272 public boolean hasMimeEntries(String mimeType) { 273 return mEntries.containsKey(mimeType); 274 } 275 addEntry(ValuesDelta entry)276 public ValuesDelta addEntry(ValuesDelta entry) { 277 final String mimeType = entry.getMimetype(); 278 getMimeEntries(mimeType, true).add(entry); 279 return entry; 280 } 281 getContentValues()282 public ArrayList<ContentValues> getContentValues() { 283 ArrayList<ContentValues> values = Lists.newArrayList(); 284 for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) { 285 for (ValuesDelta entry : mimeEntries) { 286 if (!entry.isDelete()) { 287 values.add(entry.getCompleteValues()); 288 } 289 } 290 } 291 return values; 292 } 293 294 /** 295 * Find entry with the given {@link BaseColumns#_ID} value. 296 */ getEntry(Long childId)297 public ValuesDelta getEntry(Long childId) { 298 if (childId == null) { 299 // Requesting an "insert" entry, which has no "before" 300 return null; 301 } 302 303 // Search all children for requested entry 304 for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) { 305 for (ValuesDelta entry : mimeEntries) { 306 if (childId.equals(entry.getId())) { 307 return entry; 308 } 309 } 310 } 311 return null; 312 } 313 314 /** 315 * Return the total number of {@link ValuesDelta} contained. 316 */ getEntryCount(boolean onlyVisible)317 public int getEntryCount(boolean onlyVisible) { 318 int count = 0; 319 for (String mimeType : mEntries.keySet()) { 320 count += getMimeEntriesCount(mimeType, onlyVisible); 321 } 322 return count; 323 } 324 325 @Override equals(Object object)326 public boolean equals(Object object) { 327 if (object instanceof RawContactDelta) { 328 final RawContactDelta other = (RawContactDelta)object; 329 330 // Equality failed if parent values different 331 if (!other.mValues.equals(mValues)) return false; 332 333 for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) { 334 for (ValuesDelta child : mimeEntries) { 335 // Equality failed if any children unmatched 336 if (!other.containsEntry(child)) return false; 337 } 338 } 339 340 // Passed all tests, so equal 341 return true; 342 } 343 return false; 344 } 345 containsEntry(ValuesDelta entry)346 private boolean containsEntry(ValuesDelta entry) { 347 for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) { 348 for (ValuesDelta child : mimeEntries) { 349 // Contained if we find any child that matches 350 if (child.equals(entry)) return true; 351 } 352 } 353 return false; 354 } 355 356 /** 357 * Mark this entire object deleted, including any {@link ValuesDelta}. 358 */ markDeleted()359 public void markDeleted() { 360 this.mValues.markDeleted(); 361 for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) { 362 for (ValuesDelta child : mimeEntries) { 363 child.markDeleted(); 364 } 365 } 366 } 367 368 @Override toString()369 public String toString() { 370 final StringBuilder builder = new StringBuilder(); 371 builder.append("\n("); 372 builder.append("Uri="); 373 builder.append(mContactsQueryUri); 374 builder.append(", Values="); 375 builder.append(mValues != null ? mValues.toString() : "null"); 376 builder.append(", Entries={"); 377 for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) { 378 for (ValuesDelta child : mimeEntries) { 379 builder.append("\n\t"); 380 child.toString(builder); 381 } 382 } 383 builder.append("\n})\n"); 384 return builder.toString(); 385 } 386 387 /** 388 * Consider building the given {@link ContentProviderOperation.Builder} and 389 * appending it to the given list, which only happens if builder is valid. 390 */ possibleAdd(ArrayList<ContentProviderOperation> diff, ContentProviderOperation.Builder builder)391 private void possibleAdd(ArrayList<ContentProviderOperation> diff, 392 ContentProviderOperation.Builder builder) { 393 if (builder != null) { 394 diff.add(builder.build()); 395 } 396 } 397 398 /** 399 * Build a list of {@link ContentProviderOperation} that will assert any 400 * "before" state hasn't changed. This is maintained separately so that all 401 * asserts can take place before any updates occur. 402 */ buildAssert(ArrayList<ContentProviderOperation> buildInto)403 public void buildAssert(ArrayList<ContentProviderOperation> buildInto) { 404 final boolean isContactInsert = mValues.isInsert(); 405 if (!isContactInsert) { 406 // Assert version is consistent while persisting changes 407 final Long beforeId = mValues.getId(); 408 final Long beforeVersion = mValues.getAsLong(RawContacts.VERSION); 409 if (beforeId == null || beforeVersion == null) return; 410 411 final ContentProviderOperation.Builder builder = ContentProviderOperation 412 .newAssertQuery(mContactsQueryUri); 413 builder.withSelection(RawContacts._ID + "=" + beforeId, null); 414 builder.withValue(RawContacts.VERSION, beforeVersion); 415 buildInto.add(builder.build()); 416 } 417 } 418 419 /** 420 * Build a list of {@link ContentProviderOperation} that will transform the 421 * current "before" {@link Entity} state into the modified state which this 422 * {@link RawContactDelta} represents. 423 */ buildDiff(ArrayList<ContentProviderOperation> buildInto)424 public void buildDiff(ArrayList<ContentProviderOperation> buildInto) { 425 final int firstIndex = buildInto.size(); 426 427 final boolean isContactInsert = mValues.isInsert(); 428 final boolean isContactDelete = mValues.isDelete(); 429 final boolean isContactUpdate = !isContactInsert && !isContactDelete; 430 431 final Long beforeId = mValues.getId(); 432 433 Builder builder; 434 435 if (isContactInsert) { 436 // TODO: for now simply disabling aggregation when a new contact is 437 // created on the phone. In the future, will show aggregation suggestions 438 // after saving the contact. 439 mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED); 440 } 441 442 // Build possible operation at Contact level 443 builder = mValues.buildDiff(mContactsQueryUri); 444 possibleAdd(buildInto, builder); 445 446 // Build operations for all children 447 for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) { 448 for (ValuesDelta child : mimeEntries) { 449 // Ignore children if parent was deleted 450 if (isContactDelete) continue; 451 452 // Use the profile data URI if the contact is the profile. 453 if (mContactsQueryUri.equals(Profile.CONTENT_RAW_CONTACTS_URI)) { 454 builder = child.buildDiff(Uri.withAppendedPath(Profile.CONTENT_URI, 455 RawContacts.Data.CONTENT_DIRECTORY)); 456 } else { 457 builder = child.buildDiff(Data.CONTENT_URI); 458 } 459 460 if (child.isInsert()) { 461 if (isContactInsert) { 462 // Parent is brand new insert, so back-reference _id 463 builder.withValueBackReference(Data.RAW_CONTACT_ID, firstIndex); 464 } else { 465 // Inserting under existing, so fill with known _id 466 builder.withValue(Data.RAW_CONTACT_ID, beforeId); 467 } 468 } else if (isContactInsert && builder != null) { 469 // Child must be insert when Contact insert 470 throw new IllegalArgumentException("When parent insert, child must be also"); 471 } 472 possibleAdd(buildInto, builder); 473 } 474 } 475 476 final boolean addedOperations = buildInto.size() > firstIndex; 477 if (addedOperations && isContactUpdate) { 478 // Suspend aggregation while persisting updates 479 builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_SUSPENDED); 480 buildInto.add(firstIndex, builder.build()); 481 482 // Restore aggregation mode as last operation 483 builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_DEFAULT); 484 buildInto.add(builder.build()); 485 } else if (isContactInsert) { 486 // Restore aggregation mode as last operation 487 builder = ContentProviderOperation.newUpdate(mContactsQueryUri); 488 builder.withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT); 489 builder.withSelection(RawContacts._ID + "=?", new String[1]); 490 builder.withSelectionBackReference(0, firstIndex); 491 buildInto.add(builder.build()); 492 } 493 } 494 495 /** 496 * Build a {@link ContentProviderOperation} that changes 497 * {@link RawContacts#AGGREGATION_MODE} to the given value. 498 */ buildSetAggregationMode(Long beforeId, int mode)499 protected Builder buildSetAggregationMode(Long beforeId, int mode) { 500 Builder builder = ContentProviderOperation.newUpdate(mContactsQueryUri); 501 builder.withValue(RawContacts.AGGREGATION_MODE, mode); 502 builder.withSelection(RawContacts._ID + "=" + beforeId, null); 503 return builder; 504 } 505 506 /** {@inheritDoc} */ describeContents()507 public int describeContents() { 508 // Nothing special about this parcel 509 return 0; 510 } 511 512 /** {@inheritDoc} */ writeToParcel(Parcel dest, int flags)513 public void writeToParcel(Parcel dest, int flags) { 514 final int size = this.getEntryCount(false); 515 dest.writeInt(size); 516 dest.writeParcelable(mValues, flags); 517 dest.writeParcelable(mContactsQueryUri, flags); 518 for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) { 519 for (ValuesDelta child : mimeEntries) { 520 dest.writeParcelable(child, flags); 521 } 522 } 523 } 524 readFromParcel(Parcel source)525 public void readFromParcel(Parcel source) { 526 final ClassLoader loader = getClass().getClassLoader(); 527 final int size = source.readInt(); 528 mValues = source.<ValuesDelta> readParcelable(loader); 529 mContactsQueryUri = source.<Uri> readParcelable(loader); 530 for (int i = 0; i < size; i++) { 531 final ValuesDelta child = source.<ValuesDelta> readParcelable(loader); 532 this.addEntry(child); 533 } 534 } 535 536 /** 537 * Used to set the query URI to the profile URI to store profiles. 538 */ setProfileQueryUri()539 public void setProfileQueryUri() { 540 mContactsQueryUri = Profile.CONTENT_RAW_CONTACTS_URI; 541 } 542 543 public static final Parcelable.Creator<RawContactDelta> CREATOR = 544 new Parcelable.Creator<RawContactDelta>() { 545 public RawContactDelta createFromParcel(Parcel in) { 546 final RawContactDelta state = new RawContactDelta(); 547 state.readFromParcel(in); 548 return state; 549 } 550 551 public RawContactDelta[] newArray(int size) { 552 return new RawContactDelta[size]; 553 } 554 }; 555 556 } 557