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