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.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 
     83     public EntityDelta() {
     84     }
     85 
     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      */
     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      */
    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 
    148     public ValuesDelta getValues() {
    149         return mValues;
    150     }
    151 
    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      */
    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      */
    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
    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      */
    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 
    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      */
    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 
    240     public ArrayList<ValuesDelta> getMimeEntries(String mimeType) {
    241         return getMimeEntries(mimeType, false);
    242     }
    243 
    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 
    257     public boolean hasMimeEntries(String mimeType) {
    258         return mEntries.containsKey(mimeType);
    259     }
    260 
    261     public ValuesDelta addEntry(ValuesDelta entry) {
    262         final String mimeType = entry.getMimetype();
    263         getMimeEntries(mimeType, true).add(entry);
    264         return entry;
    265     }
    266 
    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      */
    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      */
    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
    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 
    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      */
    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
    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      */
    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      */
    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      */
    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      */
    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} */
    492     public int describeContents() {
    493         // Nothing special about this parcel
    494         return 0;
    495     }
    496 
    497     /** {@inheritDoc} */
    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 
    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      */
    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 
    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          */
    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          */
    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
    588         public ContentValues getAfter() {
    589             return mAfter;
    590         }
    591 
    592         public boolean containsKey(String key) {
    593             return ((mAfter != null && mAfter.containsKey(key)) ||
    594                     (mBefore != null && mBefore.containsKey(key)));
    595         }
    596 
    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 
    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 
    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 
    627         public Integer getAsInteger(String key) {
    628             return getAsInteger(key, null);
    629         }
    630 
    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 
    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 
    656         public String getMimetype() {
    657             return getAsString(Data.MIMETYPE);
    658         }
    659 
    660         public Long getId() {
    661             return getAsLong(mIdColumn);
    662         }
    663 
    664         public void setIdColumn(String idColumn) {
    665             mIdColumn = idColumn;
    666         }
    667 
    668         public boolean isPrimary() {
    669             final Long isPrimary = getAsLong(Data.IS_PRIMARY);
    670             return isPrimary == null ? false : isPrimary != 0;
    671         }
    672 
    673         public void setFromTemplate(boolean isFromTemplate) {
    674             mFromTemplate = isFromTemplate;
    675         }
    676 
    677         public boolean isFromTemplate() {
    678             return mFromTemplate;
    679         }
    680 
    681         public boolean isSuperPrimary() {
    682             final Long isSuperPrimary = getAsLong(Data.IS_SUPER_PRIMARY);
    683             return isSuperPrimary == null ? false : isSuperPrimary != 0;
    684         }
    685 
    686         public boolean beforeExists() {
    687             return (mBefore != null && mBefore.containsKey(mIdColumn));
    688         }
    689 
    690         /**
    691          * When "after" is present, then visible
    692          */
    693         public boolean isVisible() {
    694             return (mAfter != null);
    695         }
    696 
    697         /**
    698          * When "after" is wiped, action is "delete"
    699          */
    700         public boolean isDelete() {
    701             return beforeExists() && (mAfter == null);
    702         }
    703 
    704         /**
    705          * When no "before" or "after", is transient
    706          */
    707         public boolean isTransient() {
    708             return (mBefore == null) && (mAfter == null);
    709         }
    710 
    711         /**
    712          * When "after" has some changes, action is "update"
    713          */
    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          */
    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          */
    742         public boolean isInsert() {
    743             return !beforeExists() && (mAfter != null);
    744         }
    745 
    746         public void markDeleted() {
    747             mAfter = null;
    748         }
    749 
    750         /**
    751          * Ensure that our internal structure is ready for storing updates.
    752          */
    753         private void ensureUpdate() {
    754             if (mAfter == null) {
    755                 mAfter = new ContentValues();
    756             }
    757         }
    758 
    759         public void put(String key, String value) {
    760             ensureUpdate();
    761             mAfter.put(key, value);
    762         }
    763 
    764         public void put(String key, byte[] value) {
    765             ensureUpdate();
    766             mAfter.put(key, value);
    767         }
    768 
    769         public void put(String key, int value) {
    770             ensureUpdate();
    771             mAfter.put(key, value);
    772         }
    773 
    774         public void put(String key, long value) {
    775             ensureUpdate();
    776             mAfter.put(key, value);
    777         }
    778 
    779         public void putNull(String key) {
    780             ensureUpdate();
    781             mAfter.putNull(key);
    782         }
    783 
    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          */
    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          */
    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          */
    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
    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
    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          */
    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          */
    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          */
    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} */
    936         public int describeContents() {
    937             // Nothing special about this parcel
    938             return 0;
    939         }
    940 
    941         /** {@inheritDoc} */
    942         public void writeToParcel(Parcel dest, int flags) {
    943             dest.writeParcelable(mBefore, flags);
    944             dest.writeParcelable(mAfter, flags);
    945             dest.writeString(mIdColumn);
    946         }
    947 
    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