• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
RawContactDelta()85     public RawContactDelta() {
86     }
87 
RawContactDelta(ValuesDelta values)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      */
fromBefore(RawContact before)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      */
mergeAfter(RawContactDelta local, RawContactDelta remote)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 
getValues()150     public ValuesDelta getValues() {
151         return mValues;
152     }
153 
isContactInsert()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      */
getPrimaryEntry(String mimeType)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      */
getSuperPrimaryEntry(String mimeType)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
getSuperPrimaryEntry(String mimeType, boolean forceSelection)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      */
getRawContactAccountType(Context context)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 
getRawContactId()225     public Long getRawContactId() {
226         return getValues().getAsLong(RawContacts._ID);
227     }
228 
getAccountName()229     public String getAccountName() {
230         return getValues().getAsString(RawContacts.ACCOUNT_NAME);
231     }
232 
getAccountType()233     public String getAccountType() {
234         return getValues().getAsString(RawContacts.ACCOUNT_TYPE);
235     }
236 
getDataSet()237     public String getDataSet() {
238         return getValues().getAsString(RawContacts.DATA_SET);
239     }
240 
getAccountType(AccountTypeManager manager)241     public AccountType getAccountType(AccountTypeManager manager) {
242         return manager.getAccountType(getAccountType(), getDataSet());
243     }
244 
isVisible()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      */
getMimeEntries(String mimeType, boolean lazyCreate)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 
getMimeEntries(String mimeType)262     public ArrayList<ValuesDelta> getMimeEntries(String mimeType) {
263         return getMimeEntries(mimeType, false);
264     }
265 
getMimeEntriesCount(String mimeType, boolean onlyVisible)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 
hasMimeEntries(String mimeType)279     public boolean hasMimeEntries(String mimeType) {
280         return mEntries.containsKey(mimeType);
281     }
282 
addEntry(ValuesDelta entry)283     public ValuesDelta addEntry(ValuesDelta entry) {
284         final String mimeType = entry.getMimetype();
285         getMimeEntries(mimeType, true).add(entry);
286         return entry;
287     }
288 
getContentValues()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      */
getEntry(Long childId)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      */
getEntryCount(boolean onlyVisible)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
equals(Object object)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 
containsEntry(ValuesDelta entry)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      */
markDeleted()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
toString()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      */
possibleAdd(ArrayList<ContentProviderOperation> diff, ContentProviderOperation.Builder builder)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      */
buildAssert(ArrayList<ContentProviderOperation> buildInto)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      */
buildDiff(ArrayList<ContentProviderOperation> buildInto)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      */
buildSetAggregationMode(Long beforeId, int mode)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} */
describeContents()514     public int describeContents() {
515         // Nothing special about this parcel
516         return 0;
517     }
518 
519     /** {@inheritDoc} */
writeToParcel(Parcel dest, int flags)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 
readFromParcel(Parcel source)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      */
setProfileQueryUri()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 
ValuesDelta()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          */
fromBefore(ContentValues before)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          */
fromAfter(ContentValues after)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
getAfter()611         public ContentValues getAfter() {
612             return mAfter;
613         }
614 
containsKey(String key)615         public boolean containsKey(String key) {
616             return ((mAfter != null && mAfter.containsKey(key)) ||
617                     (mBefore != null && mBefore.containsKey(key)));
618         }
619 
getAsString(String key)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 
getAsByteArray(String key)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 
getAsLong(String key)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 
getAsInteger(String key)650         public Integer getAsInteger(String key) {
651             return getAsInteger(key, null);
652         }
653 
getAsInteger(String key, Integer defaultValue)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 
isChanged(String key)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 
getMimetype()679         public String getMimetype() {
680             return getAsString(Data.MIMETYPE);
681         }
682 
getId()683         public Long getId() {
684             return getAsLong(mIdColumn);
685         }
686 
setIdColumn(String idColumn)687         public void setIdColumn(String idColumn) {
688             mIdColumn = idColumn;
689         }
690 
isPrimary()691         public boolean isPrimary() {
692             final Long isPrimary = getAsLong(Data.IS_PRIMARY);
693             return isPrimary == null ? false : isPrimary != 0;
694         }
695 
setFromTemplate(boolean isFromTemplate)696         public void setFromTemplate(boolean isFromTemplate) {
697             mFromTemplate = isFromTemplate;
698         }
699 
isFromTemplate()700         public boolean isFromTemplate() {
701             return mFromTemplate;
702         }
703 
isSuperPrimary()704         public boolean isSuperPrimary() {
705             final Long isSuperPrimary = getAsLong(Data.IS_SUPER_PRIMARY);
706             return isSuperPrimary == null ? false : isSuperPrimary != 0;
707         }
708 
beforeExists()709         public boolean beforeExists() {
710             return (mBefore != null && mBefore.containsKey(mIdColumn));
711         }
712 
713         /**
714          * When "after" is present, then visible
715          */
isVisible()716         public boolean isVisible() {
717             return (mAfter != null);
718         }
719 
720         /**
721          * When "after" is wiped, action is "delete"
722          */
isDelete()723         public boolean isDelete() {
724             return beforeExists() && (mAfter == null);
725         }
726 
727         /**
728          * When no "before" or "after", is transient
729          */
isTransient()730         public boolean isTransient() {
731             return (mBefore == null) && (mAfter == null);
732         }
733 
734         /**
735          * When "after" has some changes, action is "update"
736          */
isUpdate()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          */
isNoop()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          */
isInsert()765         public boolean isInsert() {
766             return !beforeExists() && (mAfter != null);
767         }
768 
markDeleted()769         public void markDeleted() {
770             mAfter = null;
771         }
772 
773         /**
774          * Ensure that our internal structure is ready for storing updates.
775          */
ensureUpdate()776         private void ensureUpdate() {
777             if (mAfter == null) {
778                 mAfter = new ContentValues();
779             }
780         }
781 
put(String key, String value)782         public void put(String key, String value) {
783             ensureUpdate();
784             mAfter.put(key, value);
785         }
786 
put(String key, byte[] value)787         public void put(String key, byte[] value) {
788             ensureUpdate();
789             mAfter.put(key, value);
790         }
791 
put(String key, int value)792         public void put(String key, int value) {
793             ensureUpdate();
794             mAfter.put(key, value);
795         }
796 
put(String key, long value)797         public void put(String key, long value) {
798             ensureUpdate();
799             mAfter.put(key, value);
800         }
801 
putNull(String key)802         public void putNull(String key) {
803             ensureUpdate();
804             mAfter.putNull(key);
805         }
806 
copyStringFrom(ValuesDelta from, String key)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          */
keySet()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          */
getCompleteValues()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          */
mergeAfter(ValuesDelta local, ValuesDelta remote)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
equals(Object object)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
toString()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          */
toString(StringBuilder builder)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          */
subsetEquals(ValuesDelta other)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          */
buildDiff(Uri targetUri)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} */
describeContents()959         public int describeContents() {
960             // Nothing special about this parcel
961             return 0;
962         }
963 
964         /** {@inheritDoc} */
writeToParcel(Parcel dest, int flags)965         public void writeToParcel(Parcel dest, int flags) {
966             dest.writeParcelable(mBefore, flags);
967             dest.writeParcelable(mAfter, flags);
968             dest.writeString(mIdColumn);
969         }
970 
readFromParcel(Parcel source)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 
setGroupRowId(long groupId)990         public void setGroupRowId(long groupId) {
991             put(GroupMembership.GROUP_ROW_ID, groupId);
992         }
993 
getGroupRowId()994         public Long getGroupRowId() {
995             return getAsLong(GroupMembership.GROUP_ROW_ID);
996         }
997 
setPhoto(byte[] value)998         public void setPhoto(byte[] value) {
999             put(Photo.PHOTO, value);
1000         }
1001 
getPhoto()1002         public byte[] getPhoto() {
1003             return getAsByteArray(Photo.PHOTO);
1004         }
1005 
setSuperPrimary(boolean val)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 
setPhoneticFamilyName(String value)1014         public void setPhoneticFamilyName(String value) {
1015             put(StructuredName.PHONETIC_FAMILY_NAME, value);
1016         }
1017 
setPhoneticMiddleName(String value)1018         public void setPhoneticMiddleName(String value) {
1019             put(StructuredName.PHONETIC_MIDDLE_NAME, value);
1020         }
1021 
setPhoneticGivenName(String value)1022         public void setPhoneticGivenName(String value) {
1023             put(StructuredName.PHONETIC_GIVEN_NAME, value);
1024         }
1025 
getPhoneticFamilyName()1026         public String getPhoneticFamilyName() {
1027             return getAsString(StructuredName.PHONETIC_FAMILY_NAME);
1028         }
1029 
getPhoneticMiddleName()1030         public String getPhoneticMiddleName() {
1031             return getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
1032         }
1033 
getPhoneticGivenName()1034         public String getPhoneticGivenName() {
1035             return getAsString(StructuredName.PHONETIC_GIVEN_NAME);
1036         }
1037 
getDisplayName()1038         public String getDisplayName() {
1039             return getAsString(StructuredName.DISPLAY_NAME);
1040         }
1041 
setDisplayName(String name)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 
copyStructuredNameFieldsFrom(ValuesDelta name)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 
getPhoneNumber()1067         public String getPhoneNumber() {
1068             return getAsString(Phone.NUMBER);
1069         }
1070 
getPhoneNormalizedNumber()1071         public String getPhoneNormalizedNumber() {
1072             return getAsString(Phone.NORMALIZED_NUMBER);
1073         }
1074 
phoneHasType()1075         public boolean phoneHasType() {
1076             return containsKey(Phone.TYPE);
1077         }
1078 
getPhoneType()1079         public int getPhoneType() {
1080             return getAsInteger(Phone.TYPE);
1081         }
1082 
getPhoneLabel()1083         public String getPhoneLabel() {
1084             return getAsString(Phone.LABEL);
1085         }
1086 
getEmailData()1087         public String getEmailData() {
1088             return getAsString(Email.DATA);
1089         }
1090 
emailHasType()1091         public boolean emailHasType() {
1092             return containsKey(Email.TYPE);
1093         }
1094 
getEmailType()1095         public int getEmailType() {
1096             return getAsInteger(Email.TYPE);
1097         }
1098 
getEmailLabel()1099         public String getEmailLabel() {
1100             return getAsString(Email.LABEL);
1101         }
1102     }
1103 }
1104