• 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.loaderapp.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.Data;
33 import android.provider.ContactsContract.RawContacts;
34 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
35 import android.util.Log;
36 
37 import java.util.ArrayList;
38 import java.util.HashMap;
39 import java.util.HashSet;
40 import java.util.Map;
41 import 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  */
56 public 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 
EntityDelta()73     public EntityDelta() {
74     }
75 
EntityDelta(ValuesDelta values)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      */
fromBefore(Entity before)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      */
mergeAfter(EntityDelta local, EntityDelta remote)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 
getValues()138     public ValuesDelta getValues() {
139         return mValues;
140     }
141 
isContactInsert()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      */
getPrimaryEntry(String mimeType)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      */
getSuperPrimaryEntry(String mimeType)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      */
getSuperPrimaryEntry(String mimeType, boolean forceSelection)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      */
getMimeEntries(String mimeType, boolean lazyCreate)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 
getMimeEntries(String mimeType)215     public ArrayList<ValuesDelta> getMimeEntries(String mimeType) {
216         return getMimeEntries(mimeType, false);
217     }
218 
getMimeEntriesCount(String mimeType, boolean onlyVisible)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 
hasMimeEntries(String mimeType)232     public boolean hasMimeEntries(String mimeType) {
233         return mEntries.containsKey(mimeType);
234     }
235 
addEntry(ValuesDelta entry)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      */
getEntry(Long childId)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      */
getEntryCount(boolean onlyVisible)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
equals(Object object)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 
containsEntry(ValuesDelta entry)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      */
markDeleted()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
toString()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      */
possibleAdd(ArrayList<ContentProviderOperation> diff, ContentProviderOperation.Builder builder)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      */
buildAssert(ArrayList<ContentProviderOperation> buildInto)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      */
buildDiff(ArrayList<ContentProviderOperation> buildInto)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      */
buildSetAggregationMode(Long beforeId, int mode)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} */
describeContents()445     public int describeContents() {
446         // Nothing special about this parcel
447         return 0;
448     }
449 
450     /** {@inheritDoc} */
writeToParcel(Parcel dest, int flags)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 
readFromParcel(Parcel source)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 
ValuesDelta()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          */
fromBefore(ContentValues before)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          */
fromAfter(ContentValues after)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 
getAfter()531         public ContentValues getAfter() {
532             return mAfter;
533         }
534 
getAsString(String key)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 
getAsByteArray(String key)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 
getAsLong(String key)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 
getAsInteger(String key)565         public Integer getAsInteger(String key) {
566             return getAsInteger(key, null);
567         }
568 
getAsInteger(String key, Integer defaultValue)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 
getMimetype()579         public String getMimetype() {
580             return getAsString(Data.MIMETYPE);
581         }
582 
getId()583         public Long getId() {
584             return getAsLong(mIdColumn);
585         }
586 
setIdColumn(String idColumn)587         public void setIdColumn(String idColumn) {
588             mIdColumn = idColumn;
589         }
590 
isPrimary()591         public boolean isPrimary() {
592             final Long isPrimary = getAsLong(Data.IS_PRIMARY);
593             return isPrimary == null ? false : isPrimary != 0;
594         }
595 
setFromTemplate(boolean isFromTemplate)596         public void setFromTemplate(boolean isFromTemplate) {
597             mFromTemplate = isFromTemplate;
598         }
599 
isFromTemplate()600         public boolean isFromTemplate() {
601             return mFromTemplate;
602         }
603 
isSuperPrimary()604         public boolean isSuperPrimary() {
605             final Long isSuperPrimary = getAsLong(Data.IS_SUPER_PRIMARY);
606             return isSuperPrimary == null ? false : isSuperPrimary != 0;
607         }
608 
beforeExists()609         public boolean beforeExists() {
610             return (mBefore != null && mBefore.containsKey(mIdColumn));
611         }
612 
isVisible()613         public boolean isVisible() {
614             // When "after" is present, then visible
615             return (mAfter != null);
616         }
617 
isDelete()618         public boolean isDelete() {
619             // When "after" is wiped, action is "delete"
620             return beforeExists() && (mAfter == null);
621         }
622 
isTransient()623         public boolean isTransient() {
624             // When no "before" or "after", is transient
625             return (mBefore == null) && (mAfter == null);
626         }
627 
isUpdate()628         public boolean isUpdate() {
629             // When "after" has some changes, action is "update"
630             return beforeExists() && (mAfter != null && mAfter.size() > 0);
631         }
632 
isNoop()633         public boolean isNoop() {
634             // When "after" has no changes, action is no-op
635             return beforeExists() && (mAfter != null && mAfter.size() == 0);
636         }
637 
isInsert()638         public boolean isInsert() {
639             // When no "before" id, and has "after", action is "insert"
640             return !beforeExists() && (mAfter != null);
641         }
642 
markDeleted()643         public void markDeleted() {
644             mAfter = null;
645         }
646 
647         /**
648          * Ensure that our internal structure is ready for storing updates.
649          */
ensureUpdate()650         private void ensureUpdate() {
651             if (mAfter == null) {
652                 mAfter = new ContentValues();
653             }
654         }
655 
put(String key, String value)656         public void put(String key, String value) {
657             ensureUpdate();
658             mAfter.put(key, value);
659         }
660 
put(String key, byte[] value)661         public void put(String key, byte[] value) {
662             ensureUpdate();
663             mAfter.put(key, value);
664         }
665 
put(String key, int value)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          */
keySet()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          */
getCompleteValues()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          */
mergeAfter(ValuesDelta local, ValuesDelta remote)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
equals(Object object)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
toString()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          */
toString(StringBuilder builder)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          */
subsetEquals(ValuesDelta other)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          */
buildDiff(Uri targetUri)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} */
describeContents()813         public int describeContents() {
814             // Nothing special about this parcel
815             return 0;
816         }
817 
818         /** {@inheritDoc} */
writeToParcel(Parcel dest, int flags)819         public void writeToParcel(Parcel dest, int flags) {
820             dest.writeParcelable(mBefore, flags);
821             dest.writeParcelable(mAfter, flags);
822             dest.writeString(mIdColumn);
823         }
824 
readFromParcel(Parcel source)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