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