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