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