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