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