1 /* 2 * Copyright (C) 2012 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.common.model; 18 19 import android.content.ContentProviderOperation; 20 import android.content.ContentValues; 21 import android.net.Uri; 22 import android.os.Parcel; 23 import android.os.Parcelable; 24 import android.provider.BaseColumns; 25 import android.provider.ContactsContract; 26 27 import com.android.contacts.common.compat.CompatUtils; 28 import com.android.contacts.common.model.BuilderWrapper; 29 import com.android.contacts.common.testing.NeededForTesting; 30 import com.google.common.collect.Sets; 31 32 import java.util.HashSet; 33 import java.util.Map; 34 import java.util.Set; 35 36 /** 37 * Type of {@link android.content.ContentValues} that maintains both an original state and a 38 * modified version of that state. This allows us to build insert, update, 39 * or delete operations based on a "before" {@link Entity} snapshot. 40 */ 41 public class ValuesDelta implements Parcelable { 42 protected ContentValues mBefore; 43 protected ContentValues mAfter; 44 protected String mIdColumn = BaseColumns._ID; 45 private boolean mFromTemplate; 46 47 /** 48 * Next value to assign to {@link #mIdColumn} when building an insert 49 * operation through {@link #fromAfter(android.content.ContentValues)}. This is used so 50 * we can concretely reference this {@link ValuesDelta} before it has 51 * been persisted. 52 */ 53 protected static int sNextInsertId = -1; 54 55 protected ValuesDelta() { 56 } 57 58 /** 59 * Create {@link ValuesDelta}, using the given object as the 60 * "before" state, usually from an {@link Entity}. 61 */ 62 public static ValuesDelta fromBefore(ContentValues before) { 63 final ValuesDelta entry = new ValuesDelta(); 64 entry.mBefore = before; 65 entry.mAfter = new ContentValues(); 66 return entry; 67 } 68 69 /** 70 * Create {@link ValuesDelta}, using the given object as the "after" 71 * state, usually when we are inserting a row instead of updating. 72 */ 73 public static ValuesDelta fromAfter(ContentValues after) { 74 final ValuesDelta entry = new ValuesDelta(); 75 entry.mBefore = null; 76 entry.mAfter = after; 77 78 // Assign temporary id which is dropped before insert. 79 entry.mAfter.put(entry.mIdColumn, sNextInsertId--); 80 return entry; 81 } 82 83 @NeededForTesting 84 public ContentValues getAfter() { 85 return mAfter; 86 } 87 88 public boolean containsKey(String key) { 89 return ((mAfter != null && mAfter.containsKey(key)) || 90 (mBefore != null && mBefore.containsKey(key))); 91 } 92 93 public String getAsString(String key) { 94 if (mAfter != null && mAfter.containsKey(key)) { 95 return mAfter.getAsString(key); 96 } else if (mBefore != null && mBefore.containsKey(key)) { 97 return mBefore.getAsString(key); 98 } else { 99 return null; 100 } 101 } 102 103 public byte[] getAsByteArray(String key) { 104 if (mAfter != null && mAfter.containsKey(key)) { 105 return mAfter.getAsByteArray(key); 106 } else if (mBefore != null && mBefore.containsKey(key)) { 107 return mBefore.getAsByteArray(key); 108 } else { 109 return null; 110 } 111 } 112 113 public Long getAsLong(String key) { 114 if (mAfter != null && mAfter.containsKey(key)) { 115 return mAfter.getAsLong(key); 116 } else if (mBefore != null && mBefore.containsKey(key)) { 117 return mBefore.getAsLong(key); 118 } else { 119 return null; 120 } 121 } 122 123 public Integer getAsInteger(String key) { 124 return getAsInteger(key, null); 125 } 126 127 public Integer getAsInteger(String key, Integer defaultValue) { 128 if (mAfter != null && mAfter.containsKey(key)) { 129 return mAfter.getAsInteger(key); 130 } else if (mBefore != null && mBefore.containsKey(key)) { 131 return mBefore.getAsInteger(key); 132 } else { 133 return defaultValue; 134 } 135 } 136 137 public boolean isChanged(String key) { 138 if (mAfter == null || !mAfter.containsKey(key)) { 139 return false; 140 } 141 142 Object newValue = mAfter.get(key); 143 Object oldValue = mBefore.get(key); 144 145 if (oldValue == null) { 146 return newValue != null; 147 } 148 149 return !oldValue.equals(newValue); 150 } 151 152 public String getMimetype() { 153 return getAsString(ContactsContract.Data.MIMETYPE); 154 } 155 156 public Long getId() { 157 return getAsLong(mIdColumn); 158 } 159 160 public void setIdColumn(String idColumn) { 161 mIdColumn = idColumn; 162 } 163 164 public boolean isPrimary() { 165 final Long isPrimary = getAsLong(ContactsContract.Data.IS_PRIMARY); 166 return isPrimary == null ? false : isPrimary != 0; 167 } 168 169 public void setFromTemplate(boolean isFromTemplate) { 170 mFromTemplate = isFromTemplate; 171 } 172 173 public boolean isFromTemplate() { 174 return mFromTemplate; 175 } 176 177 public boolean isSuperPrimary() { 178 final Long isSuperPrimary = getAsLong(ContactsContract.Data.IS_SUPER_PRIMARY); 179 return isSuperPrimary == null ? false : isSuperPrimary != 0; 180 } 181 182 public boolean beforeExists() { 183 return (mBefore != null && mBefore.containsKey(mIdColumn)); 184 } 185 186 /** 187 * When "after" is present, then visible 188 */ 189 public boolean isVisible() { 190 return (mAfter != null); 191 } 192 193 /** 194 * When "after" is wiped, action is "delete" 195 */ 196 public boolean isDelete() { 197 return beforeExists() && (mAfter == null); 198 } 199 200 /** 201 * When no "before" or "after", is transient 202 */ 203 public boolean isTransient() { 204 return (mBefore == null) && (mAfter == null); 205 } 206 207 /** 208 * When "after" has some changes, action is "update" 209 */ 210 public boolean isUpdate() { 211 if (!beforeExists() || mAfter == null || mAfter.size() == 0) { 212 return false; 213 } 214 for (String key : mAfter.keySet()) { 215 Object newValue = mAfter.get(key); 216 Object oldValue = mBefore.get(key); 217 if (oldValue == null) { 218 if (newValue != null) { 219 return true; 220 } 221 } else if (!oldValue.equals(newValue)) { 222 return true; 223 } 224 } 225 return false; 226 } 227 228 /** 229 * When "after" has no changes, action is no-op 230 */ 231 public boolean isNoop() { 232 return beforeExists() && (mAfter != null && mAfter.size() == 0); 233 } 234 235 /** 236 * When no "before" id, and has "after", action is "insert" 237 */ 238 public boolean isInsert() { 239 return !beforeExists() && (mAfter != null); 240 } 241 242 public void markDeleted() { 243 mAfter = null; 244 } 245 246 /** 247 * Ensure that our internal structure is ready for storing updates. 248 */ 249 private void ensureUpdate() { 250 if (mAfter == null) { 251 mAfter = new ContentValues(); 252 } 253 } 254 255 public void put(String key, String value) { 256 ensureUpdate(); 257 mAfter.put(key, value); 258 } 259 260 public void put(String key, byte[] value) { 261 ensureUpdate(); 262 mAfter.put(key, value); 263 } 264 265 public void put(String key, int value) { 266 ensureUpdate(); 267 mAfter.put(key, value); 268 } 269 270 public void put(String key, long value) { 271 ensureUpdate(); 272 mAfter.put(key, value); 273 } 274 275 public void putNull(String key) { 276 ensureUpdate(); 277 mAfter.putNull(key); 278 } 279 280 public void copyStringFrom(ValuesDelta from, String key) { 281 ensureUpdate(); 282 if (containsKey(key) || from.containsKey(key)) { 283 put(key, from.getAsString(key)); 284 } 285 } 286 287 /** 288 * Return set of all keys defined through this object. 289 */ 290 public Set<String> keySet() { 291 final HashSet<String> keys = Sets.newHashSet(); 292 293 if (mBefore != null) { 294 for (Map.Entry<String, Object> entry : mBefore.valueSet()) { 295 keys.add(entry.getKey()); 296 } 297 } 298 299 if (mAfter != null) { 300 for (Map.Entry<String, Object> entry : mAfter.valueSet()) { 301 keys.add(entry.getKey()); 302 } 303 } 304 305 return keys; 306 } 307 308 /** 309 * Return complete set of "before" and "after" values mixed together, 310 * giving full state regardless of edits. 311 */ 312 public ContentValues getCompleteValues() { 313 final ContentValues values = new ContentValues(); 314 if (mBefore != null) { 315 values.putAll(mBefore); 316 } 317 if (mAfter != null) { 318 values.putAll(mAfter); 319 } 320 if (values.containsKey(ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID)) { 321 // Clear to avoid double-definitions, and prefer rows 322 values.remove(ContactsContract.CommonDataKinds.GroupMembership.GROUP_SOURCE_ID); 323 } 324 325 return values; 326 } 327 328 /** 329 * Merge the "after" values from the given {@link ValuesDelta}, 330 * discarding any existing "after" state. This is typically used when 331 * re-parenting changes onto an updated {@link Entity}. 332 */ 333 public static ValuesDelta mergeAfter(ValuesDelta local, ValuesDelta remote) { 334 // Bail early if trying to merge delete with missing local 335 if (local == null && (remote.isDelete() || remote.isTransient())) return null; 336 337 // Create local version if none exists yet 338 if (local == null) local = new ValuesDelta(); 339 340 if (!local.beforeExists()) { 341 // Any "before" record is missing, so take all values as "insert" 342 local.mAfter = remote.getCompleteValues(); 343 } else { 344 // Existing "update" with only "after" values 345 local.mAfter = remote.mAfter; 346 } 347 348 return local; 349 } 350 351 @Override 352 public boolean equals(Object object) { 353 if (object instanceof ValuesDelta) { 354 // Only exactly equal with both are identical subsets 355 final ValuesDelta other = (ValuesDelta)object; 356 return this.subsetEquals(other) && other.subsetEquals(this); 357 } 358 return false; 359 } 360 361 @Override 362 public String toString() { 363 final StringBuilder builder = new StringBuilder(); 364 toString(builder); 365 return builder.toString(); 366 } 367 368 /** 369 * Helper for building string representation, leveraging the given 370 * {@link StringBuilder} to minimize allocations. 371 */ 372 public void toString(StringBuilder builder) { 373 builder.append("{ "); 374 builder.append("IdColumn="); 375 builder.append(mIdColumn); 376 builder.append(", FromTemplate="); 377 builder.append(mFromTemplate); 378 builder.append(", "); 379 for (String key : this.keySet()) { 380 builder.append(key); 381 builder.append("="); 382 builder.append(this.getAsString(key)); 383 builder.append(", "); 384 } 385 builder.append("}"); 386 } 387 388 /** 389 * Check if the given {@link ValuesDelta} is both a subset of this 390 * object, and any defined keys have equal values. 391 */ 392 public boolean subsetEquals(ValuesDelta other) { 393 for (String key : this.keySet()) { 394 final String ourValue = this.getAsString(key); 395 final String theirValue = other.getAsString(key); 396 if (ourValue == null) { 397 // If they have value when we're null, no match 398 if (theirValue != null) return false; 399 } else { 400 // If both values defined and aren't equal, no match 401 if (!ourValue.equals(theirValue)) return false; 402 } 403 } 404 // All values compared and matched 405 return true; 406 } 407 408 /** 409 * Build a {@link android.content.ContentProviderOperation} that will transform our 410 * "before" state into our "after" state, using insert, update, or 411 * delete as needed. 412 */ 413 public ContentProviderOperation.Builder buildDiff(Uri targetUri) { 414 return buildDiffHelper(targetUri); 415 } 416 417 /** 418 * For compatibility purpose. 419 */ 420 public BuilderWrapper buildDiffWrapper(Uri targetUri) { 421 final ContentProviderOperation.Builder builder = buildDiffHelper(targetUri); 422 BuilderWrapper bw = null; 423 if (isInsert()) { 424 bw = new BuilderWrapper(builder, CompatUtils.TYPE_INSERT); 425 } else if (isDelete()) { 426 bw = new BuilderWrapper(builder, CompatUtils.TYPE_DELETE); 427 } else if (isUpdate()) { 428 bw = new BuilderWrapper(builder, CompatUtils.TYPE_UPDATE); 429 } 430 return bw; 431 } 432 433 private ContentProviderOperation.Builder buildDiffHelper(Uri targetUri) { 434 ContentProviderOperation.Builder builder = null; 435 if (isInsert()) { 436 // Changed values are "insert" back-referenced to Contact 437 mAfter.remove(mIdColumn); 438 builder = ContentProviderOperation.newInsert(targetUri); 439 builder.withValues(mAfter); 440 } else if (isDelete()) { 441 // When marked for deletion and "before" exists, then "delete" 442 builder = ContentProviderOperation.newDelete(targetUri); 443 builder.withSelection(mIdColumn + "=" + getId(), null); 444 } else if (isUpdate()) { 445 // When has changes and "before" exists, then "update" 446 builder = ContentProviderOperation.newUpdate(targetUri); 447 builder.withSelection(mIdColumn + "=" + getId(), null); 448 builder.withValues(mAfter); 449 } 450 return builder; 451 } 452 453 /** {@inheritDoc} */ 454 public int describeContents() { 455 // Nothing special about this parcel 456 return 0; 457 } 458 459 /** {@inheritDoc} */ 460 public void writeToParcel(Parcel dest, int flags) { 461 dest.writeParcelable(mBefore, flags); 462 dest.writeParcelable(mAfter, flags); 463 dest.writeString(mIdColumn); 464 } 465 466 public void readFromParcel(Parcel source) { 467 final ClassLoader loader = getClass().getClassLoader(); 468 mBefore = source.<ContentValues> readParcelable(loader); 469 mAfter = source.<ContentValues> readParcelable(loader); 470 mIdColumn = source.readString(); 471 } 472 473 public static final Creator<ValuesDelta> CREATOR = new Creator<ValuesDelta>() { 474 public ValuesDelta createFromParcel(Parcel in) { 475 final ValuesDelta values = new ValuesDelta(); 476 values.readFromParcel(in); 477 return values; 478 } 479 480 public ValuesDelta[] newArray(int size) { 481 return new ValuesDelta[size]; 482 } 483 }; 484 485 public void setGroupRowId(long groupId) { 486 put(ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID, groupId); 487 } 488 489 public Long getGroupRowId() { 490 return getAsLong(ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID); 491 } 492 493 public void setPhoto(byte[] value) { 494 put(ContactsContract.CommonDataKinds.Photo.PHOTO, value); 495 } 496 497 public byte[] getPhoto() { 498 return getAsByteArray(ContactsContract.CommonDataKinds.Photo.PHOTO); 499 } 500 501 public void setSuperPrimary(boolean val) { 502 if (val) { 503 put(ContactsContract.Data.IS_SUPER_PRIMARY, 1); 504 } else { 505 put(ContactsContract.Data.IS_SUPER_PRIMARY, 0); 506 } 507 } 508 509 public void setPhoneticFamilyName(String value) { 510 put(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_FAMILY_NAME, value); 511 } 512 513 public void setPhoneticMiddleName(String value) { 514 put(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_MIDDLE_NAME, value); 515 } 516 517 public void setPhoneticGivenName(String value) { 518 put(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_GIVEN_NAME, value); 519 } 520 521 public String getPhoneticFamilyName() { 522 return getAsString(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_FAMILY_NAME); 523 } 524 525 public String getPhoneticMiddleName() { 526 return getAsString(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_MIDDLE_NAME); 527 } 528 529 public String getPhoneticGivenName() { 530 return getAsString(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_GIVEN_NAME); 531 } 532 533 public String getDisplayName() { 534 return getAsString(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME); 535 } 536 537 public void setDisplayName(String name) { 538 if (name == null) { 539 putNull(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME); 540 } else { 541 put(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name); 542 } 543 } 544 545 public void copyStructuredNameFieldsFrom(ValuesDelta name) { 546 copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME); 547 548 copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME); 549 copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME); 550 copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.PREFIX); 551 copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME); 552 copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.SUFFIX); 553 554 copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.PHONETIC_GIVEN_NAME); 555 copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.PHONETIC_MIDDLE_NAME); 556 copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.PHONETIC_FAMILY_NAME); 557 558 copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.FULL_NAME_STYLE); 559 copyStringFrom(name, ContactsContract.Data.DATA11); 560 } 561 562 public String getPhoneNumber() { 563 return getAsString(ContactsContract.CommonDataKinds.Phone.NUMBER); 564 } 565 566 public String getPhoneNormalizedNumber() { 567 return getAsString(ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER); 568 } 569 570 public boolean hasPhoneType() { 571 return getPhoneType() != null; 572 } 573 574 public Integer getPhoneType() { 575 return getAsInteger(ContactsContract.CommonDataKinds.Phone.TYPE); 576 } 577 578 public String getPhoneLabel() { 579 return getAsString(ContactsContract.CommonDataKinds.Phone.LABEL); 580 } 581 582 public String getEmailData() { 583 return getAsString(ContactsContract.CommonDataKinds.Email.DATA); 584 } 585 586 public boolean hasEmailType() { 587 return getEmailType() != null; 588 } 589 590 public Integer getEmailType() { 591 return getAsInteger(ContactsContract.CommonDataKinds.Email.TYPE); 592 } 593 594 public String getEmailLabel() { 595 return getAsString(ContactsContract.CommonDataKinds.Email.LABEL); 596 } 597 } 598