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