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