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