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.loaderapp.model; 18 19 import com.google.android.collect.Lists; 20 import com.google.android.collect.Maps; 21 import com.google.android.collect.Sets; 22 23 import android.content.ContentProviderOperation; 24 import android.content.ContentValues; 25 import android.content.Entity; 26 import android.content.ContentProviderOperation.Builder; 27 import android.content.Entity.NamedContentValues; 28 import android.net.Uri; 29 import android.os.Parcel; 30 import android.os.Parcelable; 31 import android.provider.BaseColumns; 32 import android.provider.ContactsContract.Data; 33 import android.provider.ContactsContract.RawContacts; 34 import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 35 import android.util.Log; 36 37 import java.util.ArrayList; 38 import java.util.HashMap; 39 import java.util.HashSet; 40 import java.util.Map; 41 import java.util.Set; 42 43 /** 44 * Contains an {@link Entity} and records any modifications separately so the 45 * original {@link Entity} can be swapped out with a newer version and the 46 * changes still cleanly applied. 47 * <p> 48 * One benefit of this approach is that we can build changes entirely on an 49 * empty {@link Entity}, which then becomes an insert {@link RawContacts} case. 50 * <p> 51 * When applying modifications over an {@link Entity}, we try finding the 52 * original {@link Data#_ID} rows where the modifications took place. If those 53 * rows are missing from the new {@link Entity}, we know the original data must 54 * be deleted, but to preserve the user modifications we treat as an insert. 55 */ 56 public class EntityDelta implements Parcelable { 57 // TODO: optimize by using contentvalues pool, since we allocate so many of them 58 59 private static final String TAG = "EntityDelta"; 60 private static final boolean LOGV = true; 61 62 /** 63 * Direct values from {@link Entity#getEntityValues()}. 64 */ 65 private ValuesDelta mValues; 66 67 /** 68 * Internal map of children values from {@link Entity#getSubValues()}, which 69 * we store here sorted into {@link Data#MIMETYPE} bins. 70 */ 71 private HashMap<String, ArrayList<ValuesDelta>> mEntries = Maps.newHashMap(); 72 73 public EntityDelta() { 74 } 75 76 public EntityDelta(ValuesDelta values) { 77 mValues = values; 78 } 79 80 /** 81 * Build an {@link EntityDelta} using the given {@link Entity} as a 82 * starting point; the "before" snapshot. 83 */ 84 public static EntityDelta fromBefore(Entity before) { 85 final EntityDelta entity = new EntityDelta(); 86 entity.mValues = ValuesDelta.fromBefore(before.getEntityValues()); 87 entity.mValues.setIdColumn(RawContacts._ID); 88 for (NamedContentValues namedValues : before.getSubValues()) { 89 entity.addEntry(ValuesDelta.fromBefore(namedValues.values)); 90 } 91 return entity; 92 } 93 94 /** 95 * Merge the "after" values from the given {@link EntityDelta} onto the 96 * "before" state represented by this {@link EntityDelta}, discarding any 97 * existing "after" states. This is typically used when re-parenting changes 98 * onto an updated {@link Entity}. 99 */ 100 public static EntityDelta mergeAfter(EntityDelta local, EntityDelta remote) { 101 // Bail early if trying to merge delete with missing local 102 final ValuesDelta remoteValues = remote.mValues; 103 if (local == null && (remoteValues.isDelete() || remoteValues.isTransient())) return null; 104 105 // Create local version if none exists yet 106 if (local == null) local = new EntityDelta(); 107 108 if (LOGV) { 109 final Long localVersion = (local.mValues == null) ? null : local.mValues 110 .getAsLong(RawContacts.VERSION); 111 final Long remoteVersion = remote.mValues.getAsLong(RawContacts.VERSION); 112 Log.d(TAG, "Re-parenting from original version " + remoteVersion + " to " 113 + localVersion); 114 } 115 116 // Create values if needed, and merge "after" changes 117 local.mValues = ValuesDelta.mergeAfter(local.mValues, remote.mValues); 118 119 // Find matching local entry for each remote values, or create 120 for (ArrayList<ValuesDelta> mimeEntries : remote.mEntries.values()) { 121 for (ValuesDelta remoteEntry : mimeEntries) { 122 final Long childId = remoteEntry.getId(); 123 124 // Find or create local match and merge 125 final ValuesDelta localEntry = local.getEntry(childId); 126 final ValuesDelta merged = ValuesDelta.mergeAfter(localEntry, remoteEntry); 127 128 if (localEntry == null && merged != null) { 129 // No local entry before, so insert 130 local.addEntry(merged); 131 } 132 } 133 } 134 135 return local; 136 } 137 138 public ValuesDelta getValues() { 139 return mValues; 140 } 141 142 public boolean isContactInsert() { 143 return mValues.isInsert(); 144 } 145 146 /** 147 * Get the {@link ValuesDelta} child marked as {@link Data#IS_PRIMARY}, 148 * which may return null when no entry exists. 149 */ 150 public ValuesDelta getPrimaryEntry(String mimeType) { 151 final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false); 152 if (mimeEntries == null) return null; 153 154 for (ValuesDelta entry : mimeEntries) { 155 if (entry.isPrimary()) { 156 return entry; 157 } 158 } 159 160 // When no direct primary, return something 161 return mimeEntries.size() > 0 ? mimeEntries.get(0) : null; 162 } 163 164 /** 165 * calls {@link #getSuperPrimaryEntry(String, boolean)} with true 166 * @see #getSuperPrimaryEntry(String, boolean) 167 */ 168 public ValuesDelta getSuperPrimaryEntry(String mimeType) { 169 return getSuperPrimaryEntry(mimeType, true); 170 } 171 172 /** 173 * Returns the super-primary entry for the given mime type 174 * @param forceSelection if true, will try to return some value even if a super-primary 175 * doesn't exist (may be a primary, or just a random item 176 * @return 177 */ 178 public ValuesDelta getSuperPrimaryEntry(String mimeType, boolean forceSelection) { 179 final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false); 180 if (mimeEntries == null) return null; 181 182 ValuesDelta primary = null; 183 for (ValuesDelta entry : mimeEntries) { 184 if (entry.isSuperPrimary()) { 185 return entry; 186 } else if (entry.isPrimary()) { 187 primary = entry; 188 } 189 } 190 191 if (!forceSelection) { 192 return null; 193 } 194 195 // When no direct super primary, return something 196 if (primary != null) { 197 return primary; 198 } 199 return mimeEntries.size() > 0 ? mimeEntries.get(0) : null; 200 } 201 202 /** 203 * Return the list of child {@link ValuesDelta} from our optimized map, 204 * creating the list if requested. 205 */ 206 private ArrayList<ValuesDelta> getMimeEntries(String mimeType, boolean lazyCreate) { 207 ArrayList<ValuesDelta> mimeEntries = mEntries.get(mimeType); 208 if (mimeEntries == null && lazyCreate) { 209 mimeEntries = Lists.newArrayList(); 210 mEntries.put(mimeType, mimeEntries); 211 } 212 return mimeEntries; 213 } 214 215 public ArrayList<ValuesDelta> getMimeEntries(String mimeType) { 216 return getMimeEntries(mimeType, false); 217 } 218 219 public int getMimeEntriesCount(String mimeType, boolean onlyVisible) { 220 final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType); 221 if (mimeEntries == null) return 0; 222 223 int count = 0; 224 for (ValuesDelta child : mimeEntries) { 225 // Skip deleted items when requesting only visible 226 if (onlyVisible && !child.isVisible()) continue; 227 count++; 228 } 229 return count; 230 } 231 232 public boolean hasMimeEntries(String mimeType) { 233 return mEntries.containsKey(mimeType); 234 } 235 236 public ValuesDelta addEntry(ValuesDelta entry) { 237 final String mimeType = entry.getMimetype(); 238 getMimeEntries(mimeType, true).add(entry); 239 return entry; 240 } 241 242 /** 243 * Find entry with the given {@link BaseColumns#_ID} value. 244 */ 245 public ValuesDelta getEntry(Long childId) { 246 if (childId == null) { 247 // Requesting an "insert" entry, which has no "before" 248 return null; 249 } 250 251 // Search all children for requested entry 252 for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) { 253 for (ValuesDelta entry : mimeEntries) { 254 if (childId.equals(entry.getId())) { 255 return entry; 256 } 257 } 258 } 259 return null; 260 } 261 262 /** 263 * Return the total number of {@link ValuesDelta} contained. 264 */ 265 public int getEntryCount(boolean onlyVisible) { 266 int count = 0; 267 for (String mimeType : mEntries.keySet()) { 268 count += getMimeEntriesCount(mimeType, onlyVisible); 269 } 270 return count; 271 } 272 273 @Override 274 public boolean equals(Object object) { 275 if (object instanceof EntityDelta) { 276 final EntityDelta other = (EntityDelta)object; 277 278 // Equality failed if parent values different 279 if (!other.mValues.equals(mValues)) return false; 280 281 for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) { 282 for (ValuesDelta child : mimeEntries) { 283 // Equality failed if any children unmatched 284 if (!other.containsEntry(child)) return false; 285 } 286 } 287 288 // Passed all tests, so equal 289 return true; 290 } 291 return false; 292 } 293 294 private boolean containsEntry(ValuesDelta entry) { 295 for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) { 296 for (ValuesDelta child : mimeEntries) { 297 // Contained if we find any child that matches 298 if (child.equals(entry)) return true; 299 } 300 } 301 return false; 302 } 303 304 /** 305 * Mark this entire object deleted, including any {@link ValuesDelta}. 306 */ 307 public void markDeleted() { 308 this.mValues.markDeleted(); 309 for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) { 310 for (ValuesDelta child : mimeEntries) { 311 child.markDeleted(); 312 } 313 } 314 } 315 316 @Override 317 public String toString() { 318 final StringBuilder builder = new StringBuilder(); 319 builder.append("\n("); 320 builder.append(mValues.toString()); 321 builder.append(") = {"); 322 for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) { 323 for (ValuesDelta child : mimeEntries) { 324 builder.append("\n\t"); 325 child.toString(builder); 326 } 327 } 328 builder.append("\n}\n"); 329 return builder.toString(); 330 } 331 332 /** 333 * Consider building the given {@link ContentProviderOperation.Builder} and 334 * appending it to the given list, which only happens if builder is valid. 335 */ 336 private void possibleAdd(ArrayList<ContentProviderOperation> diff, 337 ContentProviderOperation.Builder builder) { 338 if (builder != null) { 339 diff.add(builder.build()); 340 } 341 } 342 343 /** 344 * Build a list of {@link ContentProviderOperation} that will assert any 345 * "before" state hasn't changed. This is maintained separately so that all 346 * asserts can take place before any updates occur. 347 */ 348 public void buildAssert(ArrayList<ContentProviderOperation> buildInto) { 349 final boolean isContactInsert = mValues.isInsert(); 350 if (!isContactInsert) { 351 // Assert version is consistent while persisting changes 352 final Long beforeId = mValues.getId(); 353 final Long beforeVersion = mValues.getAsLong(RawContacts.VERSION); 354 if (beforeId == null || beforeVersion == null) return; 355 356 final ContentProviderOperation.Builder builder = ContentProviderOperation 357 .newAssertQuery(RawContacts.CONTENT_URI); 358 builder.withSelection(RawContacts._ID + "=" + beforeId, null); 359 builder.withValue(RawContacts.VERSION, beforeVersion); 360 buildInto.add(builder.build()); 361 } 362 } 363 364 /** 365 * Build a list of {@link ContentProviderOperation} that will transform the 366 * current "before" {@link Entity} state into the modified state which this 367 * {@link EntityDelta} represents. 368 */ 369 public void buildDiff(ArrayList<ContentProviderOperation> buildInto) { 370 final int firstIndex = buildInto.size(); 371 372 final boolean isContactInsert = mValues.isInsert(); 373 final boolean isContactDelete = mValues.isDelete(); 374 final boolean isContactUpdate = !isContactInsert && !isContactDelete; 375 376 final Long beforeId = mValues.getId(); 377 378 Builder builder; 379 380 if (isContactInsert) { 381 // TODO: for now simply disabling aggregation when a new contact is 382 // created on the phone. In the future, will show aggregation suggestions 383 // after saving the contact. 384 mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED); 385 } 386 387 // Build possible operation at Contact level 388 builder = mValues.buildDiff(RawContacts.CONTENT_URI); 389 possibleAdd(buildInto, builder); 390 391 // Build operations for all children 392 for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) { 393 for (ValuesDelta child : mimeEntries) { 394 // Ignore children if parent was deleted 395 if (isContactDelete) continue; 396 397 builder = child.buildDiff(Data.CONTENT_URI); 398 if (child.isInsert()) { 399 if (isContactInsert) { 400 // Parent is brand new insert, so back-reference _id 401 builder.withValueBackReference(Data.RAW_CONTACT_ID, firstIndex); 402 } else { 403 // Inserting under existing, so fill with known _id 404 builder.withValue(Data.RAW_CONTACT_ID, beforeId); 405 } 406 } else if (isContactInsert && builder != null) { 407 // Child must be insert when Contact insert 408 throw new IllegalArgumentException("When parent insert, child must be also"); 409 } 410 possibleAdd(buildInto, builder); 411 } 412 } 413 414 final boolean addedOperations = buildInto.size() > firstIndex; 415 if (addedOperations && isContactUpdate) { 416 // Suspend aggregation while persisting updates 417 builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_SUSPENDED); 418 buildInto.add(firstIndex, builder.build()); 419 420 // Restore aggregation mode as last operation 421 builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_DEFAULT); 422 buildInto.add(builder.build()); 423 } else if (isContactInsert) { 424 // Restore aggregation mode as last operation 425 builder = ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI); 426 builder.withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT); 427 builder.withSelection(RawContacts._ID + "=?", new String[1]); 428 builder.withSelectionBackReference(0, firstIndex); 429 buildInto.add(builder.build()); 430 } 431 } 432 433 /** 434 * Build a {@link ContentProviderOperation} that changes 435 * {@link RawContacts#AGGREGATION_MODE} to the given value. 436 */ 437 protected Builder buildSetAggregationMode(Long beforeId, int mode) { 438 Builder builder = ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI); 439 builder.withValue(RawContacts.AGGREGATION_MODE, mode); 440 builder.withSelection(RawContacts._ID + "=" + beforeId, null); 441 return builder; 442 } 443 444 /** {@inheritDoc} */ 445 public int describeContents() { 446 // Nothing special about this parcel 447 return 0; 448 } 449 450 /** {@inheritDoc} */ 451 public void writeToParcel(Parcel dest, int flags) { 452 final int size = this.getEntryCount(false); 453 dest.writeInt(size); 454 dest.writeParcelable(mValues, flags); 455 for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) { 456 for (ValuesDelta child : mimeEntries) { 457 dest.writeParcelable(child, flags); 458 } 459 } 460 } 461 462 public void readFromParcel(Parcel source) { 463 final ClassLoader loader = getClass().getClassLoader(); 464 final int size = source.readInt(); 465 mValues = source.<ValuesDelta> readParcelable(loader); 466 for (int i = 0; i < size; i++) { 467 final ValuesDelta child = source.<ValuesDelta> readParcelable(loader); 468 this.addEntry(child); 469 } 470 } 471 472 public static final Parcelable.Creator<EntityDelta> CREATOR = new Parcelable.Creator<EntityDelta>() { 473 public EntityDelta createFromParcel(Parcel in) { 474 final EntityDelta state = new EntityDelta(); 475 state.readFromParcel(in); 476 return state; 477 } 478 479 public EntityDelta[] newArray(int size) { 480 return new EntityDelta[size]; 481 } 482 }; 483 484 /** 485 * Type of {@link ContentValues} that maintains both an original state and a 486 * modified version of that state. This allows us to build insert, update, 487 * or delete operations based on a "before" {@link Entity} snapshot. 488 */ 489 public static class ValuesDelta implements Parcelable { 490 protected ContentValues mBefore; 491 protected ContentValues mAfter; 492 protected String mIdColumn = BaseColumns._ID; 493 private boolean mFromTemplate; 494 495 /** 496 * Next value to assign to {@link #mIdColumn} when building an insert 497 * operation through {@link #fromAfter(ContentValues)}. This is used so 498 * we can concretely reference this {@link ValuesDelta} before it has 499 * been persisted. 500 */ 501 protected static int sNextInsertId = -1; 502 503 protected ValuesDelta() { 504 } 505 506 /** 507 * Create {@link ValuesDelta}, using the given object as the 508 * "before" state, usually from an {@link Entity}. 509 */ 510 public static ValuesDelta fromBefore(ContentValues before) { 511 final ValuesDelta entry = new ValuesDelta(); 512 entry.mBefore = before; 513 entry.mAfter = new ContentValues(); 514 return entry; 515 } 516 517 /** 518 * Create {@link ValuesDelta}, using the given object as the "after" 519 * state, usually when we are inserting a row instead of updating. 520 */ 521 public static ValuesDelta fromAfter(ContentValues after) { 522 final ValuesDelta entry = new ValuesDelta(); 523 entry.mBefore = null; 524 entry.mAfter = after; 525 526 // Assign temporary id which is dropped before insert. 527 entry.mAfter.put(entry.mIdColumn, sNextInsertId--); 528 return entry; 529 } 530 531 public ContentValues getAfter() { 532 return mAfter; 533 } 534 535 public String getAsString(String key) { 536 if (mAfter != null && mAfter.containsKey(key)) { 537 return mAfter.getAsString(key); 538 } else if (mBefore != null && mBefore.containsKey(key)) { 539 return mBefore.getAsString(key); 540 } else { 541 return null; 542 } 543 } 544 545 public byte[] getAsByteArray(String key) { 546 if (mAfter != null && mAfter.containsKey(key)) { 547 return mAfter.getAsByteArray(key); 548 } else if (mBefore != null && mBefore.containsKey(key)) { 549 return mBefore.getAsByteArray(key); 550 } else { 551 return null; 552 } 553 } 554 555 public Long getAsLong(String key) { 556 if (mAfter != null && mAfter.containsKey(key)) { 557 return mAfter.getAsLong(key); 558 } else if (mBefore != null && mBefore.containsKey(key)) { 559 return mBefore.getAsLong(key); 560 } else { 561 return null; 562 } 563 } 564 565 public Integer getAsInteger(String key) { 566 return getAsInteger(key, null); 567 } 568 569 public Integer getAsInteger(String key, Integer defaultValue) { 570 if (mAfter != null && mAfter.containsKey(key)) { 571 return mAfter.getAsInteger(key); 572 } else if (mBefore != null && mBefore.containsKey(key)) { 573 return mBefore.getAsInteger(key); 574 } else { 575 return defaultValue; 576 } 577 } 578 579 public String getMimetype() { 580 return getAsString(Data.MIMETYPE); 581 } 582 583 public Long getId() { 584 return getAsLong(mIdColumn); 585 } 586 587 public void setIdColumn(String idColumn) { 588 mIdColumn = idColumn; 589 } 590 591 public boolean isPrimary() { 592 final Long isPrimary = getAsLong(Data.IS_PRIMARY); 593 return isPrimary == null ? false : isPrimary != 0; 594 } 595 596 public void setFromTemplate(boolean isFromTemplate) { 597 mFromTemplate = isFromTemplate; 598 } 599 600 public boolean isFromTemplate() { 601 return mFromTemplate; 602 } 603 604 public boolean isSuperPrimary() { 605 final Long isSuperPrimary = getAsLong(Data.IS_SUPER_PRIMARY); 606 return isSuperPrimary == null ? false : isSuperPrimary != 0; 607 } 608 609 public boolean beforeExists() { 610 return (mBefore != null && mBefore.containsKey(mIdColumn)); 611 } 612 613 public boolean isVisible() { 614 // When "after" is present, then visible 615 return (mAfter != null); 616 } 617 618 public boolean isDelete() { 619 // When "after" is wiped, action is "delete" 620 return beforeExists() && (mAfter == null); 621 } 622 623 public boolean isTransient() { 624 // When no "before" or "after", is transient 625 return (mBefore == null) && (mAfter == null); 626 } 627 628 public boolean isUpdate() { 629 // When "after" has some changes, action is "update" 630 return beforeExists() && (mAfter != null && mAfter.size() > 0); 631 } 632 633 public boolean isNoop() { 634 // When "after" has no changes, action is no-op 635 return beforeExists() && (mAfter != null && mAfter.size() == 0); 636 } 637 638 public boolean isInsert() { 639 // When no "before" id, and has "after", action is "insert" 640 return !beforeExists() && (mAfter != null); 641 } 642 643 public void markDeleted() { 644 mAfter = null; 645 } 646 647 /** 648 * Ensure that our internal structure is ready for storing updates. 649 */ 650 private void ensureUpdate() { 651 if (mAfter == null) { 652 mAfter = new ContentValues(); 653 } 654 } 655 656 public void put(String key, String value) { 657 ensureUpdate(); 658 mAfter.put(key, value); 659 } 660 661 public void put(String key, byte[] value) { 662 ensureUpdate(); 663 mAfter.put(key, value); 664 } 665 666 public void put(String key, int value) { 667 ensureUpdate(); 668 mAfter.put(key, value); 669 } 670 671 /** 672 * Return set of all keys defined through this object. 673 */ 674 public Set<String> keySet() { 675 final HashSet<String> keys = Sets.newHashSet(); 676 677 if (mBefore != null) { 678 for (Map.Entry<String, Object> entry : mBefore.valueSet()) { 679 keys.add(entry.getKey()); 680 } 681 } 682 683 if (mAfter != null) { 684 for (Map.Entry<String, Object> entry : mAfter.valueSet()) { 685 keys.add(entry.getKey()); 686 } 687 } 688 689 return keys; 690 } 691 692 /** 693 * Return complete set of "before" and "after" values mixed together, 694 * giving full state regardless of edits. 695 */ 696 public ContentValues getCompleteValues() { 697 final ContentValues values = new ContentValues(); 698 if (mBefore != null) { 699 values.putAll(mBefore); 700 } 701 if (mAfter != null) { 702 values.putAll(mAfter); 703 } 704 if (values.containsKey(GroupMembership.GROUP_ROW_ID)) { 705 // Clear to avoid double-definitions, and prefer rows 706 values.remove(GroupMembership.GROUP_SOURCE_ID); 707 } 708 709 return values; 710 } 711 712 /** 713 * Merge the "after" values from the given {@link ValuesDelta}, 714 * discarding any existing "after" state. This is typically used when 715 * re-parenting changes onto an updated {@link Entity}. 716 */ 717 public static ValuesDelta mergeAfter(ValuesDelta local, ValuesDelta remote) { 718 // Bail early if trying to merge delete with missing local 719 if (local == null && (remote.isDelete() || remote.isTransient())) return null; 720 721 // Create local version if none exists yet 722 if (local == null) local = new ValuesDelta(); 723 724 if (!local.beforeExists()) { 725 // Any "before" record is missing, so take all values as "insert" 726 local.mAfter = remote.getCompleteValues(); 727 } else { 728 // Existing "update" with only "after" values 729 local.mAfter = remote.mAfter; 730 } 731 732 return local; 733 } 734 735 @Override 736 public boolean equals(Object object) { 737 if (object instanceof ValuesDelta) { 738 // Only exactly equal with both are identical subsets 739 final ValuesDelta other = (ValuesDelta)object; 740 return this.subsetEquals(other) && other.subsetEquals(this); 741 } 742 return false; 743 } 744 745 @Override 746 public String toString() { 747 final StringBuilder builder = new StringBuilder(); 748 toString(builder); 749 return builder.toString(); 750 } 751 752 /** 753 * Helper for building string representation, leveraging the given 754 * {@link StringBuilder} to minimize allocations. 755 */ 756 public void toString(StringBuilder builder) { 757 builder.append("{ "); 758 for (String key : this.keySet()) { 759 builder.append(key); 760 builder.append("="); 761 builder.append(this.getAsString(key)); 762 builder.append(", "); 763 } 764 builder.append("}"); 765 } 766 767 /** 768 * Check if the given {@link ValuesDelta} is both a subset of this 769 * object, and any defined keys have equal values. 770 */ 771 public boolean subsetEquals(ValuesDelta other) { 772 for (String key : this.keySet()) { 773 final String ourValue = this.getAsString(key); 774 final String theirValue = other.getAsString(key); 775 if (ourValue == null) { 776 // If they have value when we're null, no match 777 if (theirValue != null) return false; 778 } else { 779 // If both values defined and aren't equal, no match 780 if (!ourValue.equals(theirValue)) return false; 781 } 782 } 783 // All values compared and matched 784 return true; 785 } 786 787 /** 788 * Build a {@link ContentProviderOperation} that will transform our 789 * "before" state into our "after" state, using insert, update, or 790 * delete as needed. 791 */ 792 public ContentProviderOperation.Builder buildDiff(Uri targetUri) { 793 Builder builder = null; 794 if (isInsert()) { 795 // Changed values are "insert" back-referenced to Contact 796 mAfter.remove(mIdColumn); 797 builder = ContentProviderOperation.newInsert(targetUri); 798 builder.withValues(mAfter); 799 } else if (isDelete()) { 800 // When marked for deletion and "before" exists, then "delete" 801 builder = ContentProviderOperation.newDelete(targetUri); 802 builder.withSelection(mIdColumn + "=" + getId(), null); 803 } else if (isUpdate()) { 804 // When has changes and "before" exists, then "update" 805 builder = ContentProviderOperation.newUpdate(targetUri); 806 builder.withSelection(mIdColumn + "=" + getId(), null); 807 builder.withValues(mAfter); 808 } 809 return builder; 810 } 811 812 /** {@inheritDoc} */ 813 public int describeContents() { 814 // Nothing special about this parcel 815 return 0; 816 } 817 818 /** {@inheritDoc} */ 819 public void writeToParcel(Parcel dest, int flags) { 820 dest.writeParcelable(mBefore, flags); 821 dest.writeParcelable(mAfter, flags); 822 dest.writeString(mIdColumn); 823 } 824 825 public void readFromParcel(Parcel source) { 826 final ClassLoader loader = getClass().getClassLoader(); 827 mBefore = source.<ContentValues> readParcelable(loader); 828 mAfter = source.<ContentValues> readParcelable(loader); 829 mIdColumn = source.readString(); 830 } 831 832 public static final Parcelable.Creator<ValuesDelta> CREATOR = new Parcelable.Creator<ValuesDelta>() { 833 public ValuesDelta createFromParcel(Parcel in) { 834 final ValuesDelta values = new ValuesDelta(); 835 values.readFromParcel(in); 836 return values; 837 } 838 839 public ValuesDelta[] newArray(int size) { 840 return new ValuesDelta[size]; 841 } 842 }; 843 } 844 } 845