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.common.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.Data;
     28 import android.provider.ContactsContract.Profile;
     29 import android.provider.ContactsContract.RawContacts;
     30 import android.util.Log;
     31 
     32 import com.android.contacts.common.compat.CompatUtils;
     33 import com.android.contacts.common.model.AccountTypeManager;
     34 import com.android.contacts.common.model.BuilderWrapper;
     35 import com.android.contacts.common.model.CPOWrapper;
     36 import com.android.contacts.common.model.ValuesDelta;
     37 import com.android.contacts.common.model.account.AccountType;
     38 import com.android.contacts.common.testing.NeededForTesting;
     39 import com.google.common.collect.Lists;
     40 import com.google.common.collect.Maps;
     41 
     42 import java.util.ArrayList;
     43 import java.util.HashMap;
     44 
     45 /**
     46  * Contains a {@link RawContact} and records any modifications separately so the
     47  * original {@link RawContact} 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 RawContact}, which then becomes an insert {@link RawContacts} case.
     52  * <p>
     53  * When applying modifications over an {@link RawContact}, 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 RawContact}, we know the original data must
     56  * be deleted, but to preserve the user modifications we treat as an insert.
     57  */
     58 public class RawContactDelta 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 final HashMap<String, ArrayList<ValuesDelta>> mEntries = Maps.newHashMap();
     80 
     81     public RawContactDelta() {
     82     }
     83 
     84     public RawContactDelta(ValuesDelta values) {
     85         mValues = values;
     86     }
     87 
     88     /**
     89      * Build an {@link RawContactDelta} using the given {@link RawContact} as a
     90      * starting point; the "before" snapshot.
     91      */
     92     public static RawContactDelta fromBefore(RawContact before) {
     93         final RawContactDelta rawContactDelta = new RawContactDelta();
     94         rawContactDelta.mValues = ValuesDelta.fromBefore(before.getValues());
     95         rawContactDelta.mValues.setIdColumn(RawContacts._ID);
     96         for (final ContentValues values : before.getContentValues()) {
     97             rawContactDelta.addEntry(ValuesDelta.fromBefore(values));
     98         }
     99         return rawContactDelta;
    100     }
    101 
    102     /**
    103      * Merge the "after" values from the given {@link RawContactDelta} onto the
    104      * "before" state represented by this {@link RawContactDelta}, discarding any
    105      * existing "after" states. This is typically used when re-parenting changes
    106      * onto an updated {@link Entity}.
    107      */
    108     public static RawContactDelta mergeAfter(RawContactDelta local, RawContactDelta 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 RawContactDelta();
    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     @NeededForTesting
    187     public ValuesDelta getSuperPrimaryEntry(String mimeType, boolean forceSelection) {
    188         final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false);
    189         if (mimeEntries == null) return null;
    190 
    191         ValuesDelta primary = null;
    192         for (ValuesDelta entry : mimeEntries) {
    193             if (entry.isSuperPrimary()) {
    194                 return entry;
    195             } else if (entry.isPrimary()) {
    196                 primary = entry;
    197             }
    198         }
    199 
    200         if (!forceSelection) {
    201             return null;
    202         }
    203 
    204         // When no direct super primary, return something
    205         if (primary != null) {
    206             return primary;
    207         }
    208         return mimeEntries.size() > 0 ? mimeEntries.get(0) : null;
    209     }
    210 
    211     /**
    212      * Return the AccountType that this raw-contact belongs to.
    213      */
    214     public AccountType getRawContactAccountType(Context context) {
    215         ContentValues entityValues = getValues().getCompleteValues();
    216         String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
    217         String dataSet = entityValues.getAsString(RawContacts.DATA_SET);
    218         return AccountTypeManager.getInstance(context).getAccountType(type, dataSet);
    219     }
    220 
    221     public Long getRawContactId() {
    222         return getValues().getAsLong(RawContacts._ID);
    223     }
    224 
    225     public String getAccountName() {
    226         return getValues().getAsString(RawContacts.ACCOUNT_NAME);
    227     }
    228 
    229     public String getAccountType() {
    230         return getValues().getAsString(RawContacts.ACCOUNT_TYPE);
    231     }
    232 
    233     public String getDataSet() {
    234         return getValues().getAsString(RawContacts.DATA_SET);
    235     }
    236 
    237     public AccountType getAccountType(AccountTypeManager manager) {
    238         return manager.getAccountType(getAccountType(), getDataSet());
    239     }
    240 
    241     public boolean isVisible() {
    242         return getValues().isVisible();
    243     }
    244 
    245     /**
    246      * Return the list of child {@link ValuesDelta} from our optimized map,
    247      * creating the list if requested.
    248      */
    249     private ArrayList<ValuesDelta> getMimeEntries(String mimeType, boolean lazyCreate) {
    250         ArrayList<ValuesDelta> mimeEntries = mEntries.get(mimeType);
    251         if (mimeEntries == null && lazyCreate) {
    252             mimeEntries = Lists.newArrayList();
    253             mEntries.put(mimeType, mimeEntries);
    254         }
    255         return mimeEntries;
    256     }
    257 
    258     public ArrayList<ValuesDelta> getMimeEntries(String mimeType) {
    259         return getMimeEntries(mimeType, false);
    260     }
    261 
    262     public int getMimeEntriesCount(String mimeType, boolean onlyVisible) {
    263         final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType);
    264         if (mimeEntries == null) return 0;
    265 
    266         int count = 0;
    267         for (ValuesDelta child : mimeEntries) {
    268             // Skip deleted items when requesting only visible
    269             if (onlyVisible && !child.isVisible()) continue;
    270             count++;
    271         }
    272         return count;
    273     }
    274 
    275     public boolean hasMimeEntries(String mimeType) {
    276         return mEntries.containsKey(mimeType);
    277     }
    278 
    279     public ValuesDelta addEntry(ValuesDelta entry) {
    280         final String mimeType = entry.getMimetype();
    281         getMimeEntries(mimeType, true).add(entry);
    282         return entry;
    283     }
    284 
    285     public ArrayList<ContentValues> getContentValues() {
    286         ArrayList<ContentValues> values = Lists.newArrayList();
    287         for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
    288             for (ValuesDelta entry : mimeEntries) {
    289                 if (!entry.isDelete()) {
    290                     values.add(entry.getCompleteValues());
    291                 }
    292             }
    293         }
    294         return values;
    295     }
    296 
    297     /**
    298      * Find entry with the given {@link BaseColumns#_ID} value.
    299      */
    300     public ValuesDelta getEntry(Long childId) {
    301         if (childId == null) {
    302             // Requesting an "insert" entry, which has no "before"
    303             return null;
    304         }
    305 
    306         // Search all children for requested entry
    307         for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
    308             for (ValuesDelta entry : mimeEntries) {
    309                 if (childId.equals(entry.getId())) {
    310                     return entry;
    311                 }
    312             }
    313         }
    314         return null;
    315     }
    316 
    317     /**
    318      * Return the total number of {@link ValuesDelta} contained.
    319      */
    320     public int getEntryCount(boolean onlyVisible) {
    321         int count = 0;
    322         for (String mimeType : mEntries.keySet()) {
    323             count += getMimeEntriesCount(mimeType, onlyVisible);
    324         }
    325         return count;
    326     }
    327 
    328     @Override
    329     public boolean equals(Object object) {
    330         if (object instanceof RawContactDelta) {
    331             final RawContactDelta other = (RawContactDelta)object;
    332 
    333             // Equality failed if parent values different
    334             if (!other.mValues.equals(mValues)) return false;
    335 
    336             for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
    337                 for (ValuesDelta child : mimeEntries) {
    338                     // Equality failed if any children unmatched
    339                     if (!other.containsEntry(child)) return false;
    340                 }
    341             }
    342 
    343             // Passed all tests, so equal
    344             return true;
    345         }
    346         return false;
    347     }
    348 
    349     private boolean containsEntry(ValuesDelta entry) {
    350         for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
    351             for (ValuesDelta child : mimeEntries) {
    352                 // Contained if we find any child that matches
    353                 if (child.equals(entry)) return true;
    354             }
    355         }
    356         return false;
    357     }
    358 
    359     /**
    360      * Mark this entire object deleted, including any {@link ValuesDelta}.
    361      */
    362     public void markDeleted() {
    363         this.mValues.markDeleted();
    364         for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
    365             for (ValuesDelta child : mimeEntries) {
    366                 child.markDeleted();
    367             }
    368         }
    369     }
    370 
    371     @Override
    372     public String toString() {
    373         final StringBuilder builder = new StringBuilder();
    374         builder.append("\n(");
    375         builder.append("Uri=");
    376         builder.append(mContactsQueryUri);
    377         builder.append(", Values=");
    378         builder.append(mValues != null ? mValues.toString() : "null");
    379         builder.append(", Entries={");
    380         for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
    381             for (ValuesDelta child : mimeEntries) {
    382                 builder.append("\n\t");
    383                 child.toString(builder);
    384             }
    385         }
    386         builder.append("\n})\n");
    387         return builder.toString();
    388     }
    389 
    390     /**
    391      * Consider building the given {@link ContentProviderOperation.Builder} and
    392      * appending it to the given list, which only happens if builder is valid.
    393      */
    394     private void possibleAdd(ArrayList<ContentProviderOperation> diff,
    395             ContentProviderOperation.Builder builder) {
    396         if (builder != null) {
    397             diff.add(builder.build());
    398         }
    399     }
    400 
    401     /**
    402      * For compatibility purpose, this method is copied from {@link #possibleAdd} and takes
    403      * BuilderWrapper and an ArrayList of CPOWrapper as parameters.
    404      */
    405     private void possibleAddWrapper(ArrayList<CPOWrapper> diff, BuilderWrapper bw) {
    406         if (bw != null && bw.getBuilder() != null) {
    407             diff.add(new CPOWrapper(bw.getBuilder().build(), bw.getType()));
    408         }
    409     }
    410 
    411     /**
    412      * Build a list of {@link ContentProviderOperation} that will assert any
    413      * "before" state hasn't changed. This is maintained separately so that all
    414      * asserts can take place before any updates occur.
    415      */
    416     public void buildAssert(ArrayList<ContentProviderOperation> buildInto) {
    417         final Builder builder = buildAssertHelper();
    418         if (builder != null) {
    419             buildInto.add(builder.build());
    420         }
    421     }
    422 
    423     /**
    424      * For compatibility purpose, this method is copied from {@link #buildAssert} and takes an
    425      * ArrayList of CPOWrapper as parameter.
    426      */
    427     public void buildAssertWrapper(ArrayList<CPOWrapper> buildInto) {
    428         final Builder builder = buildAssertHelper();
    429         if (builder != null) {
    430             buildInto.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_ASSERT));
    431         }
    432     }
    433 
    434     private Builder buildAssertHelper() {
    435         final boolean isContactInsert = mValues.isInsert();
    436         ContentProviderOperation.Builder builder = null;
    437         if (!isContactInsert) {
    438             // Assert version is consistent while persisting changes
    439             final Long beforeId = mValues.getId();
    440             final Long beforeVersion = mValues.getAsLong(RawContacts.VERSION);
    441             if (beforeId == null || beforeVersion == null) return builder;
    442             builder = ContentProviderOperation.newAssertQuery(mContactsQueryUri);
    443             builder.withSelection(RawContacts._ID + "=" + beforeId, null);
    444             builder.withValue(RawContacts.VERSION, beforeVersion);
    445         }
    446         return builder;
    447     }
    448 
    449     /**
    450      * Build a list of {@link ContentProviderOperation} that will transform the
    451      * current "before" {@link Entity} state into the modified state which this
    452      * {@link RawContactDelta} represents.
    453      */
    454     public void buildDiff(ArrayList<ContentProviderOperation> buildInto) {
    455         final int firstIndex = buildInto.size();
    456 
    457         final boolean isContactInsert = mValues.isInsert();
    458         final boolean isContactDelete = mValues.isDelete();
    459         final boolean isContactUpdate = !isContactInsert && !isContactDelete;
    460 
    461         final Long beforeId = mValues.getId();
    462 
    463         Builder builder;
    464 
    465         if (isContactInsert) {
    466             // TODO: for now simply disabling aggregation when a new contact is
    467             // created on the phone.  In the future, will show aggregation suggestions
    468             // after saving the contact.
    469             mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED);
    470         }
    471 
    472         // Build possible operation at Contact level
    473         builder = mValues.buildDiff(mContactsQueryUri);
    474         possibleAdd(buildInto, builder);
    475 
    476         // Build operations for all children
    477         for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
    478             for (ValuesDelta child : mimeEntries) {
    479                 // Ignore children if parent was deleted
    480                 if (isContactDelete) continue;
    481 
    482                 // Use the profile data URI if the contact is the profile.
    483                 if (mContactsQueryUri.equals(Profile.CONTENT_RAW_CONTACTS_URI)) {
    484                     builder = child.buildDiff(Uri.withAppendedPath(Profile.CONTENT_URI,
    485                             RawContacts.Data.CONTENT_DIRECTORY));
    486                 } else {
    487                     builder = child.buildDiff(Data.CONTENT_URI);
    488                 }
    489 
    490                 if (child.isInsert()) {
    491                     if (isContactInsert) {
    492                         // Parent is brand new insert, so back-reference _id
    493                         builder.withValueBackReference(Data.RAW_CONTACT_ID, firstIndex);
    494                     } else {
    495                         // Inserting under existing, so fill with known _id
    496                         builder.withValue(Data.RAW_CONTACT_ID, beforeId);
    497                     }
    498                 } else if (isContactInsert && builder != null) {
    499                     // Child must be insert when Contact insert
    500                     throw new IllegalArgumentException("When parent insert, child must be also");
    501                 }
    502                 possibleAdd(buildInto, builder);
    503             }
    504         }
    505 
    506         final boolean addedOperations = buildInto.size() > firstIndex;
    507         if (addedOperations && isContactUpdate) {
    508             // Suspend aggregation while persisting updates
    509             builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_SUSPENDED);
    510             buildInto.add(firstIndex, builder.build());
    511 
    512             // Restore aggregation mode as last operation
    513             builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_DEFAULT);
    514             buildInto.add(builder.build());
    515         } else if (isContactInsert) {
    516             // Restore aggregation mode as last operation
    517             builder = ContentProviderOperation.newUpdate(mContactsQueryUri);
    518             builder.withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT);
    519             builder.withSelection(RawContacts._ID + "=?", new String[1]);
    520             builder.withSelectionBackReference(0, firstIndex);
    521             buildInto.add(builder.build());
    522         }
    523     }
    524 
    525     /**
    526      * For compatibility purpose, this method is copied from {@link #buildDiff} and takes an
    527      * ArrayList of CPOWrapper as parameter.
    528      */
    529     public void buildDiffWrapper(ArrayList<CPOWrapper> buildInto) {
    530         final int firstIndex = buildInto.size();
    531 
    532         final boolean isContactInsert = mValues.isInsert();
    533         final boolean isContactDelete = mValues.isDelete();
    534         final boolean isContactUpdate = !isContactInsert && !isContactDelete;
    535 
    536         final Long beforeId = mValues.getId();
    537 
    538         if (isContactInsert) {
    539             // TODO: for now simply disabling aggregation when a new contact is
    540             // created on the phone.  In the future, will show aggregation suggestions
    541             // after saving the contact.
    542             mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED);
    543         }
    544 
    545         // Build possible operation at Contact level
    546         BuilderWrapper bw = mValues.buildDiffWrapper(mContactsQueryUri);
    547         possibleAddWrapper(buildInto, bw);
    548 
    549         // Build operations for all children
    550         for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
    551             for (ValuesDelta child : mimeEntries) {
    552                 // Ignore children if parent was deleted
    553                 if (isContactDelete) continue;
    554 
    555                 // Use the profile data URI if the contact is the profile.
    556                 if (mContactsQueryUri.equals(Profile.CONTENT_RAW_CONTACTS_URI)) {
    557                     bw = child.buildDiffWrapper(Uri.withAppendedPath(Profile.CONTENT_URI,
    558                             RawContacts.Data.CONTENT_DIRECTORY));
    559                 } else {
    560                     bw = child.buildDiffWrapper(Data.CONTENT_URI);
    561                 }
    562 
    563                 if (child.isInsert()) {
    564                     if (isContactInsert) {
    565                         // Parent is brand new insert, so back-reference _id
    566                         bw.getBuilder().withValueBackReference(Data.RAW_CONTACT_ID, firstIndex);
    567                     } else {
    568                         // Inserting under existing, so fill with known _id
    569                         bw.getBuilder().withValue(Data.RAW_CONTACT_ID, beforeId);
    570                     }
    571                 } else if (isContactInsert && bw != null && bw.getBuilder() != null) {
    572                     // Child must be insert when Contact insert
    573                     throw new IllegalArgumentException("When parent insert, child must be also");
    574                 }
    575                 possibleAddWrapper(buildInto, bw);
    576             }
    577         }
    578 
    579         final boolean addedOperations = buildInto.size() > firstIndex;
    580         if (addedOperations && isContactUpdate) {
    581             // Suspend aggregation while persisting updates
    582             Builder builder =
    583                     buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_SUSPENDED);
    584             buildInto.add(firstIndex, new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE));
    585 
    586             // Restore aggregation mode as last operation
    587             builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_DEFAULT);
    588             buildInto.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE));
    589         } else if (isContactInsert) {
    590             // Restore aggregation mode as last operation
    591             Builder builder = ContentProviderOperation.newUpdate(mContactsQueryUri);
    592             builder.withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT);
    593             builder.withSelection(RawContacts._ID + "=?", new String[1]);
    594             builder.withSelectionBackReference(0, firstIndex);
    595             buildInto.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE));
    596         }
    597     }
    598 
    599     /**
    600      * Build a {@link ContentProviderOperation} that changes
    601      * {@link RawContacts#AGGREGATION_MODE} to the given value.
    602      */
    603     protected Builder buildSetAggregationMode(Long beforeId, int mode) {
    604         Builder builder = ContentProviderOperation.newUpdate(mContactsQueryUri);
    605         builder.withValue(RawContacts.AGGREGATION_MODE, mode);
    606         builder.withSelection(RawContacts._ID + "=" + beforeId, null);
    607         return builder;
    608     }
    609 
    610     /** {@inheritDoc} */
    611     public int describeContents() {
    612         // Nothing special about this parcel
    613         return 0;
    614     }
    615 
    616     /** {@inheritDoc} */
    617     public void writeToParcel(Parcel dest, int flags) {
    618         final int size = this.getEntryCount(false);
    619         dest.writeInt(size);
    620         dest.writeParcelable(mValues, flags);
    621         dest.writeParcelable(mContactsQueryUri, flags);
    622         for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
    623             for (ValuesDelta child : mimeEntries) {
    624                 dest.writeParcelable(child, flags);
    625             }
    626         }
    627     }
    628 
    629     public void readFromParcel(Parcel source) {
    630         final ClassLoader loader = getClass().getClassLoader();
    631         final int size = source.readInt();
    632         mValues = source.<ValuesDelta> readParcelable(loader);
    633         mContactsQueryUri = source.<Uri> readParcelable(loader);
    634         for (int i = 0; i < size; i++) {
    635             final ValuesDelta child = source.<ValuesDelta> readParcelable(loader);
    636             this.addEntry(child);
    637         }
    638     }
    639 
    640     /**
    641      * Used to set the query URI to the profile URI to store profiles.
    642      */
    643     public void setProfileQueryUri() {
    644         mContactsQueryUri = Profile.CONTENT_RAW_CONTACTS_URI;
    645     }
    646 
    647     public static final Parcelable.Creator<RawContactDelta> CREATOR =
    648             new Parcelable.Creator<RawContactDelta>() {
    649         public RawContactDelta createFromParcel(Parcel in) {
    650             final RawContactDelta state = new RawContactDelta();
    651             state.readFromParcel(in);
    652             return state;
    653         }
    654 
    655         public RawContactDelta[] newArray(int size) {
    656             return new RawContactDelta[size];
    657         }
    658     };
    659 
    660 }
    661