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