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.common.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.common.model.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    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     */
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     */
76    public static RawContactDeltaList fromIterator(Iterator<?> iterator) {
77        final RawContactDeltaList state = new RawContactDeltaList();
78        state.addAll(iterator);
79        return state;
80    }
81
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     */
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     */
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     * */
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. */
347    public RawContactDelta getFirstWritableRawContact(Context context) {
348        final int index = indexOfFirstWritableRawContact(context);
349        return (index == -1) ? null : get(index);
350    }
351
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     */
379    public void markRawContactsForSplitting() {
380        mSplitRawContacts = true;
381    }
382
383    public boolean isMarkedForSplitting() {
384        return mSplitRawContacts;
385    }
386
387    public void setJoinWithRawContacts(long[] rawContactIds) {
388        mJoinWithRawContactIds = rawContactIds;
389    }
390
391    public boolean isMarkedForJoining() {
392        return mJoinWithRawContactIds != null && mJoinWithRawContactIds.length > 0;
393    }
394
395    /** {@inheritDoc} */
396    @Override
397    public int describeContents() {
398        // Nothing special about this parcel
399        return 0;
400    }
401
402    /** {@inheritDoc} */
403    @Override
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")
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
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