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