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.Data; 28 import android.provider.ContactsContract.Profile; 29 import android.provider.ContactsContract.RawContacts; 30 import android.util.Log; 31 32 import com.android.contacts.compat.CompatUtils; 33 import com.android.contacts.model.account.AccountType; 34 import com.android.contacts.model.account.AccountWithDataSet; 35 36 import com.google.common.collect.Lists; 37 import com.google.common.collect.Maps; 38 39 import java.util.ArrayList; 40 import java.util.HashMap; 41 42 /** 43 * Contains a {@link RawContact} and records any modifications separately so the 44 * original {@link RawContact} can be swapped out with a newer version and the 45 * changes still cleanly applied. 46 * <p> 47 * One benefit of this approach is that we can build changes entirely on an 48 * empty {@link RawContact}, which then becomes an insert {@link RawContacts} case. 49 * <p> 50 * When applying modifications over an {@link RawContact}, we try finding the 51 * original {@link Data#_ID} rows where the modifications took place. If those 52 * rows are missing from the new {@link RawContact}, we know the original data must 53 * be deleted, but to preserve the user modifications we treat as an insert. 54 */ 55 public class RawContactDelta implements Parcelable { 56 // TODO: optimize by using contentvalues pool, since we allocate so many of them 57 58 private static final String TAG = "EntityDelta"; 59 private static final boolean DEBUG = false; 60 61 /** 62 * Direct values from {@link Entity#getEntityValues()}. 63 */ 64 private ValuesDelta mValues; 65 66 /** 67 * URI used for contacts queries, by default it is set to query raw contacts. 68 * It can be set to query the profile's raw contact(s). 69 */ 70 private Uri mContactsQueryUri = RawContacts.CONTENT_URI; 71 72 /** 73 * Internal map of children values from {@link Entity#getSubValues()}, which 74 * we store here sorted into {@link Data#MIMETYPE} bins. 75 */ 76 private final HashMap<String, ArrayList<ValuesDelta>> mEntries = Maps.newHashMap(); 77 RawContactDelta()78 public RawContactDelta() { 79 } 80 RawContactDelta(ValuesDelta values)81 public RawContactDelta(ValuesDelta values) { 82 mValues = values; 83 } 84 85 /** 86 * Build an {@link RawContactDelta} using the given {@link RawContact} as a 87 * starting point; the "before" snapshot. 88 */ fromBefore(RawContact before)89 public static RawContactDelta fromBefore(RawContact before) { 90 final RawContactDelta rawContactDelta = new RawContactDelta(); 91 rawContactDelta.mValues = ValuesDelta.fromBefore(before.getValues()); 92 rawContactDelta.mValues.setIdColumn(RawContacts._ID); 93 for (final ContentValues values : before.getContentValues()) { 94 rawContactDelta.addEntry(ValuesDelta.fromBefore(values)); 95 } 96 return rawContactDelta; 97 } 98 99 /** 100 * Merge the "after" values from the given {@link RawContactDelta} onto the 101 * "before" state represented by this {@link RawContactDelta}, discarding any 102 * existing "after" states. This is typically used when re-parenting changes 103 * onto an updated {@link Entity}. 104 */ mergeAfter(RawContactDelta local, RawContactDelta remote)105 public static RawContactDelta mergeAfter(RawContactDelta local, RawContactDelta remote) { 106 // Bail early if trying to merge delete with missing local 107 final ValuesDelta remoteValues = remote.mValues; 108 if (local == null && (remoteValues.isDelete() || remoteValues.isTransient())) return null; 109 110 // Create local version if none exists yet 111 if (local == null) local = new RawContactDelta(); 112 113 if (DEBUG) { 114 final Long localVersion = (local.mValues == null) ? null : local.mValues 115 .getAsLong(RawContacts.VERSION); 116 final Long remoteVersion = remote.mValues.getAsLong(RawContacts.VERSION); 117 Log.d(TAG, "Re-parenting from original version " + remoteVersion + " to " 118 + localVersion); 119 } 120 121 // Create values if needed, and merge "after" changes 122 local.mValues = ValuesDelta.mergeAfter(local.mValues, remote.mValues); 123 124 // Find matching local entry for each remote values, or create 125 for (ArrayList<ValuesDelta> mimeEntries : remote.mEntries.values()) { 126 for (ValuesDelta remoteEntry : mimeEntries) { 127 final Long childId = remoteEntry.getId(); 128 129 // Find or create local match and merge 130 final ValuesDelta localEntry = local.getEntry(childId); 131 final ValuesDelta merged = ValuesDelta.mergeAfter(localEntry, remoteEntry); 132 133 if (localEntry == null && merged != null) { 134 // No local entry before, so insert 135 local.addEntry(merged); 136 } 137 } 138 } 139 140 return local; 141 } 142 getValues()143 public ValuesDelta getValues() { 144 return mValues; 145 } 146 isContactInsert()147 public boolean isContactInsert() { 148 return mValues.isInsert(); 149 } 150 151 /** 152 * Get the {@link ValuesDelta} child marked as {@link Data#IS_PRIMARY}, 153 * which may return null when no entry exists. 154 */ getPrimaryEntry(String mimeType)155 public ValuesDelta getPrimaryEntry(String mimeType) { 156 final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false); 157 if (mimeEntries == null) return null; 158 159 for (ValuesDelta entry : mimeEntries) { 160 if (entry.isPrimary()) { 161 return entry; 162 } 163 } 164 165 // When no direct primary, return something 166 return mimeEntries.size() > 0 ? mimeEntries.get(0) : null; 167 } 168 169 /** 170 * calls {@link #getSuperPrimaryEntry(String, boolean)} with true 171 * @see #getSuperPrimaryEntry(String, boolean) 172 */ getSuperPrimaryEntry(String mimeType)173 public ValuesDelta getSuperPrimaryEntry(String mimeType) { 174 return getSuperPrimaryEntry(mimeType, true); 175 } 176 177 /** 178 * Returns the super-primary entry for the given mime type 179 * @param forceSelection if true, will try to return some value even if a super-primary 180 * doesn't exist (may be a primary, or just a random item 181 * @return 182 */ getSuperPrimaryEntry(String mimeType, boolean forceSelection)183 public ValuesDelta getSuperPrimaryEntry(String mimeType, boolean forceSelection) { 184 final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false); 185 if (mimeEntries == null) return null; 186 187 ValuesDelta primary = null; 188 for (ValuesDelta entry : mimeEntries) { 189 if (entry.isSuperPrimary()) { 190 return entry; 191 } else if (entry.isPrimary()) { 192 primary = entry; 193 } 194 } 195 196 if (!forceSelection) { 197 return null; 198 } 199 200 // When no direct super primary, return something 201 if (primary != null) { 202 return primary; 203 } 204 return mimeEntries.size() > 0 ? mimeEntries.get(0) : null; 205 } 206 207 /** 208 * Return the AccountType that this raw-contact belongs to. 209 */ getRawContactAccountType(Context context)210 public AccountType getRawContactAccountType(Context context) { 211 ContentValues entityValues = getValues().getCompleteValues(); 212 String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE); 213 String dataSet = entityValues.getAsString(RawContacts.DATA_SET); 214 return AccountTypeManager.getInstance(context).getAccountType(type, dataSet); 215 } 216 getRawContactId()217 public Long getRawContactId() { 218 return getValues().getAsLong(RawContacts._ID); 219 } 220 getAccountName()221 public String getAccountName() { 222 return getValues().getAsString(RawContacts.ACCOUNT_NAME); 223 } 224 getAccountType()225 public String getAccountType() { 226 return getValues().getAsString(RawContacts.ACCOUNT_TYPE); 227 } 228 getDataSet()229 public String getDataSet() { 230 return getValues().getAsString(RawContacts.DATA_SET); 231 } 232 getAccountType(AccountTypeManager manager)233 public AccountType getAccountType(AccountTypeManager manager) { 234 return manager.getAccountType(getAccountType(), getDataSet()); 235 } 236 getAccountWithDataSet()237 public AccountWithDataSet getAccountWithDataSet() { 238 return new AccountWithDataSet(getAccountName(), getAccountType(), getDataSet()); 239 } 240 isVisible()241 public boolean isVisible() { 242 return getValues().isVisible(); 243 } 244 245 /** 246 * Return the list of child {@link ValuesDelta} from our optimized map, 247 * creating the list if requested. 248 */ getMimeEntries(String mimeType, boolean lazyCreate)249 private ArrayList<ValuesDelta> getMimeEntries(String mimeType, boolean lazyCreate) { 250 ArrayList<ValuesDelta> mimeEntries = mEntries.get(mimeType); 251 if (mimeEntries == null && lazyCreate) { 252 mimeEntries = Lists.newArrayList(); 253 mEntries.put(mimeType, mimeEntries); 254 } 255 return mimeEntries; 256 } 257 getMimeEntries(String mimeType)258 public ArrayList<ValuesDelta> getMimeEntries(String mimeType) { 259 return getMimeEntries(mimeType, false); 260 } 261 getMimeEntriesCount(String mimeType, boolean onlyVisible)262 public int getMimeEntriesCount(String mimeType, boolean onlyVisible) { 263 final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType); 264 if (mimeEntries == null) return 0; 265 266 int count = 0; 267 for (ValuesDelta child : mimeEntries) { 268 // Skip deleted items when requesting only visible 269 if (onlyVisible && !child.isVisible()) continue; 270 count++; 271 } 272 return count; 273 } 274 hasMimeEntries(String mimeType)275 public boolean hasMimeEntries(String mimeType) { 276 return mEntries.containsKey(mimeType); 277 } 278 addEntry(ValuesDelta entry)279 public ValuesDelta addEntry(ValuesDelta entry) { 280 final String mimeType = entry.getMimetype(); 281 getMimeEntries(mimeType, true).add(entry); 282 return entry; 283 } 284 getContentValues()285 public ArrayList<ContentValues> getContentValues() { 286 ArrayList<ContentValues> values = Lists.newArrayList(); 287 for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) { 288 for (ValuesDelta entry : mimeEntries) { 289 if (!entry.isDelete()) { 290 values.add(entry.getCompleteValues()); 291 } 292 } 293 } 294 return values; 295 } 296 297 /** 298 * Find entry with the given {@link BaseColumns#_ID} value. 299 */ getEntry(Long childId)300 public ValuesDelta getEntry(Long childId) { 301 if (childId == null) { 302 // Requesting an "insert" entry, which has no "before" 303 return null; 304 } 305 306 // Search all children for requested entry 307 for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) { 308 for (ValuesDelta entry : mimeEntries) { 309 if (childId.equals(entry.getId())) { 310 return entry; 311 } 312 } 313 } 314 return null; 315 } 316 317 /** 318 * Return the total number of {@link ValuesDelta} contained. 319 */ getEntryCount(boolean onlyVisible)320 public int getEntryCount(boolean onlyVisible) { 321 int count = 0; 322 for (String mimeType : mEntries.keySet()) { 323 count += getMimeEntriesCount(mimeType, onlyVisible); 324 } 325 return count; 326 } 327 328 @Override equals(Object object)329 public boolean equals(Object object) { 330 if (object instanceof RawContactDelta) { 331 final RawContactDelta other = (RawContactDelta)object; 332 333 // Equality failed if parent values different 334 if (!other.mValues.equals(mValues)) return false; 335 336 for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) { 337 for (ValuesDelta child : mimeEntries) { 338 // Equality failed if any children unmatched 339 if (!other.containsEntry(child)) return false; 340 } 341 } 342 343 // Passed all tests, so equal 344 return true; 345 } 346 return false; 347 } 348 containsEntry(ValuesDelta entry)349 private boolean containsEntry(ValuesDelta entry) { 350 for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) { 351 for (ValuesDelta child : mimeEntries) { 352 // Contained if we find any child that matches 353 if (child.equals(entry)) return true; 354 } 355 } 356 return false; 357 } 358 359 /** 360 * Mark this entire object deleted, including any {@link ValuesDelta}. 361 */ markDeleted()362 public void markDeleted() { 363 this.mValues.markDeleted(); 364 for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) { 365 for (ValuesDelta child : mimeEntries) { 366 child.markDeleted(); 367 } 368 } 369 } 370 371 @Override toString()372 public String toString() { 373 final StringBuilder builder = new StringBuilder(); 374 builder.append("\n("); 375 builder.append("Uri="); 376 builder.append(mContactsQueryUri); 377 builder.append(", Values="); 378 builder.append(mValues != null ? mValues.toString() : "null"); 379 builder.append(", Entries={"); 380 for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) { 381 for (ValuesDelta child : mimeEntries) { 382 builder.append("\n\t"); 383 child.toString(builder); 384 } 385 } 386 builder.append("\n})\n"); 387 return builder.toString(); 388 } 389 390 /** 391 * Consider building the given {@link ContentProviderOperation.Builder} and 392 * appending it to the given list, which only happens if builder is valid. 393 */ possibleAdd(ArrayList<ContentProviderOperation> diff, ContentProviderOperation.Builder builder)394 private void possibleAdd(ArrayList<ContentProviderOperation> diff, 395 ContentProviderOperation.Builder builder) { 396 if (builder != null) { 397 diff.add(builder.build()); 398 } 399 } 400 401 /** 402 * For compatibility purpose, this method is copied from {@link #possibleAdd} and takes 403 * BuilderWrapper and an ArrayList of CPOWrapper as parameters. 404 */ possibleAddWrapper(ArrayList<CPOWrapper> diff, BuilderWrapper bw)405 private void possibleAddWrapper(ArrayList<CPOWrapper> diff, BuilderWrapper bw) { 406 if (bw != null && bw.getBuilder() != null) { 407 diff.add(new CPOWrapper(bw.getBuilder().build(), bw.getType())); 408 } 409 } 410 411 /** 412 * Build a list of {@link ContentProviderOperation} that will assert any 413 * "before" state hasn't changed. This is maintained separately so that all 414 * asserts can take place before any updates occur. 415 */ buildAssert(ArrayList<ContentProviderOperation> buildInto)416 public void buildAssert(ArrayList<ContentProviderOperation> buildInto) { 417 final Builder builder = buildAssertHelper(); 418 if (builder != null) { 419 buildInto.add(builder.build()); 420 } 421 } 422 423 /** 424 * For compatibility purpose, this method is copied from {@link #buildAssert} and takes an 425 * ArrayList of CPOWrapper as parameter. 426 */ buildAssertWrapper(ArrayList<CPOWrapper> buildInto)427 public void buildAssertWrapper(ArrayList<CPOWrapper> buildInto) { 428 final Builder builder = buildAssertHelper(); 429 if (builder != null) { 430 buildInto.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_ASSERT)); 431 } 432 } 433 buildAssertHelper()434 private Builder buildAssertHelper() { 435 final boolean isContactInsert = mValues.isInsert(); 436 ContentProviderOperation.Builder builder = null; 437 if (!isContactInsert) { 438 // Assert version is consistent while persisting changes 439 final Long beforeId = mValues.getId(); 440 final Long beforeVersion = mValues.getAsLong(RawContacts.VERSION); 441 if (beforeId == null || beforeVersion == null) return builder; 442 builder = ContentProviderOperation.newAssertQuery(mContactsQueryUri); 443 builder.withSelection(RawContacts._ID + "=" + beforeId, null); 444 builder.withValue(RawContacts.VERSION, beforeVersion); 445 } 446 return builder; 447 } 448 449 /** 450 * Build a list of {@link ContentProviderOperation} that will transform the 451 * current "before" {@link Entity} state into the modified state which this 452 * {@link RawContactDelta} represents. 453 */ buildDiff(ArrayList<ContentProviderOperation> buildInto)454 public void buildDiff(ArrayList<ContentProviderOperation> buildInto) { 455 final int firstIndex = buildInto.size(); 456 457 final boolean isContactInsert = mValues.isInsert(); 458 final boolean isContactDelete = mValues.isDelete(); 459 final boolean isContactUpdate = !isContactInsert && !isContactDelete; 460 461 final Long beforeId = mValues.getId(); 462 463 Builder builder; 464 465 if (isContactInsert) { 466 // TODO: for now simply disabling aggregation when a new contact is 467 // created on the phone. In the future, will show aggregation suggestions 468 // after saving the contact. 469 mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED); 470 } 471 472 // Build possible operation at Contact level 473 builder = mValues.buildDiff(mContactsQueryUri); 474 possibleAdd(buildInto, builder); 475 476 // Build operations for all children 477 for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) { 478 for (ValuesDelta child : mimeEntries) { 479 // Ignore children if parent was deleted 480 if (isContactDelete) continue; 481 482 // Use the profile data URI if the contact is the profile. 483 if (mContactsQueryUri.equals(Profile.CONTENT_RAW_CONTACTS_URI)) { 484 builder = child.buildDiff(Uri.withAppendedPath(Profile.CONTENT_URI, 485 RawContacts.Data.CONTENT_DIRECTORY)); 486 } else { 487 builder = child.buildDiff(Data.CONTENT_URI); 488 } 489 490 if (child.isInsert()) { 491 if (isContactInsert) { 492 // Parent is brand new insert, so back-reference _id 493 builder.withValueBackReference(Data.RAW_CONTACT_ID, firstIndex); 494 } else { 495 // Inserting under existing, so fill with known _id 496 builder.withValue(Data.RAW_CONTACT_ID, beforeId); 497 } 498 } else if (isContactInsert && builder != null) { 499 // Child must be insert when Contact insert 500 throw new IllegalArgumentException("When parent insert, child must be also"); 501 } 502 possibleAdd(buildInto, builder); 503 } 504 } 505 506 final boolean addedOperations = buildInto.size() > firstIndex; 507 if (addedOperations && isContactUpdate) { 508 // Suspend aggregation while persisting updates 509 builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_SUSPENDED); 510 buildInto.add(firstIndex, builder.build()); 511 512 // Restore aggregation mode as last operation 513 builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_DEFAULT); 514 buildInto.add(builder.build()); 515 } else if (isContactInsert) { 516 // Restore aggregation mode as last operation 517 builder = ContentProviderOperation.newUpdate(mContactsQueryUri); 518 builder.withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT); 519 builder.withSelection(RawContacts._ID + "=?", new String[1]); 520 builder.withSelectionBackReference(0, firstIndex); 521 buildInto.add(builder.build()); 522 } 523 } 524 525 /** 526 * For compatibility purpose, this method is copied from {@link #buildDiff} and takes an 527 * ArrayList of CPOWrapper as parameter. 528 */ buildDiffWrapper(ArrayList<CPOWrapper> buildInto)529 public void buildDiffWrapper(ArrayList<CPOWrapper> buildInto) { 530 final int firstIndex = buildInto.size(); 531 532 final boolean isContactInsert = mValues.isInsert(); 533 final boolean isContactDelete = mValues.isDelete(); 534 final boolean isContactUpdate = !isContactInsert && !isContactDelete; 535 536 final Long beforeId = mValues.getId(); 537 538 if (isContactInsert) { 539 // TODO: for now simply disabling aggregation when a new contact is 540 // created on the phone. In the future, will show aggregation suggestions 541 // after saving the contact. 542 mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED); 543 } 544 545 // Build possible operation at Contact level 546 BuilderWrapper bw = mValues.buildDiffWrapper(mContactsQueryUri); 547 possibleAddWrapper(buildInto, bw); 548 549 // Build operations for all children 550 for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) { 551 for (ValuesDelta child : mimeEntries) { 552 // Ignore children if parent was deleted 553 if (isContactDelete) continue; 554 555 // Use the profile data URI if the contact is the profile. 556 if (mContactsQueryUri.equals(Profile.CONTENT_RAW_CONTACTS_URI)) { 557 bw = child.buildDiffWrapper(Uri.withAppendedPath(Profile.CONTENT_URI, 558 RawContacts.Data.CONTENT_DIRECTORY)); 559 } else { 560 bw = child.buildDiffWrapper(Data.CONTENT_URI); 561 } 562 563 if (child.isInsert()) { 564 if (isContactInsert) { 565 // Parent is brand new insert, so back-reference _id 566 bw.getBuilder().withValueBackReference(Data.RAW_CONTACT_ID, firstIndex); 567 } else { 568 // Inserting under existing, so fill with known _id 569 bw.getBuilder().withValue(Data.RAW_CONTACT_ID, beforeId); 570 } 571 } else if (isContactInsert && bw != null && bw.getBuilder() != null) { 572 // Child must be insert when Contact insert 573 throw new IllegalArgumentException("When parent insert, child must be also"); 574 } 575 possibleAddWrapper(buildInto, bw); 576 } 577 } 578 579 final boolean addedOperations = buildInto.size() > firstIndex; 580 if (addedOperations && isContactUpdate) { 581 // Suspend aggregation while persisting updates 582 Builder builder = 583 buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_SUSPENDED); 584 buildInto.add(firstIndex, new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE)); 585 586 // Restore aggregation mode as last operation 587 builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_DEFAULT); 588 buildInto.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE)); 589 } else if (isContactInsert) { 590 // Restore aggregation mode as last operation 591 Builder builder = ContentProviderOperation.newUpdate(mContactsQueryUri); 592 builder.withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT); 593 builder.withSelection(RawContacts._ID + "=?", new String[1]); 594 builder.withSelectionBackReference(0, firstIndex); 595 buildInto.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE)); 596 } 597 } 598 599 /** 600 * Build a {@link ContentProviderOperation} that changes 601 * {@link RawContacts#AGGREGATION_MODE} to the given value. 602 */ buildSetAggregationMode(Long beforeId, int mode)603 protected Builder buildSetAggregationMode(Long beforeId, int mode) { 604 Builder builder = ContentProviderOperation.newUpdate(mContactsQueryUri); 605 builder.withValue(RawContacts.AGGREGATION_MODE, mode); 606 builder.withSelection(RawContacts._ID + "=" + beforeId, null); 607 return builder; 608 } 609 610 /** {@inheritDoc} */ describeContents()611 public int describeContents() { 612 // Nothing special about this parcel 613 return 0; 614 } 615 616 /** {@inheritDoc} */ writeToParcel(Parcel dest, int flags)617 public void writeToParcel(Parcel dest, int flags) { 618 final int size = this.getEntryCount(false); 619 dest.writeInt(size); 620 dest.writeParcelable(mValues, flags); 621 dest.writeParcelable(mContactsQueryUri, flags); 622 for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) { 623 for (ValuesDelta child : mimeEntries) { 624 dest.writeParcelable(child, flags); 625 } 626 } 627 } 628 readFromParcel(Parcel source)629 public void readFromParcel(Parcel source) { 630 final ClassLoader loader = getClass().getClassLoader(); 631 final int size = source.readInt(); 632 mValues = source.<ValuesDelta> readParcelable(loader); 633 mContactsQueryUri = source.<Uri> readParcelable(loader); 634 for (int i = 0; i < size; i++) { 635 final ValuesDelta child = source.<ValuesDelta> readParcelable(loader); 636 this.addEntry(child); 637 } 638 } 639 640 /** 641 * Used to set the query URI to the profile URI to store profiles. 642 */ setProfileQueryUri()643 public void setProfileQueryUri() { 644 mContactsQueryUri = Profile.CONTENT_RAW_CONTACTS_URI; 645 } 646 647 public static final Parcelable.Creator<RawContactDelta> CREATOR = 648 new Parcelable.Creator<RawContactDelta>() { 649 public RawContactDelta createFromParcel(Parcel in) { 650 final RawContactDelta state = new RawContactDelta(); 651 state.readFromParcel(in); 652 return state; 653 } 654 655 public RawContactDelta[] newArray(int size) { 656 return new RawContactDelta[size]; 657 } 658 }; 659 660 } 661