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