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.ContentResolver; 22 import android.content.Context; 23 import android.content.Entity; 24 import android.content.EntityIterator; 25 import android.net.Uri; 26 import android.os.Parcel; 27 import android.os.Parcelable; 28 import android.provider.ContactsContract.AggregationExceptions; 29 import android.provider.ContactsContract.Contacts; 30 import android.provider.ContactsContract.RawContacts; 31 import android.util.Log; 32 33 import com.android.contacts.common.compat.CompatUtils; 34 import com.android.contacts.common.model.CPOWrapper; 35 import com.android.contacts.common.model.ValuesDelta; 36 37 import com.google.common.collect.Lists; 38 39 import java.util.ArrayList; 40 import java.util.Arrays; 41 import java.util.Iterator; 42 43 /** 44 * Container for multiple {@link RawContactDelta} objects, usually when editing 45 * together as an entire aggregate. Provides convenience methods for parceling 46 * and applying another {@link RawContactDeltaList} over it. 47 */ 48 public class RawContactDeltaList extends ArrayList<RawContactDelta> implements Parcelable { 49 private static final String TAG = RawContactDeltaList.class.getSimpleName(); 50 private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE); 51 52 private boolean mSplitRawContacts; 53 private long[] mJoinWithRawContactIds; 54 RawContactDeltaList()55 public RawContactDeltaList() { 56 } 57 58 /** 59 * Create an {@link RawContactDeltaList} based on {@link Contacts} specified by the 60 * given query parameters. This closes the {@link EntityIterator} when 61 * finished, so it doesn't subscribe to updates. 62 */ fromQuery(Uri entityUri, ContentResolver resolver, String selection, String[] selectionArgs, String sortOrder)63 public static RawContactDeltaList fromQuery(Uri entityUri, ContentResolver resolver, 64 String selection, String[] selectionArgs, String sortOrder) { 65 final EntityIterator iterator = RawContacts.newEntityIterator( 66 resolver.query(entityUri, null, selection, selectionArgs, sortOrder)); 67 try { 68 return fromIterator(iterator); 69 } finally { 70 iterator.close(); 71 } 72 } 73 74 /** 75 * Create an {@link RawContactDeltaList} that contains the entities of the Iterator as before 76 * values. This function can be passed an iterator of Entity objects or an iterator of 77 * RawContact objects. 78 */ fromIterator(Iterator<?> iterator)79 public static RawContactDeltaList fromIterator(Iterator<?> iterator) { 80 final RawContactDeltaList state = new RawContactDeltaList(); 81 state.addAll(iterator); 82 return state; 83 } 84 addAll(Iterator<?> iterator)85 public void addAll(Iterator<?> iterator) { 86 // Perform background query to pull contact details 87 while (iterator.hasNext()) { 88 // Read all contacts into local deltas to prepare for edits 89 Object nextObject = iterator.next(); 90 final RawContact before = nextObject instanceof Entity 91 ? RawContact.createFrom((Entity) nextObject) 92 : (RawContact) nextObject; 93 final RawContactDelta rawContactDelta = RawContactDelta.fromBefore(before); 94 add(rawContactDelta); 95 } 96 } 97 98 /** 99 * Merge the "after" values from the given {@link RawContactDeltaList}, discarding any 100 * previous "after" states. This is typically used when re-parenting user 101 * edits onto an updated {@link RawContactDeltaList}. 102 */ mergeAfter(RawContactDeltaList local, RawContactDeltaList remote)103 public static RawContactDeltaList mergeAfter(RawContactDeltaList local, 104 RawContactDeltaList remote) { 105 if (local == null) local = new RawContactDeltaList(); 106 107 // For each entity in the remote set, try matching over existing 108 for (RawContactDelta remoteEntity : remote) { 109 final Long rawContactId = remoteEntity.getValues().getId(); 110 111 // Find or create local match and merge 112 final RawContactDelta localEntity = local.getByRawContactId(rawContactId); 113 final RawContactDelta merged = RawContactDelta.mergeAfter(localEntity, remoteEntity); 114 115 if (localEntity == null && merged != null) { 116 // No local entry before, so insert 117 local.add(merged); 118 } 119 } 120 121 return local; 122 } 123 124 /** 125 * Build a list of {@link ContentProviderOperation} that will transform all 126 * the "before" {@link Entity} states into the modified state which all 127 * {@link RawContactDelta} objects represent. This method specifically creates 128 * any {@link AggregationExceptions} rules needed to groups edits together. 129 */ buildDiff()130 public ArrayList<ContentProviderOperation> buildDiff() { 131 if (VERBOSE_LOGGING) { 132 Log.v(TAG, "buildDiff: list=" + toString()); 133 } 134 final ArrayList<ContentProviderOperation> diff = Lists.newArrayList(); 135 136 final long rawContactId = this.findRawContactId(); 137 int firstInsertRow = -1; 138 139 // First pass enforces versions remain consistent 140 for (RawContactDelta delta : this) { 141 delta.buildAssert(diff); 142 } 143 144 final int assertMark = diff.size(); 145 int backRefs[] = new int[size()]; 146 147 int rawContactIndex = 0; 148 149 // Second pass builds actual operations 150 for (RawContactDelta delta : this) { 151 final int firstBatch = diff.size(); 152 final boolean isInsert = delta.isContactInsert(); 153 backRefs[rawContactIndex++] = isInsert ? firstBatch : -1; 154 155 delta.buildDiff(diff); 156 157 // If the user chose to join with some other existing raw contact(s) at save time, 158 // add aggregation exceptions for all those raw contacts. 159 if (mJoinWithRawContactIds != null) { 160 for (Long joinedRawContactId : mJoinWithRawContactIds) { 161 final Builder builder = beginKeepTogether(); 162 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, joinedRawContactId); 163 if (rawContactId != -1) { 164 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId); 165 } else { 166 builder.withValueBackReference( 167 AggregationExceptions.RAW_CONTACT_ID2, firstBatch); 168 } 169 diff.add(builder.build()); 170 } 171 } 172 173 // Only create rules for inserts 174 if (!isInsert) continue; 175 176 // If we are going to split all contacts, there is no point in first combining them 177 if (mSplitRawContacts) continue; 178 179 if (rawContactId != -1) { 180 // Has existing contact, so bind to it strongly 181 final Builder builder = beginKeepTogether(); 182 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId); 183 builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, firstBatch); 184 diff.add(builder.build()); 185 186 } else if (firstInsertRow == -1) { 187 // First insert case, so record row 188 firstInsertRow = firstBatch; 189 190 } else { 191 // Additional insert case, so point at first insert 192 final Builder builder = beginKeepTogether(); 193 builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID1, 194 firstInsertRow); 195 builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, firstBatch); 196 diff.add(builder.build()); 197 } 198 } 199 200 if (mSplitRawContacts) { 201 buildSplitContactDiff(diff, backRefs); 202 } 203 204 // No real changes if only left with asserts 205 if (diff.size() == assertMark) { 206 diff.clear(); 207 } 208 if (VERBOSE_LOGGING) { 209 Log.v(TAG, "buildDiff: ops=" + diffToString(diff)); 210 } 211 return diff; 212 } 213 214 /** 215 * For compatibility purpose, this method is copied from {@link #buildDiff} and returns an 216 * ArrayList of CPOWrapper. 217 */ buildDiffWrapper()218 public ArrayList<CPOWrapper> buildDiffWrapper() { 219 if (VERBOSE_LOGGING) { 220 Log.v(TAG, "buildDiffWrapper: list=" + toString()); 221 } 222 final ArrayList<CPOWrapper> diffWrapper = Lists.newArrayList(); 223 224 final long rawContactId = this.findRawContactId(); 225 int firstInsertRow = -1; 226 227 // First pass enforces versions remain consistent 228 for (RawContactDelta delta : this) { 229 delta.buildAssertWrapper(diffWrapper); 230 } 231 232 final int assertMark = diffWrapper.size(); 233 int backRefs[] = new int[size()]; 234 235 int rawContactIndex = 0; 236 237 // Second pass builds actual operations 238 for (RawContactDelta delta : this) { 239 final int firstBatch = diffWrapper.size(); 240 final boolean isInsert = delta.isContactInsert(); 241 backRefs[rawContactIndex++] = isInsert ? firstBatch : -1; 242 243 delta.buildDiffWrapper(diffWrapper); 244 245 // If the user chose to join with some other existing raw contact(s) at save time, 246 // add aggregation exceptions for all those raw contacts. 247 if (mJoinWithRawContactIds != null) { 248 for (Long joinedRawContactId : mJoinWithRawContactIds) { 249 final Builder builder = beginKeepTogether(); 250 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, joinedRawContactId); 251 if (rawContactId != -1) { 252 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId); 253 } else { 254 builder.withValueBackReference( 255 AggregationExceptions.RAW_CONTACT_ID2, firstBatch); 256 } 257 diffWrapper.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE)); 258 } 259 } 260 261 // Only create rules for inserts 262 if (!isInsert) continue; 263 264 // If we are going to split all contacts, there is no point in first combining them 265 if (mSplitRawContacts) continue; 266 267 if (rawContactId != -1) { 268 // Has existing contact, so bind to it strongly 269 final Builder builder = beginKeepTogether(); 270 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId); 271 builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, firstBatch); 272 diffWrapper.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE)); 273 274 } else if (firstInsertRow == -1) { 275 // First insert case, so record row 276 firstInsertRow = firstBatch; 277 278 } else { 279 // Additional insert case, so point at first insert 280 final Builder builder = beginKeepTogether(); 281 builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID1, 282 firstInsertRow); 283 builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, firstBatch); 284 diffWrapper.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE)); 285 } 286 } 287 288 if (mSplitRawContacts) { 289 buildSplitContactDiffWrapper(diffWrapper, backRefs); 290 } 291 292 // No real changes if only left with asserts 293 if (diffWrapper.size() == assertMark) { 294 diffWrapper.clear(); 295 } 296 if (VERBOSE_LOGGING) { 297 Log.v(TAG, "buildDiff: ops=" + diffToStringWrapper(diffWrapper)); 298 } 299 return diffWrapper; 300 } 301 diffToString(ArrayList<ContentProviderOperation> ops)302 private static String diffToString(ArrayList<ContentProviderOperation> ops) { 303 final StringBuilder sb = new StringBuilder(); 304 sb.append("[\n"); 305 for (ContentProviderOperation op : ops) { 306 sb.append(op.toString()); 307 sb.append(",\n"); 308 } 309 sb.append("]\n"); 310 return sb.toString(); 311 } 312 313 /** 314 * For compatibility purpose. 315 */ diffToStringWrapper(ArrayList<CPOWrapper> cpoWrappers)316 private static String diffToStringWrapper(ArrayList<CPOWrapper> cpoWrappers) { 317 ArrayList<ContentProviderOperation> ops = Lists.newArrayList(); 318 for (CPOWrapper cpoWrapper : cpoWrappers) { 319 ops.add(cpoWrapper.getOperation()); 320 } 321 return diffToString(ops); 322 } 323 324 /** 325 * Start building a {@link ContentProviderOperation} that will keep two 326 * {@link RawContacts} together. 327 */ beginKeepTogether()328 protected Builder beginKeepTogether() { 329 final Builder builder = ContentProviderOperation 330 .newUpdate(AggregationExceptions.CONTENT_URI); 331 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER); 332 return builder; 333 } 334 335 /** 336 * Builds {@link AggregationExceptions} to split all constituent raw contacts into 337 * separate contacts. 338 */ buildSplitContactDiff(final ArrayList<ContentProviderOperation> diff, int[] backRefs)339 private void buildSplitContactDiff(final ArrayList<ContentProviderOperation> diff, 340 int[] backRefs) { 341 final int count = size(); 342 for (int i = 0; i < count; i++) { 343 for (int j = 0; j < count; j++) { 344 if (i == j) { 345 continue; 346 } 347 final Builder builder = buildSplitContactDiffHelper(i, j, backRefs); 348 if (builder != null) { 349 diff.add(builder.build()); 350 } 351 } 352 } 353 } 354 355 /** 356 * For compatibility purpose, this method is copied from {@link #buildSplitContactDiff} and 357 * takes an ArrayList of CPOWrapper as parameter. 358 */ buildSplitContactDiffWrapper(final ArrayList<CPOWrapper> diff, int[] backRefs)359 private void buildSplitContactDiffWrapper(final ArrayList<CPOWrapper> diff, int[] backRefs) { 360 final int count = size(); 361 for (int i = 0; i < count; i++) { 362 for (int j = 0; j < count; j++) { 363 if (i == j) { 364 continue; 365 } 366 final Builder builder = buildSplitContactDiffHelper(i, j, backRefs); 367 if (builder != null) { 368 diff.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE)); 369 } 370 } 371 } 372 } 373 buildSplitContactDiffHelper(int index1, int index2, int[] backRefs)374 private Builder buildSplitContactDiffHelper(int index1, int index2, int[] backRefs) { 375 final Builder builder = 376 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI); 377 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_SEPARATE); 378 379 Long rawContactId1 = get(index1).getValues().getAsLong(RawContacts._ID); 380 int backRef1 = backRefs[index1]; 381 if (rawContactId1 != null && rawContactId1 >= 0) { 382 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1); 383 } else if (backRef1 >= 0) { 384 builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID1, backRef1); 385 } else { 386 return null; 387 } 388 389 Long rawContactId2 = get(index2).getValues().getAsLong(RawContacts._ID); 390 int backRef2 = backRefs[index2]; 391 if (rawContactId2 != null && rawContactId2 >= 0) { 392 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2); 393 } else if (backRef2 >= 0) { 394 builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, backRef2); 395 } else { 396 return null; 397 } 398 return builder; 399 } 400 401 /** 402 * Search all contained {@link RawContactDelta} for the first one with an 403 * existing {@link RawContacts#_ID} value. Usually used when creating 404 * {@link AggregationExceptions} during an update. 405 */ findRawContactId()406 public long findRawContactId() { 407 for (RawContactDelta delta : this) { 408 final Long rawContactId = delta.getValues().getAsLong(RawContacts._ID); 409 if (rawContactId != null && rawContactId >= 0) { 410 return rawContactId; 411 } 412 } 413 return -1; 414 } 415 416 /** 417 * Find {@link RawContacts#_ID} of the requested {@link RawContactDelta}. 418 */ getRawContactId(int index)419 public Long getRawContactId(int index) { 420 if (index >= 0 && index < this.size()) { 421 final RawContactDelta delta = this.get(index); 422 final ValuesDelta values = delta.getValues(); 423 if (values.isVisible()) { 424 return values.getAsLong(RawContacts._ID); 425 } 426 } 427 return null; 428 } 429 430 /** 431 * Find the raw-contact (an {@link RawContactDelta}) with the specified ID. 432 */ getByRawContactId(Long rawContactId)433 public RawContactDelta getByRawContactId(Long rawContactId) { 434 final int index = this.indexOfRawContactId(rawContactId); 435 return (index == -1) ? null : this.get(index); 436 } 437 438 /** 439 * Find index of given {@link RawContacts#_ID} when present. 440 */ indexOfRawContactId(Long rawContactId)441 public int indexOfRawContactId(Long rawContactId) { 442 if (rawContactId == null) return -1; 443 final int size = this.size(); 444 for (int i = 0; i < size; i++) { 445 final Long currentId = getRawContactId(i); 446 if (rawContactId.equals(currentId)) { 447 return i; 448 } 449 } 450 return -1; 451 } 452 453 /** 454 * Return the index of the first RawContactDelta corresponding to a writable raw-contact, or -1. 455 * */ indexOfFirstWritableRawContact(Context context)456 public int indexOfFirstWritableRawContact(Context context) { 457 // Find the first writable entity. 458 int entityIndex = 0; 459 for (RawContactDelta delta : this) { 460 if (delta.getRawContactAccountType(context).areContactsWritable()) return entityIndex; 461 entityIndex++; 462 } 463 return -1; 464 } 465 466 /** Return the first RawContactDelta corresponding to a writable raw-contact, or null. */ getFirstWritableRawContact(Context context)467 public RawContactDelta getFirstWritableRawContact(Context context) { 468 final int index = indexOfFirstWritableRawContact(context); 469 return (index == -1) ? null : get(index); 470 } 471 getSuperPrimaryEntry(final String mimeType)472 public ValuesDelta getSuperPrimaryEntry(final String mimeType) { 473 ValuesDelta primary = null; 474 ValuesDelta randomEntry = null; 475 for (RawContactDelta delta : this) { 476 final ArrayList<ValuesDelta> mimeEntries = delta.getMimeEntries(mimeType); 477 if (mimeEntries == null) return null; 478 479 for (ValuesDelta entry : mimeEntries) { 480 if (entry.isSuperPrimary()) { 481 return entry; 482 } else if (primary == null && entry.isPrimary()) { 483 primary = entry; 484 } else if (randomEntry == null) { 485 randomEntry = entry; 486 } 487 } 488 } 489 // When no direct super primary, return something 490 if (primary != null) { 491 return primary; 492 } 493 return randomEntry; 494 } 495 496 /** 497 * Sets a flag that will split ("explode") the raw_contacts into seperate contacts 498 */ markRawContactsForSplitting()499 public void markRawContactsForSplitting() { 500 mSplitRawContacts = true; 501 } 502 isMarkedForSplitting()503 public boolean isMarkedForSplitting() { 504 return mSplitRawContacts; 505 } 506 setJoinWithRawContacts(long[] rawContactIds)507 public void setJoinWithRawContacts(long[] rawContactIds) { 508 mJoinWithRawContactIds = rawContactIds; 509 } 510 isMarkedForJoining()511 public boolean isMarkedForJoining() { 512 return mJoinWithRawContactIds != null && mJoinWithRawContactIds.length > 0; 513 } 514 515 /** {@inheritDoc} */ 516 @Override describeContents()517 public int describeContents() { 518 // Nothing special about this parcel 519 return 0; 520 } 521 522 /** {@inheritDoc} */ 523 @Override writeToParcel(Parcel dest, int flags)524 public void writeToParcel(Parcel dest, int flags) { 525 final int size = this.size(); 526 dest.writeInt(size); 527 for (RawContactDelta delta : this) { 528 dest.writeParcelable(delta, flags); 529 } 530 dest.writeLongArray(mJoinWithRawContactIds); 531 dest.writeInt(mSplitRawContacts ? 1 : 0); 532 } 533 534 @SuppressWarnings("unchecked") readFromParcel(Parcel source)535 public void readFromParcel(Parcel source) { 536 final ClassLoader loader = getClass().getClassLoader(); 537 final int size = source.readInt(); 538 for (int i = 0; i < size; i++) { 539 this.add(source.<RawContactDelta> readParcelable(loader)); 540 } 541 mJoinWithRawContactIds = source.createLongArray(); 542 mSplitRawContacts = source.readInt() != 0; 543 } 544 545 public static final Parcelable.Creator<RawContactDeltaList> CREATOR = 546 new Parcelable.Creator<RawContactDeltaList>() { 547 @Override 548 public RawContactDeltaList createFromParcel(Parcel in) { 549 final RawContactDeltaList state = new RawContactDeltaList(); 550 state.readFromParcel(in); 551 return state; 552 } 553 554 @Override 555 public RawContactDeltaList[] newArray(int size) { 556 return new RawContactDeltaList[size]; 557 } 558 }; 559 560 @Override toString()561 public String toString() { 562 StringBuilder sb = new StringBuilder(); 563 sb.append("("); 564 sb.append("Split="); 565 sb.append(mSplitRawContacts); 566 sb.append(", Join=["); 567 sb.append(Arrays.toString(mJoinWithRawContactIds)); 568 sb.append("], Values="); 569 sb.append(super.toString()); 570 sb.append(")"); 571 return sb.toString(); 572 } 573 } 574