• 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         // Build possible operation at Contact level
383         builder = mValues.buildDiff(RawContacts.CONTENT_URI);
384         possibleAdd(buildInto, builder);
385 
386         // Build operations for all children
387         for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
388             for (ValuesDelta child : mimeEntries) {
389                 // Ignore children if parent was deleted
390                 if (isContactDelete) continue;
391 
392                 builder = child.buildDiff(Data.CONTENT_URI);
393                 if (child.isInsert()) {
394                     if (isContactInsert) {
395                         // Parent is brand new insert, so back-reference _id
396                         builder.withValueBackReference(Data.RAW_CONTACT_ID, firstIndex);
397                     } else {
398                         // Inserting under existing, so fill with known _id
399                         builder.withValue(Data.RAW_CONTACT_ID, beforeId);
400                     }
401                 } else if (isContactInsert && builder != null) {
402                     // Child must be insert when Contact insert
403                     throw new IllegalArgumentException("When parent insert, child must be also");
404                 }
405                 possibleAdd(buildInto, builder);
406             }
407         }
408 
409         final boolean addedOperations = buildInto.size() > firstIndex;
410         if (addedOperations && isContactUpdate) {
411             // Suspend aggregation while persisting updates
412             builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_SUSPENDED);
413             buildInto.add(firstIndex, builder.build());
414 
415             // Restore aggregation as last operation
416             builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_DEFAULT);
417             buildInto.add(builder.build());
418         }
419     }
420 
421     /**
422      * Build a {@link ContentProviderOperation} that changes
423      * {@link RawContacts#AGGREGATION_MODE} to the given value.
424      */
buildSetAggregationMode(Long beforeId, int mode)425     protected Builder buildSetAggregationMode(Long beforeId, int mode) {
426         Builder builder = ContentProviderOperation.newUpdate(RawContacts.CONTENT_URI);
427         builder.withValue(RawContacts.AGGREGATION_MODE, mode);
428         builder.withSelection(RawContacts._ID + "=" + beforeId, null);
429         return builder;
430     }
431 
432     /** {@inheritDoc} */
describeContents()433     public int describeContents() {
434         // Nothing special about this parcel
435         return 0;
436     }
437 
438     /** {@inheritDoc} */
writeToParcel(Parcel dest, int flags)439     public void writeToParcel(Parcel dest, int flags) {
440         final int size = this.getEntryCount(false);
441         dest.writeInt(size);
442         dest.writeParcelable(mValues, flags);
443         for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
444             for (ValuesDelta child : mimeEntries) {
445                 dest.writeParcelable(child, flags);
446             }
447         }
448     }
449 
readFromParcel(Parcel source)450     public void readFromParcel(Parcel source) {
451         final int size = source.readInt();
452         mValues = source.<ValuesDelta> readParcelable(null);
453         for (int i = 0; i < size; i++) {
454             final ValuesDelta child = source.<ValuesDelta> readParcelable(null);
455             this.addEntry(child);
456         }
457     }
458 
459     public static final Parcelable.Creator<EntityDelta> CREATOR = new Parcelable.Creator<EntityDelta>() {
460         public EntityDelta createFromParcel(Parcel in) {
461             final EntityDelta state = new EntityDelta();
462             state.readFromParcel(in);
463             return state;
464         }
465 
466         public EntityDelta[] newArray(int size) {
467             return new EntityDelta[size];
468         }
469     };
470 
471     /**
472      * Type of {@link ContentValues} that maintains both an original state and a
473      * modified version of that state. This allows us to build insert, update,
474      * or delete operations based on a "before" {@link Entity} snapshot.
475      */
476     public static class ValuesDelta implements Parcelable {
477         protected ContentValues mBefore;
478         protected ContentValues mAfter;
479         protected String mIdColumn = BaseColumns._ID;
480         private boolean mFromTemplate;
481 
482         /**
483          * Next value to assign to {@link #mIdColumn} when building an insert
484          * operation through {@link #fromAfter(ContentValues)}. This is used so
485          * we can concretely reference this {@link ValuesDelta} before it has
486          * been persisted.
487          */
488         protected static int sNextInsertId = -1;
489 
ValuesDelta()490         protected ValuesDelta() {
491         }
492 
493         /**
494          * Create {@link ValuesDelta}, using the given object as the
495          * "before" state, usually from an {@link Entity}.
496          */
fromBefore(ContentValues before)497         public static ValuesDelta fromBefore(ContentValues before) {
498             final ValuesDelta entry = new ValuesDelta();
499             entry.mBefore = before;
500             entry.mAfter = new ContentValues();
501             return entry;
502         }
503 
504         /**
505          * Create {@link ValuesDelta}, using the given object as the "after"
506          * state, usually when we are inserting a row instead of updating.
507          */
fromAfter(ContentValues after)508         public static ValuesDelta fromAfter(ContentValues after) {
509             final ValuesDelta entry = new ValuesDelta();
510             entry.mBefore = null;
511             entry.mAfter = after;
512 
513             // Assign temporary id which is dropped before insert.
514             entry.mAfter.put(entry.mIdColumn, sNextInsertId--);
515             return entry;
516         }
517 
getAfter()518         public ContentValues getAfter() {
519             return mAfter;
520         }
521 
getAsString(String key)522         public String getAsString(String key) {
523             if (mAfter != null && mAfter.containsKey(key)) {
524                 return mAfter.getAsString(key);
525             } else if (mBefore != null && mBefore.containsKey(key)) {
526                 return mBefore.getAsString(key);
527             } else {
528                 return null;
529             }
530         }
531 
getAsByteArray(String key)532         public byte[] getAsByteArray(String key) {
533             if (mAfter != null && mAfter.containsKey(key)) {
534                 return mAfter.getAsByteArray(key);
535             } else if (mBefore != null && mBefore.containsKey(key)) {
536                 return mBefore.getAsByteArray(key);
537             } else {
538                 return null;
539             }
540         }
541 
getAsLong(String key)542         public Long getAsLong(String key) {
543             if (mAfter != null && mAfter.containsKey(key)) {
544                 return mAfter.getAsLong(key);
545             } else if (mBefore != null && mBefore.containsKey(key)) {
546                 return mBefore.getAsLong(key);
547             } else {
548                 return null;
549             }
550         }
551 
getAsInteger(String key)552         public Integer getAsInteger(String key) {
553             return getAsInteger(key, null);
554         }
555 
getAsInteger(String key, Integer defaultValue)556         public Integer getAsInteger(String key, Integer defaultValue) {
557             if (mAfter != null && mAfter.containsKey(key)) {
558                 return mAfter.getAsInteger(key);
559             } else if (mBefore != null && mBefore.containsKey(key)) {
560                 return mBefore.getAsInteger(key);
561             } else {
562                 return defaultValue;
563             }
564         }
565 
getMimetype()566         public String getMimetype() {
567             return getAsString(Data.MIMETYPE);
568         }
569 
getId()570         public Long getId() {
571             return getAsLong(mIdColumn);
572         }
573 
574         /**
575          * Return a valid integer value suitable for {@link View#setId(int)}.
576          */
getViewId()577         public int getViewId() {
578             final Long id = this.getId();
579             return (id == null) ? View.NO_ID : id.intValue();
580         }
581 
setIdColumn(String idColumn)582         public void setIdColumn(String idColumn) {
583             mIdColumn = idColumn;
584         }
585 
isPrimary()586         public boolean isPrimary() {
587             final Long isPrimary = getAsLong(Data.IS_PRIMARY);
588             return isPrimary == null ? false : isPrimary != 0;
589         }
590 
setFromTemplate(boolean isFromTemplate)591         public void setFromTemplate(boolean isFromTemplate) {
592             mFromTemplate = isFromTemplate;
593         }
594 
isFromTemplate()595         public boolean isFromTemplate() {
596             return mFromTemplate;
597         }
598 
isSuperPrimary()599         public boolean isSuperPrimary() {
600             final Long isSuperPrimary = getAsLong(Data.IS_SUPER_PRIMARY);
601             return isSuperPrimary == null ? false : isSuperPrimary != 0;
602         }
603 
beforeExists()604         public boolean beforeExists() {
605             return (mBefore != null && mBefore.containsKey(mIdColumn));
606         }
607 
isVisible()608         public boolean isVisible() {
609             // When "after" is present, then visible
610             return (mAfter != null);
611         }
612 
isDelete()613         public boolean isDelete() {
614             // When "after" is wiped, action is "delete"
615             return beforeExists() && (mAfter == null);
616         }
617 
isTransient()618         public boolean isTransient() {
619             // When no "before" or "after", is transient
620             return (mBefore == null) && (mAfter == null);
621         }
622 
isUpdate()623         public boolean isUpdate() {
624             // When "after" has some changes, action is "update"
625             return beforeExists() && (mAfter != null && mAfter.size() > 0);
626         }
627 
isNoop()628         public boolean isNoop() {
629             // When "after" has no changes, action is no-op
630             return beforeExists() && (mAfter != null && mAfter.size() == 0);
631         }
632 
isInsert()633         public boolean isInsert() {
634             // When no "before" id, and has "after", action is "insert"
635             return !beforeExists() && (mAfter != null);
636         }
637 
markDeleted()638         public void markDeleted() {
639             mAfter = null;
640         }
641 
642         /**
643          * Ensure that our internal structure is ready for storing updates.
644          */
ensureUpdate()645         private void ensureUpdate() {
646             if (mAfter == null) {
647                 mAfter = new ContentValues();
648             }
649         }
650 
put(String key, String value)651         public void put(String key, String value) {
652             ensureUpdate();
653             mAfter.put(key, value);
654         }
655 
put(String key, byte[] value)656         public void put(String key, byte[] value) {
657             ensureUpdate();
658             mAfter.put(key, value);
659         }
660 
put(String key, int value)661         public void put(String key, int value) {
662             ensureUpdate();
663             mAfter.put(key, value);
664         }
665 
666         /**
667          * Return set of all keys defined through this object.
668          */
keySet()669         public Set<String> keySet() {
670             final HashSet<String> keys = Sets.newHashSet();
671 
672             if (mBefore != null) {
673                 for (Map.Entry<String, Object> entry : mBefore.valueSet()) {
674                     keys.add(entry.getKey());
675                 }
676             }
677 
678             if (mAfter != null) {
679                 for (Map.Entry<String, Object> entry : mAfter.valueSet()) {
680                     keys.add(entry.getKey());
681                 }
682             }
683 
684             return keys;
685         }
686 
687         /**
688          * Return complete set of "before" and "after" values mixed together,
689          * giving full state regardless of edits.
690          */
getCompleteValues()691         public ContentValues getCompleteValues() {
692             final ContentValues values = new ContentValues();
693             if (mBefore != null) {
694                 values.putAll(mBefore);
695             }
696             if (mAfter != null) {
697                 values.putAll(mAfter);
698             }
699             if (values.containsKey(GroupMembership.GROUP_ROW_ID)) {
700                 // Clear to avoid double-definitions, and prefer rows
701                 values.remove(GroupMembership.GROUP_SOURCE_ID);
702             }
703 
704             return values;
705         }
706 
707         /**
708          * Merge the "after" values from the given {@link ValuesDelta},
709          * discarding any existing "after" state. This is typically used when
710          * re-parenting changes onto an updated {@link Entity}.
711          */
mergeAfter(ValuesDelta local, ValuesDelta remote)712         public static ValuesDelta mergeAfter(ValuesDelta local, ValuesDelta remote) {
713             // Bail early if trying to merge delete with missing local
714             if (local == null && (remote.isDelete() || remote.isTransient())) return null;
715 
716             // Create local version if none exists yet
717             if (local == null) local = new ValuesDelta();
718 
719             if (!local.beforeExists()) {
720                 // Any "before" record is missing, so take all values as "insert"
721                 local.mAfter = remote.getCompleteValues();
722             } else {
723                 // Existing "update" with only "after" values
724                 local.mAfter = remote.mAfter;
725             }
726 
727             return local;
728         }
729 
730         @Override
equals(Object object)731         public boolean equals(Object object) {
732             if (object instanceof ValuesDelta) {
733                 // Only exactly equal with both are identical subsets
734                 final ValuesDelta other = (ValuesDelta)object;
735                 return this.subsetEquals(other) && other.subsetEquals(this);
736             }
737             return false;
738         }
739 
740         @Override
toString()741         public String toString() {
742             final StringBuilder builder = new StringBuilder();
743             toString(builder);
744             return builder.toString();
745         }
746 
747         /**
748          * Helper for building string representation, leveraging the given
749          * {@link StringBuilder} to minimize allocations.
750          */
toString(StringBuilder builder)751         public void toString(StringBuilder builder) {
752             builder.append("{ ");
753             for (String key : this.keySet()) {
754                 builder.append(key);
755                 builder.append("=");
756                 builder.append(this.getAsString(key));
757                 builder.append(", ");
758             }
759             builder.append("}");
760         }
761 
762         /**
763          * Check if the given {@link ValuesDelta} is both a subset of this
764          * object, and any defined keys have equal values.
765          */
subsetEquals(ValuesDelta other)766         public boolean subsetEquals(ValuesDelta other) {
767             for (String key : this.keySet()) {
768                 final String ourValue = this.getAsString(key);
769                 final String theirValue = other.getAsString(key);
770                 if (ourValue == null) {
771                     // If they have value when we're null, no match
772                     if (theirValue != null) return false;
773                 } else {
774                     // If both values defined and aren't equal, no match
775                     if (!ourValue.equals(theirValue)) return false;
776                 }
777             }
778             // All values compared and matched
779             return true;
780         }
781 
782         /**
783          * Build a {@link ContentProviderOperation} that will transform our
784          * "before" state into our "after" state, using insert, update, or
785          * delete as needed.
786          */
buildDiff(Uri targetUri)787         public ContentProviderOperation.Builder buildDiff(Uri targetUri) {
788             Builder builder = null;
789             if (isInsert()) {
790                 // Changed values are "insert" back-referenced to Contact
791                 mAfter.remove(mIdColumn);
792                 builder = ContentProviderOperation.newInsert(targetUri);
793                 builder.withValues(mAfter);
794             } else if (isDelete()) {
795                 // When marked for deletion and "before" exists, then "delete"
796                 builder = ContentProviderOperation.newDelete(targetUri);
797                 builder.withSelection(mIdColumn + "=" + getId(), null);
798             } else if (isUpdate()) {
799                 // When has changes and "before" exists, then "update"
800                 builder = ContentProviderOperation.newUpdate(targetUri);
801                 builder.withSelection(mIdColumn + "=" + getId(), null);
802                 builder.withValues(mAfter);
803             }
804             return builder;
805         }
806 
807         /** {@inheritDoc} */
describeContents()808         public int describeContents() {
809             // Nothing special about this parcel
810             return 0;
811         }
812 
813         /** {@inheritDoc} */
writeToParcel(Parcel dest, int flags)814         public void writeToParcel(Parcel dest, int flags) {
815             dest.writeParcelable(mBefore, flags);
816             dest.writeParcelable(mAfter, flags);
817             dest.writeString(mIdColumn);
818         }
819 
readFromParcel(Parcel source)820         public void readFromParcel(Parcel source) {
821             mBefore = source.<ContentValues> readParcelable(null);
822             mAfter = source.<ContentValues> readParcelable(null);
823             mIdColumn = source.readString();
824         }
825 
826         public static final Parcelable.Creator<ValuesDelta> CREATOR = new Parcelable.Creator<ValuesDelta>() {
827             public ValuesDelta createFromParcel(Parcel in) {
828                 final ValuesDelta values = new ValuesDelta();
829                 values.readFromParcel(in);
830                 return values;
831             }
832 
833             public ValuesDelta[] newArray(int size) {
834                 return new ValuesDelta[size];
835             }
836         };
837     }
838 }
839