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