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.loaderapp.model;
18
19import com.android.loaderapp.model.EntityDelta.ValuesDelta;
20import com.google.android.collect.Lists;
21
22import android.content.ContentProviderOperation;
23import android.content.ContentResolver;
24import android.content.Entity;
25import android.content.EntityIterator;
26import android.content.ContentProviderOperation.Builder;
27import android.os.Parcel;
28import android.os.Parcelable;
29import android.os.RemoteException;
30import android.provider.ContactsContract.AggregationExceptions;
31import android.provider.ContactsContract.Contacts;
32import android.provider.ContactsContract.RawContacts;
33import android.provider.ContactsContract.RawContactsEntity;
34
35import java.util.ArrayList;
36
37/**
38 * Container for multiple {@link EntityDelta} objects, usually when editing
39 * together as an entire aggregate. Provides convenience methods for parceling
40 * and applying another {@link EntitySet} over it.
41 */
42public class EntitySet extends ArrayList<EntityDelta> implements Parcelable {
43    private boolean mSplitRawContacts;
44
45    private EntitySet() {
46    }
47
48    /**
49     * Create an {@link EntitySet} that contains the given {@link EntityDelta},
50     * usually when inserting a new {@link Contacts} entry.
51     */
52    public static EntitySet fromSingle(EntityDelta delta) {
53        final EntitySet state = new EntitySet();
54        state.add(delta);
55        return state;
56    }
57
58    /**
59     * Create an {@link EntitySet} 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     */
63    public static EntitySet fromQuery(ContentResolver resolver, String selection,
64            String[] selectionArgs, String sortOrder) {
65        EntityIterator iterator = RawContacts.newEntityIterator(resolver.query(
66                RawContactsEntity.CONTENT_URI, null, selection, selectionArgs,
67                sortOrder));
68        try {
69            final EntitySet state = new EntitySet();
70            // Perform background query to pull contact details
71            while (iterator.hasNext()) {
72                // Read all contacts into local deltas to prepare for edits
73                final Entity before = iterator.next();
74                final EntityDelta entity = EntityDelta.fromBefore(before);
75                state.add(entity);
76            }
77            return state;
78        } finally {
79            iterator.close();
80        }
81    }
82
83    /**
84     * Merge the "after" values from the given {@link EntitySet}, discarding any
85     * previous "after" states. This is typically used when re-parenting user
86     * edits onto an updated {@link EntitySet}.
87     */
88    public static EntitySet mergeAfter(EntitySet local, EntitySet remote) {
89        if (local == null) local = new EntitySet();
90
91        // For each entity in the remote set, try matching over existing
92        for (EntityDelta remoteEntity : remote) {
93            final Long rawContactId = remoteEntity.getValues().getId();
94
95            // Find or create local match and merge
96            final EntityDelta localEntity = local.getByRawContactId(rawContactId);
97            final EntityDelta merged = EntityDelta.mergeAfter(localEntity, remoteEntity);
98
99            if (localEntity == null && merged != null) {
100                // No local entry before, so insert
101                local.add(merged);
102            }
103        }
104
105        return local;
106    }
107
108    /**
109     * Build a list of {@link ContentProviderOperation} that will transform all
110     * the "before" {@link Entity} states into the modified state which all
111     * {@link EntityDelta} objects represent. This method specifically creates
112     * any {@link AggregationExceptions} rules needed to groups edits together.
113     */
114    public ArrayList<ContentProviderOperation> buildDiff() {
115        final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
116
117        final long rawContactId = this.findRawContactId();
118        int firstInsertRow = -1;
119
120        // First pass enforces versions remain consistent
121        for (EntityDelta delta : this) {
122            delta.buildAssert(diff);
123        }
124
125        final int assertMark = diff.size();
126        int backRefs[] = new int[size()];
127
128        int rawContactIndex = 0;
129
130        // Second pass builds actual operations
131        for (EntityDelta delta : this) {
132            final int firstBatch = diff.size();
133            backRefs[rawContactIndex++] = firstBatch;
134            delta.buildDiff(diff);
135
136            // Only create rules for inserts
137            if (!delta.isContactInsert()) continue;
138
139            // If we are going to split all contacts, there is no point in first combining them
140            if (mSplitRawContacts) continue;
141
142            if (rawContactId != -1) {
143                // Has existing contact, so bind to it strongly
144                final Builder builder = beginKeepTogether();
145                builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId);
146                builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, firstBatch);
147                diff.add(builder.build());
148
149            } else if (firstInsertRow == -1) {
150                // First insert case, so record row
151                firstInsertRow = firstBatch;
152
153            } else {
154                // Additional insert case, so point at first insert
155                final Builder builder = beginKeepTogether();
156                builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID1, firstInsertRow);
157                builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, firstBatch);
158                diff.add(builder.build());
159            }
160        }
161
162        if (mSplitRawContacts) {
163            buildSplitContactDiff(diff, backRefs);
164        }
165
166        // No real changes if only left with asserts
167        if (diff.size() == assertMark) {
168            diff.clear();
169        }
170
171        return diff;
172    }
173
174    /**
175     * Start building a {@link ContentProviderOperation} that will keep two
176     * {@link RawContacts} together.
177     */
178    protected Builder beginKeepTogether() {
179        final Builder builder = ContentProviderOperation
180                .newUpdate(AggregationExceptions.CONTENT_URI);
181        builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
182        return builder;
183    }
184
185    /**
186     * Builds {@link AggregationExceptions} to split all constituent raw contacts into
187     * separate contacts.
188     */
189    private void buildSplitContactDiff(final ArrayList<ContentProviderOperation> diff,
190            int[] backRefs) {
191        int count = size();
192        for (int i = 0; i < count; i++) {
193            for (int j = 0; j < count; j++) {
194                if (i != j) {
195                    buildSplitContactDiff(diff, i, j, backRefs);
196                }
197            }
198        }
199    }
200
201    /**
202     * Construct a {@link AggregationExceptions#TYPE_KEEP_SEPARATE}.
203     */
204    private void buildSplitContactDiff(ArrayList<ContentProviderOperation> diff, int index1,
205            int index2, int[] backRefs) {
206        Builder builder =
207                ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
208        builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_SEPARATE);
209
210        Long rawContactId1 = get(index1).getValues().getAsLong(RawContacts._ID);
211        if (rawContactId1 != null && rawContactId1 >= 0) {
212            builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
213        } else {
214            builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID1, backRefs[index1]);
215        }
216
217        Long rawContactId2 = get(index2).getValues().getAsLong(RawContacts._ID);
218        if (rawContactId2 != null && rawContactId2 >= 0) {
219            builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
220        } else {
221            builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, backRefs[index2]);
222        }
223        diff.add(builder.build());
224    }
225
226    /**
227     * Search all contained {@link EntityDelta} for the first one with an
228     * existing {@link RawContacts#_ID} value. Usually used when creating
229     * {@link AggregationExceptions} during an update.
230     */
231    public long findRawContactId() {
232        for (EntityDelta delta : this) {
233            final Long rawContactId = delta.getValues().getAsLong(RawContacts._ID);
234            if (rawContactId != null && rawContactId >= 0) {
235                return rawContactId;
236            }
237        }
238        return -1;
239    }
240
241    /**
242     * Find {@link RawContacts#_ID} of the requested {@link EntityDelta}.
243     */
244    public Long getRawContactId(int index) {
245        if (index >= 0 && index < this.size()) {
246            final EntityDelta delta = this.get(index);
247            final ValuesDelta values = delta.getValues();
248            if (values.isVisible()) {
249                return values.getAsLong(RawContacts._ID);
250            }
251        }
252        return null;
253    }
254
255    public EntityDelta getByRawContactId(Long rawContactId) {
256        final int index = this.indexOfRawContactId(rawContactId);
257        return (index == -1) ? null : this.get(index);
258    }
259
260    /**
261     * Find index of given {@link RawContacts#_ID} when present.
262     */
263    public int indexOfRawContactId(Long rawContactId) {
264        if (rawContactId == null) return -1;
265        final int size = this.size();
266        for (int i = 0; i < size; i++) {
267            final Long currentId = getRawContactId(i);
268            if (rawContactId.equals(currentId)) {
269                return i;
270            }
271        }
272        return -1;
273    }
274
275    public ValuesDelta getSuperPrimaryEntry(final String mimeType) {
276        ValuesDelta primary = null;
277        ValuesDelta randomEntry = null;
278        for (EntityDelta delta : this) {
279            final ArrayList<ValuesDelta> mimeEntries = delta.getMimeEntries(mimeType);
280            if (mimeEntries == null) return null;
281
282            for (ValuesDelta entry : mimeEntries) {
283                if (entry.isSuperPrimary()) {
284                    return entry;
285                } else if (primary == null && entry.isPrimary()) {
286                    primary = entry;
287                } else if (randomEntry == null) {
288                    randomEntry = entry;
289                }
290            }
291        }
292        // When no direct super primary, return something
293        if (primary != null) {
294            return primary;
295        }
296        return randomEntry;
297    }
298
299    public void splitRawContacts() {
300        mSplitRawContacts = true;
301    }
302
303    /** {@inheritDoc} */
304    public int describeContents() {
305        // Nothing special about this parcel
306        return 0;
307    }
308
309    /** {@inheritDoc} */
310    public void writeToParcel(Parcel dest, int flags) {
311        final int size = this.size();
312        dest.writeInt(size);
313        for (EntityDelta delta : this) {
314            dest.writeParcelable(delta, flags);
315        }
316    }
317
318    public void readFromParcel(Parcel source) {
319        final ClassLoader loader = getClass().getClassLoader();
320        final int size = source.readInt();
321        for (int i = 0; i < size; i++) {
322            this.add(source.<EntityDelta> readParcelable(loader));
323        }
324    }
325
326    public static final Parcelable.Creator<EntitySet> CREATOR = new Parcelable.Creator<EntitySet>() {
327        public EntitySet createFromParcel(Parcel in) {
328            final EntitySet state = new EntitySet();
329            state.readFromParcel(in);
330            return state;
331        }
332
333        public EntitySet[] newArray(int size) {
334            return new EntitySet[size];
335        }
336    };
337}
338