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.google.android.collect.Lists;
20import com.google.android.collect.Maps;
21import com.google.android.collect.Sets;
22
23import android.content.ContentProviderOperation;
24import android.content.ContentValues;
25import android.content.Entity;
26import android.content.ContentProviderOperation.Builder;
27import android.content.Entity.NamedContentValues;
28import android.net.Uri;
29import android.os.Parcel;
30import android.os.Parcelable;
31import android.provider.BaseColumns;
32import android.provider.ContactsContract.Data;
33import android.provider.ContactsContract.RawContacts;
34import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
35import android.util.Log;
36
37import java.util.ArrayList;
38import java.util.HashMap;
39import java.util.HashSet;
40import java.util.Map;
41import java.util.Set;
42
43/**
44 * Contains an {@link Entity} and records any modifications separately so the
45 * original {@link Entity} can be swapped out with a newer version and the
46 * changes still cleanly applied.
47 * <p>
48 * One benefit of this approach is that we can build changes entirely on an
49 * empty {@link Entity}, which then becomes an insert {@link RawContacts} case.
50 * <p>
51 * When applying modifications over an {@link Entity}, we try finding the
52 * original {@link Data#_ID} rows where the modifications took place. If those
53 * rows are missing from the new {@link Entity}, we know the original data must
54 * be deleted, but to preserve the user modifications we treat as an insert.
55 */
56public class EntityDelta implements Parcelable {
57    // TODO: optimize by using contentvalues pool, since we allocate so many of them
58
59    private static final String TAG = "EntityDelta";
60    private static final boolean LOGV = true;
61
62    /**
63     * Direct values from {@link Entity#getEntityValues()}.
64     */
65    private ValuesDelta mValues;
66
67    /**
68     * Internal map of children values from {@link Entity#getSubValues()}, which
69     * we store here sorted into {@link Data#MIMETYPE} bins.
70     */
71    private HashMap<String, ArrayList<ValuesDelta>> mEntries = Maps.newHashMap();
72
73    public EntityDelta() {
74    }
75
76    public EntityDelta(ValuesDelta values) {
77        mValues = values;
78    }
79
80    /**
81     * Build an {@link EntityDelta} using the given {@link Entity} as a
82     * starting point; the "before" snapshot.
83     */
84    public static EntityDelta fromBefore(Entity before) {
85        final EntityDelta entity = new EntityDelta();
86        entity.mValues = ValuesDelta.fromBefore(before.getEntityValues());
87        entity.mValues.setIdColumn(RawContacts._ID);
88        for (NamedContentValues namedValues : before.getSubValues()) {
89            entity.addEntry(ValuesDelta.fromBefore(namedValues.values));
90        }
91        return entity;
92    }
93
94    /**
95     * Merge the "after" values from the given {@link EntityDelta} onto the
96     * "before" state represented by this {@link EntityDelta}, discarding any
97     * existing "after" states. This is typically used when re-parenting changes
98     * onto an updated {@link Entity}.
99     */
100    public static EntityDelta mergeAfter(EntityDelta local, EntityDelta remote) {
101        // Bail early if trying to merge delete with missing local
102        final ValuesDelta remoteValues = remote.mValues;
103        if (local == null && (remoteValues.isDelete() || remoteValues.isTransient())) return null;
104
105        // Create local version if none exists yet
106        if (local == null) local = new EntityDelta();
107
108        if (LOGV) {
109            final Long localVersion = (local.mValues == null) ? null : local.mValues
110                    .getAsLong(RawContacts.VERSION);
111            final Long remoteVersion = remote.mValues.getAsLong(RawContacts.VERSION);
112            Log.d(TAG, "Re-parenting from original version " + remoteVersion + " to "
113                    + localVersion);
114        }
115
116        // Create values if needed, and merge "after" changes
117        local.mValues = ValuesDelta.mergeAfter(local.mValues, remote.mValues);
118
119        // Find matching local entry for each remote values, or create
120        for (ArrayList<ValuesDelta> mimeEntries : remote.mEntries.values()) {
121            for (ValuesDelta remoteEntry : mimeEntries) {
122                final Long childId = remoteEntry.getId();
123
124                // Find or create local match and merge
125                final ValuesDelta localEntry = local.getEntry(childId);
126                final ValuesDelta merged = ValuesDelta.mergeAfter(localEntry, remoteEntry);
127
128                if (localEntry == null && merged != null) {
129                    // No local entry before, so insert
130                    local.addEntry(merged);
131                }
132            }
133        }
134
135        return local;
136    }
137
138    public ValuesDelta getValues() {
139        return mValues;
140    }
141
142    public boolean isContactInsert() {
143        return mValues.isInsert();
144    }
145
146    /**
147     * Get the {@link ValuesDelta} child marked as {@link Data#IS_PRIMARY},
148     * which may return null when no entry exists.
149     */
150    public ValuesDelta getPrimaryEntry(String mimeType) {
151        final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false);
152        if (mimeEntries == null) return null;
153
154        for (ValuesDelta entry : mimeEntries) {
155            if (entry.isPrimary()) {
156                return entry;
157            }
158        }
159
160        // When no direct primary, return something
161        return mimeEntries.size() > 0 ? mimeEntries.get(0) : null;
162    }
163
164    /**
165     * calls {@link #getSuperPrimaryEntry(String, boolean)} with true
166     * @see #getSuperPrimaryEntry(String, boolean)
167     */
168    public ValuesDelta getSuperPrimaryEntry(String mimeType) {
169        return getSuperPrimaryEntry(mimeType, true);
170    }
171
172    /**
173     * Returns the super-primary entry for the given mime type
174     * @param forceSelection if true, will try to return some value even if a super-primary
175     *     doesn't exist (may be a primary, or just a random item
176     * @return
177     */
178    public ValuesDelta getSuperPrimaryEntry(String mimeType, boolean forceSelection) {
179        final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false);
180        if (mimeEntries == null) return null;
181
182        ValuesDelta primary = null;
183        for (ValuesDelta entry : mimeEntries) {
184            if (entry.isSuperPrimary()) {
185                return entry;
186            } else if (entry.isPrimary()) {
187                primary = entry;
188            }
189        }
190
191        if (!forceSelection) {
192            return null;
193        }
194
195        // When no direct super primary, return something
196        if (primary != null) {
197            return primary;
198        }
199        return mimeEntries.size() > 0 ? mimeEntries.get(0) : null;
200    }
201
202    /**
203     * Return the list of child {@link ValuesDelta} from our optimized map,
204     * creating the list if requested.
205     */
206    private ArrayList<ValuesDelta> getMimeEntries(String mimeType, boolean lazyCreate) {
207        ArrayList<ValuesDelta> mimeEntries = mEntries.get(mimeType);
208        if (mimeEntries == null && lazyCreate) {
209            mimeEntries = Lists.newArrayList();
210            mEntries.put(mimeType, mimeEntries);
211        }
212        return mimeEntries;
213    }
214
215    public ArrayList<ValuesDelta> getMimeEntries(String mimeType) {
216        return getMimeEntries(mimeType, false);
217    }
218
219    public int getMimeEntriesCount(String mimeType, boolean onlyVisible) {
220        final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType);
221        if (mimeEntries == null) return 0;
222
223        int count = 0;
224        for (ValuesDelta child : mimeEntries) {
225            // Skip deleted items when requesting only visible
226            if (onlyVisible && !child.isVisible()) continue;
227            count++;
228        }
229        return count;
230    }
231
232    public boolean hasMimeEntries(String mimeType) {
233        return mEntries.containsKey(mimeType);
234    }
235
236    public ValuesDelta addEntry(ValuesDelta entry) {
237        final String mimeType = entry.getMimetype();
238        getMimeEntries(mimeType, true).add(entry);
239        return entry;
240    }
241
242    /**
243     * Find entry with the given {@link BaseColumns#_ID} value.
244     */
245    public ValuesDelta getEntry(Long childId) {
246        if (childId == null) {
247            // Requesting an "insert" entry, which has no "before"
248            return null;
249        }
250
251        // Search all children for requested entry
252        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
253            for (ValuesDelta entry : mimeEntries) {
254                if (childId.equals(entry.getId())) {
255                    return entry;
256                }
257            }
258        }
259        return null;
260    }
261
262    /**
263     * Return the total number of {@link ValuesDelta} contained.
264     */
265    public int getEntryCount(boolean onlyVisible) {
266        int count = 0;
267        for (String mimeType : mEntries.keySet()) {
268            count += getMimeEntriesCount(mimeType, onlyVisible);
269        }
270        return count;
271    }
272
273    @Override
274    public boolean equals(Object object) {
275        if (object instanceof EntityDelta) {
276            final EntityDelta other = (EntityDelta)object;
277
278            // Equality failed if parent values different
279            if (!other.mValues.equals(mValues)) return false;
280
281            for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
282                for (ValuesDelta child : mimeEntries) {
283                    // Equality failed if any children unmatched
284                    if (!other.containsEntry(child)) return false;
285                }
286            }
287
288            // Passed all tests, so equal
289            return true;
290        }
291        return false;
292    }
293
294    private boolean containsEntry(ValuesDelta entry) {
295        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
296            for (ValuesDelta child : mimeEntries) {
297                // Contained if we find any child that matches
298                if (child.equals(entry)) return true;
299            }
300        }
301        return false;
302    }
303
304    /**
305     * Mark this entire object deleted, including any {@link ValuesDelta}.
306     */
307    public void markDeleted() {
308        this.mValues.markDeleted();
309        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
310            for (ValuesDelta child : mimeEntries) {
311                child.markDeleted();
312            }
313        }
314    }
315
316    @Override
317    public String toString() {
318        final StringBuilder builder = new StringBuilder();
319        builder.append("\n(");
320        builder.append(mValues.toString());
321        builder.append(") = {");
322        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
323            for (ValuesDelta child : mimeEntries) {
324                builder.append("\n\t");
325                child.toString(builder);
326            }
327        }
328        builder.append("\n}\n");
329        return builder.toString();
330    }
331
332    /**
333     * Consider building the given {@link ContentProviderOperation.Builder} and
334     * appending it to the given list, which only happens if builder is valid.
335     */
336    private void possibleAdd(ArrayList<ContentProviderOperation> diff,
337            ContentProviderOperation.Builder builder) {
338        if (builder != null) {
339            diff.add(builder.build());
340        }
341    }
342
343    /**
344     * Build a list of {@link ContentProviderOperation} that will assert any
345     * "before" state hasn't changed. This is maintained separately so that all
346     * asserts can take place before any updates occur.
347     */
348    public void buildAssert(ArrayList<ContentProviderOperation> buildInto) {
349        final boolean isContactInsert = mValues.isInsert();
350        if (!isContactInsert) {
351            // Assert version is consistent while persisting changes
352            final Long beforeId = mValues.getId();
353            final Long beforeVersion = mValues.getAsLong(RawContacts.VERSION);
354            if (beforeId == null || beforeVersion == null) return;
355
356            final ContentProviderOperation.Builder builder = ContentProviderOperation
357                    .newAssertQuery(RawContacts.CONTENT_URI);
358            builder.withSelection(RawContacts._ID + "=" + beforeId, null);
359            builder.withValue(RawContacts.VERSION, beforeVersion);
360            buildInto.add(builder.build());
361        }
362    }
363
364    /**
365     * Build a list of {@link ContentProviderOperation} that will transform the
366     * current "before" {@link Entity} state into the modified state which this
367     * {@link EntityDelta} represents.
368     */
369    public void buildDiff(ArrayList<ContentProviderOperation> buildInto) {
370        final int firstIndex = buildInto.size();
371
372        final boolean isContactInsert = mValues.isInsert();
373        final boolean isContactDelete = mValues.isDelete();
374        final boolean isContactUpdate = !isContactInsert && !isContactDelete;
375
376        final Long beforeId = mValues.getId();
377
378        Builder builder;
379
380        if (isContactInsert) {
381            // TODO: for now simply disabling aggregation when a new contact is
382            // created on the phone.  In the future, will show aggregation suggestions
383            // after saving the contact.
384            mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED);
385        }
386
387        // Build possible operation at Contact level
388        builder = mValues.buildDiff(RawContacts.CONTENT_URI);
389        possibleAdd(buildInto, builder);
390
391        // Build operations for all children
392        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
393            for (ValuesDelta child : mimeEntries) {
394                // Ignore children if parent was deleted
395                if (isContactDelete) continue;
396
397                builder = child.buildDiff(Data.CONTENT_URI);
398                if (child.isInsert()) {
399                    if (isContactInsert) {
400                        // Parent is brand new insert, so back-reference _id
401                        builder.withValueBackReference(Data.RAW_CONTACT_ID, firstIndex);
402                    } else {
403                        // Inserting under existing, so fill with known _id
404                        builder.withValue(Data.RAW_CONTACT_ID, beforeId);
405                    }
406                } else if (isContactInsert && builder != null) {
407                    // Child must be insert when Contact insert
408                    throw new IllegalArgumentException("When parent insert, child must be also");
409                }
410                possibleAdd(buildInto, builder);
411            }
412        }
413
414        final boolean addedOperations = buildInto.size() > firstIndex;
415        if (addedOperations && isContactUpdate) {
416            // Suspend aggregation while persisting updates
417            builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_SUSPENDED);
418            buildInto.add(firstIndex, builder.build());
419
420            // Restore aggregation mode as last operation
421            builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_DEFAULT);
422            buildInto.add(builder.build());
423        } else if (isContactInsert) {
424            // Restore aggregation mode as last operation
425            builder = ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI);
426            builder.withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT);
427            builder.withSelection(RawContacts._ID + "=?", new String[1]);
428            builder.withSelectionBackReference(0, firstIndex);
429            buildInto.add(builder.build());
430        }
431    }
432
433    /**
434     * Build a {@link ContentProviderOperation} that changes
435     * {@link RawContacts#AGGREGATION_MODE} to the given value.
436     */
437    protected Builder buildSetAggregationMode(Long beforeId, int mode) {
438        Builder builder = ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI);
439        builder.withValue(RawContacts.AGGREGATION_MODE, mode);
440        builder.withSelection(RawContacts._ID + "=" + beforeId, null);
441        return builder;
442    }
443
444    /** {@inheritDoc} */
445    public int describeContents() {
446        // Nothing special about this parcel
447        return 0;
448    }
449
450    /** {@inheritDoc} */
451    public void writeToParcel(Parcel dest, int flags) {
452        final int size = this.getEntryCount(false);
453        dest.writeInt(size);
454        dest.writeParcelable(mValues, flags);
455        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
456            for (ValuesDelta child : mimeEntries) {
457                dest.writeParcelable(child, flags);
458            }
459        }
460    }
461
462    public void readFromParcel(Parcel source) {
463        final ClassLoader loader = getClass().getClassLoader();
464        final int size = source.readInt();
465        mValues = source.<ValuesDelta> readParcelable(loader);
466        for (int i = 0; i < size; i++) {
467            final ValuesDelta child = source.<ValuesDelta> readParcelable(loader);
468            this.addEntry(child);
469        }
470    }
471
472    public static final Parcelable.Creator<EntityDelta> CREATOR = new Parcelable.Creator<EntityDelta>() {
473        public EntityDelta createFromParcel(Parcel in) {
474            final EntityDelta state = new EntityDelta();
475            state.readFromParcel(in);
476            return state;
477        }
478
479        public EntityDelta[] newArray(int size) {
480            return new EntityDelta[size];
481        }
482    };
483
484    /**
485     * Type of {@link ContentValues} that maintains both an original state and a
486     * modified version of that state. This allows us to build insert, update,
487     * or delete operations based on a "before" {@link Entity} snapshot.
488     */
489    public static class ValuesDelta implements Parcelable {
490        protected ContentValues mBefore;
491        protected ContentValues mAfter;
492        protected String mIdColumn = BaseColumns._ID;
493        private boolean mFromTemplate;
494
495        /**
496         * Next value to assign to {@link #mIdColumn} when building an insert
497         * operation through {@link #fromAfter(ContentValues)}. This is used so
498         * we can concretely reference this {@link ValuesDelta} before it has
499         * been persisted.
500         */
501        protected static int sNextInsertId = -1;
502
503        protected ValuesDelta() {
504        }
505
506        /**
507         * Create {@link ValuesDelta}, using the given object as the
508         * "before" state, usually from an {@link Entity}.
509         */
510        public static ValuesDelta fromBefore(ContentValues before) {
511            final ValuesDelta entry = new ValuesDelta();
512            entry.mBefore = before;
513            entry.mAfter = new ContentValues();
514            return entry;
515        }
516
517        /**
518         * Create {@link ValuesDelta}, using the given object as the "after"
519         * state, usually when we are inserting a row instead of updating.
520         */
521        public static ValuesDelta fromAfter(ContentValues after) {
522            final ValuesDelta entry = new ValuesDelta();
523            entry.mBefore = null;
524            entry.mAfter = after;
525
526            // Assign temporary id which is dropped before insert.
527            entry.mAfter.put(entry.mIdColumn, sNextInsertId--);
528            return entry;
529        }
530
531        public ContentValues getAfter() {
532            return mAfter;
533        }
534
535        public String getAsString(String key) {
536            if (mAfter != null && mAfter.containsKey(key)) {
537                return mAfter.getAsString(key);
538            } else if (mBefore != null && mBefore.containsKey(key)) {
539                return mBefore.getAsString(key);
540            } else {
541                return null;
542            }
543        }
544
545        public byte[] getAsByteArray(String key) {
546            if (mAfter != null && mAfter.containsKey(key)) {
547                return mAfter.getAsByteArray(key);
548            } else if (mBefore != null && mBefore.containsKey(key)) {
549                return mBefore.getAsByteArray(key);
550            } else {
551                return null;
552            }
553        }
554
555        public Long getAsLong(String key) {
556            if (mAfter != null && mAfter.containsKey(key)) {
557                return mAfter.getAsLong(key);
558            } else if (mBefore != null && mBefore.containsKey(key)) {
559                return mBefore.getAsLong(key);
560            } else {
561                return null;
562            }
563        }
564
565        public Integer getAsInteger(String key) {
566            return getAsInteger(key, null);
567        }
568
569        public Integer getAsInteger(String key, Integer defaultValue) {
570            if (mAfter != null && mAfter.containsKey(key)) {
571                return mAfter.getAsInteger(key);
572            } else if (mBefore != null && mBefore.containsKey(key)) {
573                return mBefore.getAsInteger(key);
574            } else {
575                return defaultValue;
576            }
577        }
578
579        public String getMimetype() {
580            return getAsString(Data.MIMETYPE);
581        }
582
583        public Long getId() {
584            return getAsLong(mIdColumn);
585        }
586
587        public void setIdColumn(String idColumn) {
588            mIdColumn = idColumn;
589        }
590
591        public boolean isPrimary() {
592            final Long isPrimary = getAsLong(Data.IS_PRIMARY);
593            return isPrimary == null ? false : isPrimary != 0;
594        }
595
596        public void setFromTemplate(boolean isFromTemplate) {
597            mFromTemplate = isFromTemplate;
598        }
599
600        public boolean isFromTemplate() {
601            return mFromTemplate;
602        }
603
604        public boolean isSuperPrimary() {
605            final Long isSuperPrimary = getAsLong(Data.IS_SUPER_PRIMARY);
606            return isSuperPrimary == null ? false : isSuperPrimary != 0;
607        }
608
609        public boolean beforeExists() {
610            return (mBefore != null && mBefore.containsKey(mIdColumn));
611        }
612
613        public boolean isVisible() {
614            // When "after" is present, then visible
615            return (mAfter != null);
616        }
617
618        public boolean isDelete() {
619            // When "after" is wiped, action is "delete"
620            return beforeExists() && (mAfter == null);
621        }
622
623        public boolean isTransient() {
624            // When no "before" or "after", is transient
625            return (mBefore == null) && (mAfter == null);
626        }
627
628        public boolean isUpdate() {
629            // When "after" has some changes, action is "update"
630            return beforeExists() && (mAfter != null && mAfter.size() > 0);
631        }
632
633        public boolean isNoop() {
634            // When "after" has no changes, action is no-op
635            return beforeExists() && (mAfter != null && mAfter.size() == 0);
636        }
637
638        public boolean isInsert() {
639            // When no "before" id, and has "after", action is "insert"
640            return !beforeExists() && (mAfter != null);
641        }
642
643        public void markDeleted() {
644            mAfter = null;
645        }
646
647        /**
648         * Ensure that our internal structure is ready for storing updates.
649         */
650        private void ensureUpdate() {
651            if (mAfter == null) {
652                mAfter = new ContentValues();
653            }
654        }
655
656        public void put(String key, String value) {
657            ensureUpdate();
658            mAfter.put(key, value);
659        }
660
661        public void put(String key, byte[] value) {
662            ensureUpdate();
663            mAfter.put(key, value);
664        }
665
666        public void put(String key, int value) {
667            ensureUpdate();
668            mAfter.put(key, value);
669        }
670
671        /**
672         * Return set of all keys defined through this object.
673         */
674        public Set<String> keySet() {
675            final HashSet<String> keys = Sets.newHashSet();
676
677            if (mBefore != null) {
678                for (Map.Entry<String, Object> entry : mBefore.valueSet()) {
679                    keys.add(entry.getKey());
680                }
681            }
682
683            if (mAfter != null) {
684                for (Map.Entry<String, Object> entry : mAfter.valueSet()) {
685                    keys.add(entry.getKey());
686                }
687            }
688
689            return keys;
690        }
691
692        /**
693         * Return complete set of "before" and "after" values mixed together,
694         * giving full state regardless of edits.
695         */
696        public ContentValues getCompleteValues() {
697            final ContentValues values = new ContentValues();
698            if (mBefore != null) {
699                values.putAll(mBefore);
700            }
701            if (mAfter != null) {
702                values.putAll(mAfter);
703            }
704            if (values.containsKey(GroupMembership.GROUP_ROW_ID)) {
705                // Clear to avoid double-definitions, and prefer rows
706                values.remove(GroupMembership.GROUP_SOURCE_ID);
707            }
708
709            return values;
710        }
711
712        /**
713         * Merge the "after" values from the given {@link ValuesDelta},
714         * discarding any existing "after" state. This is typically used when
715         * re-parenting changes onto an updated {@link Entity}.
716         */
717        public static ValuesDelta mergeAfter(ValuesDelta local, ValuesDelta remote) {
718            // Bail early if trying to merge delete with missing local
719            if (local == null && (remote.isDelete() || remote.isTransient())) return null;
720
721            // Create local version if none exists yet
722            if (local == null) local = new ValuesDelta();
723
724            if (!local.beforeExists()) {
725                // Any "before" record is missing, so take all values as "insert"
726                local.mAfter = remote.getCompleteValues();
727            } else {
728                // Existing "update" with only "after" values
729                local.mAfter = remote.mAfter;
730            }
731
732            return local;
733        }
734
735        @Override
736        public boolean equals(Object object) {
737            if (object instanceof ValuesDelta) {
738                // Only exactly equal with both are identical subsets
739                final ValuesDelta other = (ValuesDelta)object;
740                return this.subsetEquals(other) && other.subsetEquals(this);
741            }
742            return false;
743        }
744
745        @Override
746        public String toString() {
747            final StringBuilder builder = new StringBuilder();
748            toString(builder);
749            return builder.toString();
750        }
751
752        /**
753         * Helper for building string representation, leveraging the given
754         * {@link StringBuilder} to minimize allocations.
755         */
756        public void toString(StringBuilder builder) {
757            builder.append("{ ");
758            for (String key : this.keySet()) {
759                builder.append(key);
760                builder.append("=");
761                builder.append(this.getAsString(key));
762                builder.append(", ");
763            }
764            builder.append("}");
765        }
766
767        /**
768         * Check if the given {@link ValuesDelta} is both a subset of this
769         * object, and any defined keys have equal values.
770         */
771        public boolean subsetEquals(ValuesDelta other) {
772            for (String key : this.keySet()) {
773                final String ourValue = this.getAsString(key);
774                final String theirValue = other.getAsString(key);
775                if (ourValue == null) {
776                    // If they have value when we're null, no match
777                    if (theirValue != null) return false;
778                } else {
779                    // If both values defined and aren't equal, no match
780                    if (!ourValue.equals(theirValue)) return false;
781                }
782            }
783            // All values compared and matched
784            return true;
785        }
786
787        /**
788         * Build a {@link ContentProviderOperation} that will transform our
789         * "before" state into our "after" state, using insert, update, or
790         * delete as needed.
791         */
792        public ContentProviderOperation.Builder buildDiff(Uri targetUri) {
793            Builder builder = null;
794            if (isInsert()) {
795                // Changed values are "insert" back-referenced to Contact
796                mAfter.remove(mIdColumn);
797                builder = ContentProviderOperation.newInsert(targetUri);
798                builder.withValues(mAfter);
799            } else if (isDelete()) {
800                // When marked for deletion and "before" exists, then "delete"
801                builder = ContentProviderOperation.newDelete(targetUri);
802                builder.withSelection(mIdColumn + "=" + getId(), null);
803            } else if (isUpdate()) {
804                // When has changes and "before" exists, then "update"
805                builder = ContentProviderOperation.newUpdate(targetUri);
806                builder.withSelection(mIdColumn + "=" + getId(), null);
807                builder.withValues(mAfter);
808            }
809            return builder;
810        }
811
812        /** {@inheritDoc} */
813        public int describeContents() {
814            // Nothing special about this parcel
815            return 0;
816        }
817
818        /** {@inheritDoc} */
819        public void writeToParcel(Parcel dest, int flags) {
820            dest.writeParcelable(mBefore, flags);
821            dest.writeParcelable(mAfter, flags);
822            dest.writeString(mIdColumn);
823        }
824
825        public void readFromParcel(Parcel source) {
826            final ClassLoader loader = getClass().getClassLoader();
827            mBefore = source.<ContentValues> readParcelable(loader);
828            mAfter = source.<ContentValues> readParcelable(loader);
829            mIdColumn = source.readString();
830        }
831
832        public static final Parcelable.Creator<ValuesDelta> CREATOR = new Parcelable.Creator<ValuesDelta>() {
833            public ValuesDelta createFromParcel(Parcel in) {
834                final ValuesDelta values = new ValuesDelta();
835                values.readFromParcel(in);
836                return values;
837            }
838
839            public ValuesDelta[] newArray(int size) {
840                return new ValuesDelta[size];
841            }
842        };
843    }
844}
845