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 
RawContactDelta()81     public RawContactDelta() {
82     }
83 
RawContactDelta(ValuesDelta values)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      */
fromBefore(RawContact before)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      */
mergeAfter(RawContactDelta local, RawContactDelta remote)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 
getValues()146     public ValuesDelta getValues() {
147         return mValues;
148     }
149 
isContactInsert()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      */
getPrimaryEntry(String mimeType)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      */
getSuperPrimaryEntry(String mimeType)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
getSuperPrimaryEntry(String mimeType, boolean forceSelection)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      */
getRawContactAccountType(Context context)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 
getRawContactId()221     public Long getRawContactId() {
222         return getValues().getAsLong(RawContacts._ID);
223     }
224 
getAccountName()225     public String getAccountName() {
226         return getValues().getAsString(RawContacts.ACCOUNT_NAME);
227     }
228 
getAccountType()229     public String getAccountType() {
230         return getValues().getAsString(RawContacts.ACCOUNT_TYPE);
231     }
232 
getDataSet()233     public String getDataSet() {
234         return getValues().getAsString(RawContacts.DATA_SET);
235     }
236 
getAccountType(AccountTypeManager manager)237     public AccountType getAccountType(AccountTypeManager manager) {
238         return manager.getAccountType(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