• 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.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 
RawContactDeltaList()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      */
fromSingle(RawContactDelta delta)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      */
fromQuery(Uri entityUri, ContentResolver resolver, String selection, String[] selectionArgs, String sortOrder)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      */
fromIterator(Iterator<?> iterator)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      */
mergeAfter(RawContactDeltaList local, RawContactDeltaList remote)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      */
buildDiff()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 
diffToString(ArrayList<ContentProviderOperation> ops)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      */
beginKeepTogether()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      */
buildSplitContactDiff(final ArrayList<ContentProviderOperation> diff, int[] backRefs)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      */
buildSplitContactDiff(ArrayList<ContentProviderOperation> diff, int index1, int index2, int[] backRefs)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      */
findRawContactId()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      */
getRawContactId(int index)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      */
getByRawContactId(Long rawContactId)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      */
indexOfRawContactId(Long rawContactId)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      * */
indexOfFirstWritableRawContact(Context context)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. */
getFirstWritableRawContact(Context context)353     public RawContactDelta getFirstWritableRawContact(Context context) {
354         final int index = indexOfFirstWritableRawContact(context);
355         return (index == -1) ? null : get(index);
356     }
357 
getSuperPrimaryEntry(final String mimeType)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      */
markRawContactsForSplitting()385     public void markRawContactsForSplitting() {
386         mSplitRawContacts = true;
387     }
388 
isMarkedForSplitting()389     public boolean isMarkedForSplitting() {
390         return mSplitRawContacts;
391     }
392 
setJoinWithRawContacts(long[] rawContactIds)393     public void setJoinWithRawContacts(long[] rawContactIds) {
394         mJoinWithRawContactIds = rawContactIds;
395     }
396 
isMarkedForJoining()397     public boolean isMarkedForJoining() {
398         return mJoinWithRawContactIds != null && mJoinWithRawContactIds.length > 0;
399     }
400 
401     /** {@inheritDoc} */
402     @Override
describeContents()403     public int describeContents() {
404         // Nothing special about this parcel
405         return 0;
406     }
407 
408     /** {@inheritDoc} */
409     @Override
writeToParcel(Parcel dest, int flags)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")
readFromParcel(Parcel source)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
toString()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