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