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.common.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.common.compat.CompatUtils; 33 import com.android.contacts.common.model.AccountTypeManager; 34 import com.android.contacts.common.model.BuilderWrapper; 35 import com.android.contacts.common.model.CPOWrapper; 36 import com.android.contacts.common.model.ValuesDelta; 37 import com.android.contacts.common.model.account.AccountType; 38 import com.android.contacts.common.testing.NeededForTesting; 39 import com.google.common.collect.Lists; 40 import com.google.common.collect.Maps; 41 42 import java.util.ArrayList; 43 import java.util.HashMap; 44 45 /** 46 * Contains a {@link RawContact} and records any modifications separately so the 47 * original {@link RawContact} can be swapped out with a newer version and the 48 * changes still cleanly applied. 49 * <p> 50 * One benefit of this approach is that we can build changes entirely on an 51 * empty {@link RawContact}, which then becomes an insert {@link RawContacts} case. 52 * <p> 53 * When applying modifications over an {@link RawContact}, we try finding the 54 * original {@link Data#_ID} rows where the modifications took place. If those 55 * rows are missing from the new {@link RawContact}, we know the original data must 56 * be deleted, but to preserve the user modifications we treat as an insert. 57 */ 58 public class RawContactDelta implements Parcelable { 59 // TODO: optimize by using contentvalues pool, since we allocate so many of them 60 61 private static final String TAG = "EntityDelta"; 62 private static final boolean LOGV = false; 63 64 /** 65 * Direct values from {@link Entity#getEntityValues()}. 66 */ 67 private ValuesDelta mValues; 68 69 /** 70 * URI used for contacts queries, by default it is set to query raw contacts. 71 * It can be set to query the profile's raw contact(s). 72 */ 73 private Uri mContactsQueryUri = RawContacts.CONTENT_URI; 74 75 /** 76 * Internal map of children values from {@link Entity#getSubValues()}, which 77 * we store here sorted into {@link Data#MIMETYPE} bins. 78 */ 79 private final HashMap<String, ArrayList<ValuesDelta>> mEntries = Maps.newHashMap(); 80 81 public RawContactDelta() { 82 } 83 84 public RawContactDelta(ValuesDelta values) { 85 mValues = values; 86 } 87 88 /** 89 * Build an {@link RawContactDelta} using the given {@link RawContact} as a 90 * starting point; the "before" snapshot. 91 */ 92 public static RawContactDelta fromBefore(RawContact before) { 93 final RawContactDelta rawContactDelta = new RawContactDelta(); 94 rawContactDelta.mValues = ValuesDelta.fromBefore(before.getValues()); 95 rawContactDelta.mValues.setIdColumn(RawContacts._ID); 96 for (final ContentValues values : before.getContentValues()) { 97 rawContactDelta.addEntry(ValuesDelta.fromBefore(values)); 98 } 99 return rawContactDelta; 100 } 101 102 /** 103 * Merge the "after" values from the given {@link RawContactDelta} onto the 104 * "before" state represented by this {@link RawContactDelta}, discarding any 105 * existing "after" states. This is typically used when re-parenting changes 106 * onto an updated {@link Entity}. 107 */ 108 public static RawContactDelta mergeAfter(RawContactDelta local, RawContactDelta remote) { 109 // Bail early if trying to merge delete with missing local 110 final ValuesDelta remoteValues = remote.mValues; 111 if (local == null && (remoteValues.isDelete() || remoteValues.isTransient())) return null; 112 113 // Create local version if none exists yet 114 if (local == null) local = new RawContactDelta(); 115 116 if (LOGV) { 117 final Long localVersion = (local.mValues == null) ? null : local.mValues 118 .getAsLong(RawContacts.VERSION); 119 final Long remoteVersion = remote.mValues.getAsLong(RawContacts.VERSION); 120 Log.d(TAG, "Re-parenting from original version " + remoteVersion + " to " 121 + localVersion); 122 } 123 124 // Create values if needed, and merge "after" changes 125 local.mValues = ValuesDelta.mergeAfter(local.mValues, remote.mValues); 126 127 // Find matching local entry for each remote values, or create 128 for (ArrayList<ValuesDelta> mimeEntries : remote.mEntries.values()) { 129 for (ValuesDelta remoteEntry : mimeEntries) { 130 final Long childId = remoteEntry.getId(); 131 132 // Find or create local match and merge 133 final ValuesDelta localEntry = local.getEntry(childId); 134 final ValuesDelta merged = ValuesDelta.mergeAfter(localEntry, remoteEntry); 135 136 if (localEntry == null && merged != null) { 137 // No local entry before, so insert 138 local.addEntry(merged); 139 } 140 } 141 } 142 143 return local; 144 } 145 146 public ValuesDelta getValues() { 147 return mValues; 148 } 149 150 public boolean isContactInsert() { 151 return mValues.isInsert(); 152 } 153 154 /** 155 * Get the {@link ValuesDelta} child marked as {@link Data#IS_PRIMARY}, 156 * which may return null when no entry exists. 157 */ 158 public ValuesDelta getPrimaryEntry(String mimeType) { 159 final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false); 160 if (mimeEntries == null) return null; 161 162 for (ValuesDelta entry : mimeEntries) { 163 if (entry.isPrimary()) { 164 return entry; 165 } 166 } 167 168 // When no direct primary, return something 169 return mimeEntries.size() > 0 ? mimeEntries.get(0) : null; 170 } 171 172 /** 173 * calls {@link #getSuperPrimaryEntry(String, boolean)} with true 174 * @see #getSuperPrimaryEntry(String, boolean) 175 */ 176 public ValuesDelta getSuperPrimaryEntry(String mimeType) { 177 return getSuperPrimaryEntry(mimeType, true); 178 } 179 180 /** 181 * Returns the super-primary entry for the given mime type 182 * @param forceSelection if true, will try to return some value even if a super-primary 183 * doesn't exist (may be a primary, or just a random item 184 * @return 185 */ 186 @NeededForTesting 187 public ValuesDelta getSuperPrimaryEntry(String mimeType, boolean forceSelection) { 188 final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false); 189 if (mimeEntries == null) return null; 190 191 ValuesDelta primary = null; 192 for (ValuesDelta entry : mimeEntries) { 193 if (entry.isSuperPrimary()) { 194 return entry; 195 } else if (entry.isPrimary()) { 196 primary = entry; 197 } 198 } 199 200 if (!forceSelection) { 201 return null; 202 } 203 204 // When no direct super primary, return something 205 if (primary != null) { 206 return primary; 207 } 208 return mimeEntries.size() > 0 ? mimeEntries.get(0) : null; 209 } 210 211 /** 212 * Return the AccountType that this raw-contact belongs to. 213 */ 214 public AccountType getRawContactAccountType(Context context) { 215 ContentValues entityValues = getValues().getCompleteValues(); 216 String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE); 217 String dataSet = entityValues.getAsString(RawContacts.DATA_SET); 218 return AccountTypeManager.getInstance(context).getAccountType(type, dataSet); 219 } 220 221 public Long getRawContactId() { 222 return getValues().getAsLong(RawContacts._ID); 223 } 224 225 public String getAccountName() { 226 return getValues().getAsString(RawContacts.ACCOUNT_NAME); 227 } 228 229 public String getAccountType() { 230 return getValues().getAsString(RawContacts.ACCOUNT_TYPE); 231 } 232 233 public String getDataSet() { 234 return getValues().getAsString(RawContacts.DATA_SET); 235 } 236 237 public AccountType getAccountType(AccountTypeManager manager) { 238 return manager.getAccountType(getAccountType(), getDataSet()); 239 } 240 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 */ 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 258 public ArrayList<ValuesDelta> getMimeEntries(String mimeType) { 259 return getMimeEntries(mimeType, false); 260 } 261 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 275 public boolean hasMimeEntries(String mimeType) { 276 return mEntries.containsKey(mimeType); 277 } 278 279 public ValuesDelta addEntry(ValuesDelta entry) { 280 final String mimeType = entry.getMimetype(); 281 getMimeEntries(mimeType, true).add(entry); 282 return entry; 283 } 284 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 */ 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 */ 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 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 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 */ 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 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 */ 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 */ 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 */ 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 */ 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 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 */ 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 */ 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 */ 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} */ 611 public int describeContents() { 612 // Nothing special about this parcel 613 return 0; 614 } 615 616 /** {@inheritDoc} */ 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 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 */ 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