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