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