Home | History | Annotate | Download | only in model
      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 
     73     public EntityDelta() {
     74     }
     75 
     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      */
     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      */
    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 
    138     public ValuesDelta getValues() {
    139         return mValues;
    140     }
    141 
    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      */
    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      */
    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      */
    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      */
    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 
    215     public ArrayList<ValuesDelta> getMimeEntries(String mimeType) {
    216         return getMimeEntries(mimeType, false);
    217     }
    218 
    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 
    232     public boolean hasMimeEntries(String mimeType) {
    233         return mEntries.containsKey(mimeType);
    234     }
    235 
    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      */
    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      */
    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
    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 
    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      */
    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
    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      */
    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      */
    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      */
    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      */
    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} */
    445     public int describeContents() {
    446         // Nothing special about this parcel
    447         return 0;
    448     }
    449 
    450     /** {@inheritDoc} */
    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 
    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 
    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          */
    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          */
    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 
    531         public ContentValues getAfter() {
    532             return mAfter;
    533         }
    534 
    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 
    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 
    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 
    565         public Integer getAsInteger(String key) {
    566             return getAsInteger(key, null);
    567         }
    568 
    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 
    579         public String getMimetype() {
    580             return getAsString(Data.MIMETYPE);
    581         }
    582 
    583         public Long getId() {
    584             return getAsLong(mIdColumn);
    585         }
    586 
    587         public void setIdColumn(String idColumn) {
    588             mIdColumn = idColumn;
    589         }
    590 
    591         public boolean isPrimary() {
    592             final Long isPrimary = getAsLong(Data.IS_PRIMARY);
    593             return isPrimary == null ? false : isPrimary != 0;
    594         }
    595 
    596         public void setFromTemplate(boolean isFromTemplate) {
    597             mFromTemplate = isFromTemplate;
    598         }
    599 
    600         public boolean isFromTemplate() {
    601             return mFromTemplate;
    602         }
    603 
    604         public boolean isSuperPrimary() {
    605             final Long isSuperPrimary = getAsLong(Data.IS_SUPER_PRIMARY);
    606             return isSuperPrimary == null ? false : isSuperPrimary != 0;
    607         }
    608 
    609         public boolean beforeExists() {
    610             return (mBefore != null && mBefore.containsKey(mIdColumn));
    611         }
    612 
    613         public boolean isVisible() {
    614             // When "after" is present, then visible
    615             return (mAfter != null);
    616         }
    617 
    618         public boolean isDelete() {
    619             // When "after" is wiped, action is "delete"
    620             return beforeExists() && (mAfter == null);
    621         }
    622 
    623         public boolean isTransient() {
    624             // When no "before" or "after", is transient
    625             return (mBefore == null) && (mAfter == null);
    626         }
    627 
    628         public boolean isUpdate() {
    629             // When "after" has some changes, action is "update"
    630             return beforeExists() && (mAfter != null && mAfter.size() > 0);
    631         }
    632 
    633         public boolean isNoop() {
    634             // When "after" has no changes, action is no-op
    635             return beforeExists() && (mAfter != null && mAfter.size() == 0);
    636         }
    637 
    638         public boolean isInsert() {
    639             // When no "before" id, and has "after", action is "insert"
    640             return !beforeExists() && (mAfter != null);
    641         }
    642 
    643         public void markDeleted() {
    644             mAfter = null;
    645         }
    646 
    647         /**
    648          * Ensure that our internal structure is ready for storing updates.
    649          */
    650         private void ensureUpdate() {
    651             if (mAfter == null) {
    652                 mAfter = new ContentValues();
    653             }
    654         }
    655 
    656         public void put(String key, String value) {
    657             ensureUpdate();
    658             mAfter.put(key, value);
    659         }
    660 
    661         public void put(String key, byte[] value) {
    662             ensureUpdate();
    663             mAfter.put(key, value);
    664         }
    665 
    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          */
    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          */
    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          */
    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
    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
    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          */
    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          */
    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          */
    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} */
    813         public int describeContents() {
    814             // Nothing special about this parcel
    815             return 0;
    816         }
    817 
    818         /** {@inheritDoc} */
    819         public void writeToParcel(Parcel dest, int flags) {
    820             dest.writeParcelable(mBefore, flags);
    821             dest.writeParcelable(mAfter, flags);
    822             dest.writeString(mIdColumn);
    823         }
    824 
    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