• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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