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