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