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.Data;
28 import android.provider.ContactsContract.Profile;
29 import android.provider.ContactsContract.RawContacts;
30 import android.util.Log;
31 
32 import com.android.contacts.compat.CompatUtils;
33 import com.android.contacts.model.account.AccountType;
34 import com.android.contacts.model.account.AccountWithDataSet;
35 
36 import com.google.common.collect.Lists;
37 import com.google.common.collect.Maps;
38 
39 import java.util.ArrayList;
40 import java.util.HashMap;
41 
42 /**
43  * Contains a {@link RawContact} and records any modifications separately so the
44  * original {@link RawContact} can be swapped out with a newer version and the
45  * changes still cleanly applied.
46  * <p>
47  * One benefit of this approach is that we can build changes entirely on an
48  * empty {@link RawContact}, which then becomes an insert {@link RawContacts} case.
49  * <p>
50  * When applying modifications over an {@link RawContact}, we try finding the
51  * original {@link Data#_ID} rows where the modifications took place. If those
52  * rows are missing from the new {@link RawContact}, we know the original data must
53  * be deleted, but to preserve the user modifications we treat as an insert.
54  */
55 public class RawContactDelta implements Parcelable {
56     // TODO: optimize by using contentvalues pool, since we allocate so many of them
57 
58     private static final String TAG = "EntityDelta";
59     private static final boolean DEBUG = false;
60 
61     /**
62      * Direct values from {@link Entity#getEntityValues()}.
63      */
64     private ValuesDelta mValues;
65 
66     /**
67      * URI used for contacts queries, by default it is set to query raw contacts.
68      * It can be set to query the profile's raw contact(s).
69      */
70     private Uri mContactsQueryUri = RawContacts.CONTENT_URI;
71 
72     /**
73      * Internal map of children values from {@link Entity#getSubValues()}, which
74      * we store here sorted into {@link Data#MIMETYPE} bins.
75      */
76     private final HashMap<String, ArrayList<ValuesDelta>> mEntries = Maps.newHashMap();
77 
RawContactDelta()78     public RawContactDelta() {
79     }
80 
RawContactDelta(ValuesDelta values)81     public RawContactDelta(ValuesDelta values) {
82         mValues = values;
83     }
84 
85     /**
86      * Build an {@link RawContactDelta} using the given {@link RawContact} as a
87      * starting point; the "before" snapshot.
88      */
fromBefore(RawContact before)89     public static RawContactDelta fromBefore(RawContact before) {
90         final RawContactDelta rawContactDelta = new RawContactDelta();
91         rawContactDelta.mValues = ValuesDelta.fromBefore(before.getValues());
92         rawContactDelta.mValues.setIdColumn(RawContacts._ID);
93         for (final ContentValues values : before.getContentValues()) {
94             rawContactDelta.addEntry(ValuesDelta.fromBefore(values));
95         }
96         return rawContactDelta;
97     }
98 
99     /**
100      * Merge the "after" values from the given {@link RawContactDelta} onto the
101      * "before" state represented by this {@link RawContactDelta}, discarding any
102      * existing "after" states. This is typically used when re-parenting changes
103      * onto an updated {@link Entity}.
104      */
mergeAfter(RawContactDelta local, RawContactDelta remote)105     public static RawContactDelta mergeAfter(RawContactDelta local, RawContactDelta remote) {
106         // Bail early if trying to merge delete with missing local
107         final ValuesDelta remoteValues = remote.mValues;
108         if (local == null && (remoteValues.isDelete() || remoteValues.isTransient())) return null;
109 
110         // Create local version if none exists yet
111         if (local == null) local = new RawContactDelta();
112 
113         if (DEBUG) {
114             final Long localVersion = (local.mValues == null) ? null : local.mValues
115                     .getAsLong(RawContacts.VERSION);
116             final Long remoteVersion = remote.mValues.getAsLong(RawContacts.VERSION);
117             Log.d(TAG, "Re-parenting from original version " + remoteVersion + " to "
118                     + localVersion);
119         }
120 
121         // Create values if needed, and merge "after" changes
122         local.mValues = ValuesDelta.mergeAfter(local.mValues, remote.mValues);
123 
124         // Find matching local entry for each remote values, or create
125         for (ArrayList<ValuesDelta> mimeEntries : remote.mEntries.values()) {
126             for (ValuesDelta remoteEntry : mimeEntries) {
127                 final Long childId = remoteEntry.getId();
128 
129                 // Find or create local match and merge
130                 final ValuesDelta localEntry = local.getEntry(childId);
131                 final ValuesDelta merged = ValuesDelta.mergeAfter(localEntry, remoteEntry);
132 
133                 if (localEntry == null && merged != null) {
134                     // No local entry before, so insert
135                     local.addEntry(merged);
136                 }
137             }
138         }
139 
140         return local;
141     }
142 
getValues()143     public ValuesDelta getValues() {
144         return mValues;
145     }
146 
isContactInsert()147     public boolean isContactInsert() {
148         return mValues.isInsert();
149     }
150 
151     /**
152      * Get the {@link ValuesDelta} child marked as {@link Data#IS_PRIMARY},
153      * which may return null when no entry exists.
154      */
getPrimaryEntry(String mimeType)155     public ValuesDelta getPrimaryEntry(String mimeType) {
156         final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false);
157         if (mimeEntries == null) return null;
158 
159         for (ValuesDelta entry : mimeEntries) {
160             if (entry.isPrimary()) {
161                 return entry;
162             }
163         }
164 
165         // When no direct primary, return something
166         return mimeEntries.size() > 0 ? mimeEntries.get(0) : null;
167     }
168 
169     /**
170      * calls {@link #getSuperPrimaryEntry(String, boolean)} with true
171      * @see #getSuperPrimaryEntry(String, boolean)
172      */
getSuperPrimaryEntry(String mimeType)173     public ValuesDelta getSuperPrimaryEntry(String mimeType) {
174         return getSuperPrimaryEntry(mimeType, true);
175     }
176 
177     /**
178      * Returns the super-primary entry for the given mime type
179      * @param forceSelection if true, will try to return some value even if a super-primary
180      *     doesn't exist (may be a primary, or just a random item
181      * @return
182      */
getSuperPrimaryEntry(String mimeType, boolean forceSelection)183     public ValuesDelta getSuperPrimaryEntry(String mimeType, boolean forceSelection) {
184         final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false);
185         if (mimeEntries == null) return null;
186 
187         ValuesDelta primary = null;
188         for (ValuesDelta entry : mimeEntries) {
189             if (entry.isSuperPrimary()) {
190                 return entry;
191             } else if (entry.isPrimary()) {
192                 primary = entry;
193             }
194         }
195 
196         if (!forceSelection) {
197             return null;
198         }
199 
200         // When no direct super primary, return something
201         if (primary != null) {
202             return primary;
203         }
204         return mimeEntries.size() > 0 ? mimeEntries.get(0) : null;
205     }
206 
207     /**
208      * Return the AccountType that this raw-contact belongs to.
209      */
getRawContactAccountType(Context context)210     public AccountType getRawContactAccountType(Context context) {
211         ContentValues entityValues = getValues().getCompleteValues();
212         String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
213         String dataSet = entityValues.getAsString(RawContacts.DATA_SET);
214         return AccountTypeManager.getInstance(context).getAccountType(type, dataSet);
215     }
216 
getRawContactId()217     public Long getRawContactId() {
218         return getValues().getAsLong(RawContacts._ID);
219     }
220 
getAccountName()221     public String getAccountName() {
222         return getValues().getAsString(RawContacts.ACCOUNT_NAME);
223     }
224 
getAccountType()225     public String getAccountType() {
226         return getValues().getAsString(RawContacts.ACCOUNT_TYPE);
227     }
228 
getDataSet()229     public String getDataSet() {
230         return getValues().getAsString(RawContacts.DATA_SET);
231     }
232 
getAccountType(AccountTypeManager manager)233     public AccountType getAccountType(AccountTypeManager manager) {
234         return manager.getAccountType(getAccountType(), getDataSet());
235     }
236 
getAccountWithDataSet()237     public AccountWithDataSet getAccountWithDataSet() {
238         return new AccountWithDataSet(getAccountName(), getAccountType(), getDataSet());
239     }
240 
isVisible()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      */
getMimeEntries(String mimeType, boolean lazyCreate)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 
getMimeEntries(String mimeType)258     public ArrayList<ValuesDelta> getMimeEntries(String mimeType) {
259         return getMimeEntries(mimeType, false);
260     }
261 
getMimeEntriesCount(String mimeType, boolean onlyVisible)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 
hasMimeEntries(String mimeType)275     public boolean hasMimeEntries(String mimeType) {
276         return mEntries.containsKey(mimeType);
277     }
278 
addEntry(ValuesDelta entry)279     public ValuesDelta addEntry(ValuesDelta entry) {
280         final String mimeType = entry.getMimetype();
281         getMimeEntries(mimeType, true).add(entry);
282         return entry;
283     }
284 
getContentValues()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      */
getEntry(Long childId)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      */
getEntryCount(boolean onlyVisible)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
equals(Object object)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 
containsEntry(ValuesDelta entry)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      */
markDeleted()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
toString()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      */
possibleAdd(ArrayList<ContentProviderOperation> diff, ContentProviderOperation.Builder builder)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      */
possibleAddWrapper(ArrayList<CPOWrapper> diff, BuilderWrapper bw)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      */
buildAssert(ArrayList<ContentProviderOperation> buildInto)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      */
buildAssertWrapper(ArrayList<CPOWrapper> buildInto)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 
buildAssertHelper()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      */
buildDiff(ArrayList<ContentProviderOperation> buildInto)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      */
buildDiffWrapper(ArrayList<CPOWrapper> buildInto)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      */
buildSetAggregationMode(Long beforeId, int mode)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} */
describeContents()611     public int describeContents() {
612         // Nothing special about this parcel
613         return 0;
614     }
615 
616     /** {@inheritDoc} */
writeToParcel(Parcel dest, int flags)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 
readFromParcel(Parcel source)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      */
setProfileQueryUri()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