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