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.ContentValues;
22import android.content.Context;
23import android.net.Uri;
24import android.os.Parcel;
25import android.os.Parcelable;
26import android.provider.BaseColumns;
27import android.provider.ContactsContract.Data;
28import android.provider.ContactsContract.Profile;
29import android.provider.ContactsContract.RawContacts;
30import android.util.Log;
31
32import com.android.contacts.compat.CompatUtils;
33import com.android.contacts.model.account.AccountType;
34import com.android.contacts.model.account.AccountWithDataSet;
35
36import com.google.common.collect.Lists;
37import com.google.common.collect.Maps;
38
39import java.util.ArrayList;
40import java.util.HashMap;
41
42/**
43 * Contains a {@link RawContact} and records any modifications separately so the
44 * original {@link RawContact} can be swapped out with a newer version and the
45 * changes still cleanly applied.
46 * <p>
47 * One benefit of this approach is that we can build changes entirely on an
48 * empty {@link RawContact}, which then becomes an insert {@link RawContacts} case.
49 * <p>
50 * When applying modifications over an {@link RawContact}, we try finding the
51 * original {@link Data#_ID} rows where the modifications took place. If those
52 * rows are missing from the new {@link RawContact}, we know the original data must
53 * be deleted, but to preserve the user modifications we treat as an insert.
54 */
55public class RawContactDelta implements Parcelable {
56    // TODO: optimize by using contentvalues pool, since we allocate so many of them
57
58    private static final String TAG = "EntityDelta";
59    private static final boolean DEBUG = false;
60
61    /**
62     * Direct values from {@link Entity#getEntityValues()}.
63     */
64    private ValuesDelta mValues;
65
66    /**
67     * URI used for contacts queries, by default it is set to query raw contacts.
68     * It can be set to query the profile's raw contact(s).
69     */
70    private Uri mContactsQueryUri = RawContacts.CONTENT_URI;
71
72    /**
73     * Internal map of children values from {@link Entity#getSubValues()}, which
74     * we store here sorted into {@link Data#MIMETYPE} bins.
75     */
76    private final HashMap<String, ArrayList<ValuesDelta>> mEntries = Maps.newHashMap();
77
78    public RawContactDelta() {
79    }
80
81    public RawContactDelta(ValuesDelta values) {
82        mValues = values;
83    }
84
85    /**
86     * Build an {@link RawContactDelta} using the given {@link RawContact} as a
87     * starting point; the "before" snapshot.
88     */
89    public static RawContactDelta fromBefore(RawContact before) {
90        final RawContactDelta rawContactDelta = new RawContactDelta();
91        rawContactDelta.mValues = ValuesDelta.fromBefore(before.getValues());
92        rawContactDelta.mValues.setIdColumn(RawContacts._ID);
93        for (final ContentValues values : before.getContentValues()) {
94            rawContactDelta.addEntry(ValuesDelta.fromBefore(values));
95        }
96        return rawContactDelta;
97    }
98
99    /**
100     * Merge the "after" values from the given {@link RawContactDelta} onto the
101     * "before" state represented by this {@link RawContactDelta}, discarding any
102     * existing "after" states. This is typically used when re-parenting changes
103     * onto an updated {@link Entity}.
104     */
105    public static RawContactDelta mergeAfter(RawContactDelta local, RawContactDelta remote) {
106        // Bail early if trying to merge delete with missing local
107        final ValuesDelta remoteValues = remote.mValues;
108        if (local == null && (remoteValues.isDelete() || remoteValues.isTransient())) return null;
109
110        // Create local version if none exists yet
111        if (local == null) local = new RawContactDelta();
112
113        if (DEBUG) {
114            final Long localVersion = (local.mValues == null) ? null : local.mValues
115                    .getAsLong(RawContacts.VERSION);
116            final Long remoteVersion = remote.mValues.getAsLong(RawContacts.VERSION);
117            Log.d(TAG, "Re-parenting from original version " + remoteVersion + " to "
118                    + localVersion);
119        }
120
121        // Create values if needed, and merge "after" changes
122        local.mValues = ValuesDelta.mergeAfter(local.mValues, remote.mValues);
123
124        // Find matching local entry for each remote values, or create
125        for (ArrayList<ValuesDelta> mimeEntries : remote.mEntries.values()) {
126            for (ValuesDelta remoteEntry : mimeEntries) {
127                final Long childId = remoteEntry.getId();
128
129                // Find or create local match and merge
130                final ValuesDelta localEntry = local.getEntry(childId);
131                final ValuesDelta merged = ValuesDelta.mergeAfter(localEntry, remoteEntry);
132
133                if (localEntry == null && merged != null) {
134                    // No local entry before, so insert
135                    local.addEntry(merged);
136                }
137            }
138        }
139
140        return local;
141    }
142
143    public ValuesDelta getValues() {
144        return mValues;
145    }
146
147    public boolean isContactInsert() {
148        return mValues.isInsert();
149    }
150
151    /**
152     * Get the {@link ValuesDelta} child marked as {@link Data#IS_PRIMARY},
153     * which may return null when no entry exists.
154     */
155    public ValuesDelta getPrimaryEntry(String mimeType) {
156        final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false);
157        if (mimeEntries == null) return null;
158
159        for (ValuesDelta entry : mimeEntries) {
160            if (entry.isPrimary()) {
161                return entry;
162            }
163        }
164
165        // When no direct primary, return something
166        return mimeEntries.size() > 0 ? mimeEntries.get(0) : null;
167    }
168
169    /**
170     * calls {@link #getSuperPrimaryEntry(String, boolean)} with true
171     * @see #getSuperPrimaryEntry(String, boolean)
172     */
173    public ValuesDelta getSuperPrimaryEntry(String mimeType) {
174        return getSuperPrimaryEntry(mimeType, true);
175    }
176
177    /**
178     * Returns the super-primary entry for the given mime type
179     * @param forceSelection if true, will try to return some value even if a super-primary
180     *     doesn't exist (may be a primary, or just a random item
181     * @return
182     */
183    public ValuesDelta getSuperPrimaryEntry(String mimeType, boolean forceSelection) {
184        final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false);
185        if (mimeEntries == null) return null;
186
187        ValuesDelta primary = null;
188        for (ValuesDelta entry : mimeEntries) {
189            if (entry.isSuperPrimary()) {
190                return entry;
191            } else if (entry.isPrimary()) {
192                primary = entry;
193            }
194        }
195
196        if (!forceSelection) {
197            return null;
198        }
199
200        // When no direct super primary, return something
201        if (primary != null) {
202            return primary;
203        }
204        return mimeEntries.size() > 0 ? mimeEntries.get(0) : null;
205    }
206
207    /**
208     * Return the AccountType that this raw-contact belongs to.
209     */
210    public AccountType getRawContactAccountType(Context context) {
211        ContentValues entityValues = getValues().getCompleteValues();
212        String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
213        String dataSet = entityValues.getAsString(RawContacts.DATA_SET);
214        return AccountTypeManager.getInstance(context).getAccountType(type, dataSet);
215    }
216
217    public Long getRawContactId() {
218        return getValues().getAsLong(RawContacts._ID);
219    }
220
221    public String getAccountName() {
222        return getValues().getAsString(RawContacts.ACCOUNT_NAME);
223    }
224
225    public String getAccountType() {
226        return getValues().getAsString(RawContacts.ACCOUNT_TYPE);
227    }
228
229    public String getDataSet() {
230        return getValues().getAsString(RawContacts.DATA_SET);
231    }
232
233    public AccountType getAccountType(AccountTypeManager manager) {
234        return manager.getAccountType(getAccountType(), getDataSet());
235    }
236
237    public AccountWithDataSet getAccountWithDataSet() {
238        return new AccountWithDataSet(getAccountName(), getAccountType(), getDataSet());
239    }
240
241    public boolean isVisible() {
242        return getValues().isVisible();
243    }
244
245    /**
246     * Return the list of child {@link ValuesDelta} from our optimized map,
247     * creating the list if requested.
248     */
249    private ArrayList<ValuesDelta> getMimeEntries(String mimeType, boolean lazyCreate) {
250        ArrayList<ValuesDelta> mimeEntries = mEntries.get(mimeType);
251        if (mimeEntries == null && lazyCreate) {
252            mimeEntries = Lists.newArrayList();
253            mEntries.put(mimeType, mimeEntries);
254        }
255        return mimeEntries;
256    }
257
258    public ArrayList<ValuesDelta> getMimeEntries(String mimeType) {
259        return getMimeEntries(mimeType, false);
260    }
261
262    public int getMimeEntriesCount(String mimeType, boolean onlyVisible) {
263        final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType);
264        if (mimeEntries == null) return 0;
265
266        int count = 0;
267        for (ValuesDelta child : mimeEntries) {
268            // Skip deleted items when requesting only visible
269            if (onlyVisible && !child.isVisible()) continue;
270            count++;
271        }
272        return count;
273    }
274
275    public boolean hasMimeEntries(String mimeType) {
276        return mEntries.containsKey(mimeType);
277    }
278
279    public ValuesDelta addEntry(ValuesDelta entry) {
280        final String mimeType = entry.getMimetype();
281        getMimeEntries(mimeType, true).add(entry);
282        return entry;
283    }
284
285    public ArrayList<ContentValues> getContentValues() {
286        ArrayList<ContentValues> values = Lists.newArrayList();
287        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
288            for (ValuesDelta entry : mimeEntries) {
289                if (!entry.isDelete()) {
290                    values.add(entry.getCompleteValues());
291                }
292            }
293        }
294        return values;
295    }
296
297    /**
298     * Find entry with the given {@link BaseColumns#_ID} value.
299     */
300    public ValuesDelta getEntry(Long childId) {
301        if (childId == null) {
302            // Requesting an "insert" entry, which has no "before"
303            return null;
304        }
305
306        // Search all children for requested entry
307        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
308            for (ValuesDelta entry : mimeEntries) {
309                if (childId.equals(entry.getId())) {
310                    return entry;
311                }
312            }
313        }
314        return null;
315    }
316
317    /**
318     * Return the total number of {@link ValuesDelta} contained.
319     */
320    public int getEntryCount(boolean onlyVisible) {
321        int count = 0;
322        for (String mimeType : mEntries.keySet()) {
323            count += getMimeEntriesCount(mimeType, onlyVisible);
324        }
325        return count;
326    }
327
328    @Override
329    public boolean equals(Object object) {
330        if (object instanceof RawContactDelta) {
331            final RawContactDelta other = (RawContactDelta)object;
332
333            // Equality failed if parent values different
334            if (!other.mValues.equals(mValues)) return false;
335
336            for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
337                for (ValuesDelta child : mimeEntries) {
338                    // Equality failed if any children unmatched
339                    if (!other.containsEntry(child)) return false;
340                }
341            }
342
343            // Passed all tests, so equal
344            return true;
345        }
346        return false;
347    }
348
349    private boolean containsEntry(ValuesDelta entry) {
350        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
351            for (ValuesDelta child : mimeEntries) {
352                // Contained if we find any child that matches
353                if (child.equals(entry)) return true;
354            }
355        }
356        return false;
357    }
358
359    /**
360     * Mark this entire object deleted, including any {@link ValuesDelta}.
361     */
362    public void markDeleted() {
363        this.mValues.markDeleted();
364        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
365            for (ValuesDelta child : mimeEntries) {
366                child.markDeleted();
367            }
368        }
369    }
370
371    @Override
372    public String toString() {
373        final StringBuilder builder = new StringBuilder();
374        builder.append("\n(");
375        builder.append("Uri=");
376        builder.append(mContactsQueryUri);
377        builder.append(", Values=");
378        builder.append(mValues != null ? mValues.toString() : "null");
379        builder.append(", Entries={");
380        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
381            for (ValuesDelta child : mimeEntries) {
382                builder.append("\n\t");
383                child.toString(builder);
384            }
385        }
386        builder.append("\n})\n");
387        return builder.toString();
388    }
389
390    /**
391     * Consider building the given {@link ContentProviderOperation.Builder} and
392     * appending it to the given list, which only happens if builder is valid.
393     */
394    private void possibleAdd(ArrayList<ContentProviderOperation> diff,
395            ContentProviderOperation.Builder builder) {
396        if (builder != null) {
397            diff.add(builder.build());
398        }
399    }
400
401    /**
402     * For compatibility purpose, this method is copied from {@link #possibleAdd} and takes
403     * BuilderWrapper and an ArrayList of CPOWrapper as parameters.
404     */
405    private void possibleAddWrapper(ArrayList<CPOWrapper> diff, BuilderWrapper bw) {
406        if (bw != null && bw.getBuilder() != null) {
407            diff.add(new CPOWrapper(bw.getBuilder().build(), bw.getType()));
408        }
409    }
410
411    /**
412     * Build a list of {@link ContentProviderOperation} that will assert any
413     * "before" state hasn't changed. This is maintained separately so that all
414     * asserts can take place before any updates occur.
415     */
416    public void buildAssert(ArrayList<ContentProviderOperation> buildInto) {
417        final Builder builder = buildAssertHelper();
418        if (builder != null) {
419            buildInto.add(builder.build());
420        }
421    }
422
423    /**
424     * For compatibility purpose, this method is copied from {@link #buildAssert} and takes an
425     * ArrayList of CPOWrapper as parameter.
426     */
427    public void buildAssertWrapper(ArrayList<CPOWrapper> buildInto) {
428        final Builder builder = buildAssertHelper();
429        if (builder != null) {
430            buildInto.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_ASSERT));
431        }
432    }
433
434    private Builder buildAssertHelper() {
435        final boolean isContactInsert = mValues.isInsert();
436        ContentProviderOperation.Builder builder = null;
437        if (!isContactInsert) {
438            // Assert version is consistent while persisting changes
439            final Long beforeId = mValues.getId();
440            final Long beforeVersion = mValues.getAsLong(RawContacts.VERSION);
441            if (beforeId == null || beforeVersion == null) return builder;
442            builder = ContentProviderOperation.newAssertQuery(mContactsQueryUri);
443            builder.withSelection(RawContacts._ID + "=" + beforeId, null);
444            builder.withValue(RawContacts.VERSION, beforeVersion);
445        }
446        return builder;
447    }
448
449    /**
450     * Build a list of {@link ContentProviderOperation} that will transform the
451     * current "before" {@link Entity} state into the modified state which this
452     * {@link RawContactDelta} represents.
453     */
454    public void buildDiff(ArrayList<ContentProviderOperation> buildInto) {
455        final int firstIndex = buildInto.size();
456
457        final boolean isContactInsert = mValues.isInsert();
458        final boolean isContactDelete = mValues.isDelete();
459        final boolean isContactUpdate = !isContactInsert && !isContactDelete;
460
461        final Long beforeId = mValues.getId();
462
463        Builder builder;
464
465        if (isContactInsert) {
466            // TODO: for now simply disabling aggregation when a new contact is
467            // created on the phone.  In the future, will show aggregation suggestions
468            // after saving the contact.
469            mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED);
470        }
471
472        // Build possible operation at Contact level
473        builder = mValues.buildDiff(mContactsQueryUri);
474        possibleAdd(buildInto, builder);
475
476        // Build operations for all children
477        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
478            for (ValuesDelta child : mimeEntries) {
479                // Ignore children if parent was deleted
480                if (isContactDelete) continue;
481
482                // Use the profile data URI if the contact is the profile.
483                if (mContactsQueryUri.equals(Profile.CONTENT_RAW_CONTACTS_URI)) {
484                    builder = child.buildDiff(Uri.withAppendedPath(Profile.CONTENT_URI,
485                            RawContacts.Data.CONTENT_DIRECTORY));
486                } else {
487                    builder = child.buildDiff(Data.CONTENT_URI);
488                }
489
490                if (child.isInsert()) {
491                    if (isContactInsert) {
492                        // Parent is brand new insert, so back-reference _id
493                        builder.withValueBackReference(Data.RAW_CONTACT_ID, firstIndex);
494                    } else {
495                        // Inserting under existing, so fill with known _id
496                        builder.withValue(Data.RAW_CONTACT_ID, beforeId);
497                    }
498                } else if (isContactInsert && builder != null) {
499                    // Child must be insert when Contact insert
500                    throw new IllegalArgumentException("When parent insert, child must be also");
501                }
502                possibleAdd(buildInto, builder);
503            }
504        }
505
506        final boolean addedOperations = buildInto.size() > firstIndex;
507        if (addedOperations && isContactUpdate) {
508            // Suspend aggregation while persisting updates
509            builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_SUSPENDED);
510            buildInto.add(firstIndex, builder.build());
511
512            // Restore aggregation mode as last operation
513            builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_DEFAULT);
514            buildInto.add(builder.build());
515        } else if (isContactInsert) {
516            // Restore aggregation mode as last operation
517            builder = ContentProviderOperation.newUpdate(mContactsQueryUri);
518            builder.withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT);
519            builder.withSelection(RawContacts._ID + "=?", new String[1]);
520            builder.withSelectionBackReference(0, firstIndex);
521            buildInto.add(builder.build());
522        }
523    }
524
525    /**
526     * For compatibility purpose, this method is copied from {@link #buildDiff} and takes an
527     * ArrayList of CPOWrapper as parameter.
528     */
529    public void buildDiffWrapper(ArrayList<CPOWrapper> buildInto) {
530        final int firstIndex = buildInto.size();
531
532        final boolean isContactInsert = mValues.isInsert();
533        final boolean isContactDelete = mValues.isDelete();
534        final boolean isContactUpdate = !isContactInsert && !isContactDelete;
535
536        final Long beforeId = mValues.getId();
537
538        if (isContactInsert) {
539            // TODO: for now simply disabling aggregation when a new contact is
540            // created on the phone.  In the future, will show aggregation suggestions
541            // after saving the contact.
542            mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED);
543        }
544
545        // Build possible operation at Contact level
546        BuilderWrapper bw = mValues.buildDiffWrapper(mContactsQueryUri);
547        possibleAddWrapper(buildInto, bw);
548
549        // Build operations for all children
550        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
551            for (ValuesDelta child : mimeEntries) {
552                // Ignore children if parent was deleted
553                if (isContactDelete) continue;
554
555                // Use the profile data URI if the contact is the profile.
556                if (mContactsQueryUri.equals(Profile.CONTENT_RAW_CONTACTS_URI)) {
557                    bw = child.buildDiffWrapper(Uri.withAppendedPath(Profile.CONTENT_URI,
558                            RawContacts.Data.CONTENT_DIRECTORY));
559                } else {
560                    bw = child.buildDiffWrapper(Data.CONTENT_URI);
561                }
562
563                if (child.isInsert()) {
564                    if (isContactInsert) {
565                        // Parent is brand new insert, so back-reference _id
566                        bw.getBuilder().withValueBackReference(Data.RAW_CONTACT_ID, firstIndex);
567                    } else {
568                        // Inserting under existing, so fill with known _id
569                        bw.getBuilder().withValue(Data.RAW_CONTACT_ID, beforeId);
570                    }
571                } else if (isContactInsert && bw != null && bw.getBuilder() != null) {
572                    // Child must be insert when Contact insert
573                    throw new IllegalArgumentException("When parent insert, child must be also");
574                }
575                possibleAddWrapper(buildInto, bw);
576            }
577        }
578
579        final boolean addedOperations = buildInto.size() > firstIndex;
580        if (addedOperations && isContactUpdate) {
581            // Suspend aggregation while persisting updates
582            Builder builder =
583                    buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_SUSPENDED);
584            buildInto.add(firstIndex, new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE));
585
586            // Restore aggregation mode as last operation
587            builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_DEFAULT);
588            buildInto.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE));
589        } else if (isContactInsert) {
590            // Restore aggregation mode as last operation
591            Builder builder = ContentProviderOperation.newUpdate(mContactsQueryUri);
592            builder.withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT);
593            builder.withSelection(RawContacts._ID + "=?", new String[1]);
594            builder.withSelectionBackReference(0, firstIndex);
595            buildInto.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE));
596        }
597    }
598
599    /**
600     * Build a {@link ContentProviderOperation} that changes
601     * {@link RawContacts#AGGREGATION_MODE} to the given value.
602     */
603    protected Builder buildSetAggregationMode(Long beforeId, int mode) {
604        Builder builder = ContentProviderOperation.newUpdate(mContactsQueryUri);
605        builder.withValue(RawContacts.AGGREGATION_MODE, mode);
606        builder.withSelection(RawContacts._ID + "=" + beforeId, null);
607        return builder;
608    }
609
610    /** {@inheritDoc} */
611    public int describeContents() {
612        // Nothing special about this parcel
613        return 0;
614    }
615
616    /** {@inheritDoc} */
617    public void writeToParcel(Parcel dest, int flags) {
618        final int size = this.getEntryCount(false);
619        dest.writeInt(size);
620        dest.writeParcelable(mValues, flags);
621        dest.writeParcelable(mContactsQueryUri, flags);
622        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
623            for (ValuesDelta child : mimeEntries) {
624                dest.writeParcelable(child, flags);
625            }
626        }
627    }
628
629    public void readFromParcel(Parcel source) {
630        final ClassLoader loader = getClass().getClassLoader();
631        final int size = source.readInt();
632        mValues = source.<ValuesDelta> readParcelable(loader);
633        mContactsQueryUri = source.<Uri> readParcelable(loader);
634        for (int i = 0; i < size; i++) {
635            final ValuesDelta child = source.<ValuesDelta> readParcelable(loader);
636            this.addEntry(child);
637        }
638    }
639
640    /**
641     * Used to set the query URI to the profile URI to store profiles.
642     */
643    public void setProfileQueryUri() {
644        mContactsQueryUri = Profile.CONTENT_RAW_CONTACTS_URI;
645    }
646
647    public static final Parcelable.Creator<RawContactDelta> CREATOR =
648            new Parcelable.Creator<RawContactDelta>() {
649        public RawContactDelta createFromParcel(Parcel in) {
650            final RawContactDelta state = new RawContactDelta();
651            state.readFromParcel(in);
652            return state;
653        }
654
655        public RawContactDelta[] newArray(int size) {
656            return new RawContactDelta[size];
657        }
658    };
659
660}
661