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