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
17package com.android.contacts.model;
18
19import android.content.ContentProviderOperation;
20import android.content.ContentProviderOperation.Builder;
21import android.content.ContentResolver;
22import android.content.Context;
23import android.content.Entity;
24import android.content.EntityIterator;
25import android.net.Uri;
26import android.os.Parcel;
27import android.os.Parcelable;
28import android.provider.ContactsContract.AggregationExceptions;
29import android.provider.ContactsContract.Contacts;
30import android.provider.ContactsContract.RawContacts;
31import android.util.Log;
32
33import com.android.contacts.model.RawContactDelta.ValuesDelta;
34import com.google.common.collect.Lists;
35
36import java.util.ArrayList;
37import java.util.Arrays;
38import 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 */
45public 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
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     */
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     */
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     */
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     */
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     */
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     * */
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. */
353    public RawContactDelta getFirstWritableRawContact(Context context) {
354        final int index = indexOfFirstWritableRawContact(context);
355        return (index == -1) ? null : get(index);
356    }
357
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     */
385    public void markRawContactsForSplitting() {
386        mSplitRawContacts = true;
387    }
388
389    public boolean isMarkedForSplitting() {
390        return mSplitRawContacts;
391    }
392
393    public void setJoinWithRawContacts(long[] rawContactIds) {
394        mJoinWithRawContactIds = rawContactIds;
395    }
396
397    public boolean isMarkedForJoining() {
398        return mJoinWithRawContactIds != null && mJoinWithRawContactIds.length > 0;
399    }
400
401    /** {@inheritDoc} */
402    @Override
403    public int describeContents() {
404        // Nothing special about this parcel
405        return 0;
406    }
407
408    /** {@inheritDoc} */
409    @Override
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")
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
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