1/*
2 * Copyright (C) 2012 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
17package com.android.contacts.common.model;
18
19import android.content.ContentProviderOperation;
20import android.content.ContentValues;
21import android.net.Uri;
22import android.os.Parcel;
23import android.os.Parcelable;
24import android.provider.BaseColumns;
25import android.provider.ContactsContract;
26
27import com.android.contacts.common.compat.CompatUtils;
28import com.android.contacts.common.model.BuilderWrapper;
29import com.android.contacts.common.testing.NeededForTesting;
30import com.google.common.collect.Sets;
31
32import java.util.HashSet;
33import java.util.Map;
34import java.util.Set;
35
36/**
37 * Type of {@link android.content.ContentValues} that maintains both an original state and a
38 * modified version of that state. This allows us to build insert, update,
39 * or delete operations based on a "before" {@link Entity} snapshot.
40 */
41public class ValuesDelta implements Parcelable {
42    protected ContentValues mBefore;
43    protected ContentValues mAfter;
44    protected String mIdColumn = BaseColumns._ID;
45    private boolean mFromTemplate;
46
47    /**
48     * Next value to assign to {@link #mIdColumn} when building an insert
49     * operation through {@link #fromAfter(android.content.ContentValues)}. This is used so
50     * we can concretely reference this {@link ValuesDelta} before it has
51     * been persisted.
52     */
53    protected static int sNextInsertId = -1;
54
55    protected ValuesDelta() {
56    }
57
58    /**
59     * Create {@link ValuesDelta}, using the given object as the
60     * "before" state, usually from an {@link Entity}.
61     */
62    public static ValuesDelta fromBefore(ContentValues before) {
63        final ValuesDelta entry = new ValuesDelta();
64        entry.mBefore = before;
65        entry.mAfter = new ContentValues();
66        return entry;
67    }
68
69    /**
70     * Create {@link ValuesDelta}, using the given object as the "after"
71     * state, usually when we are inserting a row instead of updating.
72     */
73    public static ValuesDelta fromAfter(ContentValues after) {
74        final ValuesDelta entry = new ValuesDelta();
75        entry.mBefore = null;
76        entry.mAfter = after;
77
78        // Assign temporary id which is dropped before insert.
79        entry.mAfter.put(entry.mIdColumn, sNextInsertId--);
80        return entry;
81    }
82
83    @NeededForTesting
84    public ContentValues getAfter() {
85        return mAfter;
86    }
87
88    public boolean containsKey(String key) {
89        return ((mAfter != null && mAfter.containsKey(key)) ||
90                (mBefore != null && mBefore.containsKey(key)));
91    }
92
93    public String getAsString(String key) {
94        if (mAfter != null && mAfter.containsKey(key)) {
95            return mAfter.getAsString(key);
96        } else if (mBefore != null && mBefore.containsKey(key)) {
97            return mBefore.getAsString(key);
98        } else {
99            return null;
100        }
101    }
102
103    public byte[] getAsByteArray(String key) {
104        if (mAfter != null && mAfter.containsKey(key)) {
105            return mAfter.getAsByteArray(key);
106        } else if (mBefore != null && mBefore.containsKey(key)) {
107            return mBefore.getAsByteArray(key);
108        } else {
109            return null;
110        }
111    }
112
113    public Long getAsLong(String key) {
114        if (mAfter != null && mAfter.containsKey(key)) {
115            return mAfter.getAsLong(key);
116        } else if (mBefore != null && mBefore.containsKey(key)) {
117            return mBefore.getAsLong(key);
118        } else {
119            return null;
120        }
121    }
122
123    public Integer getAsInteger(String key) {
124        return getAsInteger(key, null);
125    }
126
127    public Integer getAsInteger(String key, Integer defaultValue) {
128        if (mAfter != null && mAfter.containsKey(key)) {
129            return mAfter.getAsInteger(key);
130        } else if (mBefore != null && mBefore.containsKey(key)) {
131            return mBefore.getAsInteger(key);
132        } else {
133            return defaultValue;
134        }
135    }
136
137    public boolean isChanged(String key) {
138        if (mAfter == null || !mAfter.containsKey(key)) {
139            return false;
140        }
141
142        Object newValue = mAfter.get(key);
143        Object oldValue = mBefore.get(key);
144
145        if (oldValue == null) {
146            return newValue != null;
147        }
148
149        return !oldValue.equals(newValue);
150    }
151
152    public String getMimetype() {
153        return getAsString(ContactsContract.Data.MIMETYPE);
154    }
155
156    public Long getId() {
157        return getAsLong(mIdColumn);
158    }
159
160    public void setIdColumn(String idColumn) {
161        mIdColumn = idColumn;
162    }
163
164    public boolean isPrimary() {
165        final Long isPrimary = getAsLong(ContactsContract.Data.IS_PRIMARY);
166        return isPrimary == null ? false : isPrimary != 0;
167    }
168
169    public void setFromTemplate(boolean isFromTemplate) {
170        mFromTemplate = isFromTemplate;
171    }
172
173    public boolean isFromTemplate() {
174        return mFromTemplate;
175    }
176
177    public boolean isSuperPrimary() {
178        final Long isSuperPrimary = getAsLong(ContactsContract.Data.IS_SUPER_PRIMARY);
179        return isSuperPrimary == null ? false : isSuperPrimary != 0;
180    }
181
182    public boolean beforeExists() {
183        return (mBefore != null && mBefore.containsKey(mIdColumn));
184    }
185
186    /**
187     * When "after" is present, then visible
188     */
189    public boolean isVisible() {
190        return (mAfter != null);
191    }
192
193    /**
194     * When "after" is wiped, action is "delete"
195     */
196    public boolean isDelete() {
197        return beforeExists() && (mAfter == null);
198    }
199
200    /**
201     * When no "before" or "after", is transient
202     */
203    public boolean isTransient() {
204        return (mBefore == null) && (mAfter == null);
205    }
206
207    /**
208     * When "after" has some changes, action is "update"
209     */
210    public boolean isUpdate() {
211        if (!beforeExists() || mAfter == null || mAfter.size() == 0) {
212            return false;
213        }
214        for (String key : mAfter.keySet()) {
215            Object newValue = mAfter.get(key);
216            Object oldValue = mBefore.get(key);
217            if (oldValue == null) {
218                if (newValue != null) {
219                    return true;
220                }
221            } else if (!oldValue.equals(newValue)) {
222                return true;
223            }
224        }
225        return false;
226    }
227
228    /**
229     * When "after" has no changes, action is no-op
230     */
231    public boolean isNoop() {
232        return beforeExists() && (mAfter != null && mAfter.size() == 0);
233    }
234
235    /**
236     * When no "before" id, and has "after", action is "insert"
237     */
238    public boolean isInsert() {
239        return !beforeExists() && (mAfter != null);
240    }
241
242    public void markDeleted() {
243        mAfter = null;
244    }
245
246    /**
247     * Ensure that our internal structure is ready for storing updates.
248     */
249    private void ensureUpdate() {
250        if (mAfter == null) {
251            mAfter = new ContentValues();
252        }
253    }
254
255    public void put(String key, String value) {
256        ensureUpdate();
257        mAfter.put(key, value);
258    }
259
260    public void put(String key, byte[] value) {
261        ensureUpdate();
262        mAfter.put(key, value);
263    }
264
265    public void put(String key, int value) {
266        ensureUpdate();
267        mAfter.put(key, value);
268    }
269
270    public void put(String key, long value) {
271        ensureUpdate();
272        mAfter.put(key, value);
273    }
274
275    public void putNull(String key) {
276        ensureUpdate();
277        mAfter.putNull(key);
278    }
279
280    public void copyStringFrom(ValuesDelta from, String key) {
281        ensureUpdate();
282        if (containsKey(key) || from.containsKey(key)) {
283            put(key, from.getAsString(key));
284        }
285    }
286
287    /**
288     * Return set of all keys defined through this object.
289     */
290    public Set<String> keySet() {
291        final HashSet<String> keys = Sets.newHashSet();
292
293        if (mBefore != null) {
294            for (Map.Entry<String, Object> entry : mBefore.valueSet()) {
295                keys.add(entry.getKey());
296            }
297        }
298
299        if (mAfter != null) {
300            for (Map.Entry<String, Object> entry : mAfter.valueSet()) {
301                keys.add(entry.getKey());
302            }
303        }
304
305        return keys;
306    }
307
308    /**
309     * Return complete set of "before" and "after" values mixed together,
310     * giving full state regardless of edits.
311     */
312    public ContentValues getCompleteValues() {
313        final ContentValues values = new ContentValues();
314        if (mBefore != null) {
315            values.putAll(mBefore);
316        }
317        if (mAfter != null) {
318            values.putAll(mAfter);
319        }
320        if (values.containsKey(ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID)) {
321            // Clear to avoid double-definitions, and prefer rows
322            values.remove(ContactsContract.CommonDataKinds.GroupMembership.GROUP_SOURCE_ID);
323        }
324
325        return values;
326    }
327
328    /**
329     * Merge the "after" values from the given {@link ValuesDelta},
330     * discarding any existing "after" state. This is typically used when
331     * re-parenting changes onto an updated {@link Entity}.
332     */
333    public static ValuesDelta mergeAfter(ValuesDelta local, ValuesDelta remote) {
334        // Bail early if trying to merge delete with missing local
335        if (local == null && (remote.isDelete() || remote.isTransient())) return null;
336
337        // Create local version if none exists yet
338        if (local == null) local = new ValuesDelta();
339
340        if (!local.beforeExists()) {
341            // Any "before" record is missing, so take all values as "insert"
342            local.mAfter = remote.getCompleteValues();
343        } else {
344            // Existing "update" with only "after" values
345            local.mAfter = remote.mAfter;
346        }
347
348        return local;
349    }
350
351    @Override
352    public boolean equals(Object object) {
353        if (object instanceof ValuesDelta) {
354            // Only exactly equal with both are identical subsets
355            final ValuesDelta other = (ValuesDelta)object;
356            return this.subsetEquals(other) && other.subsetEquals(this);
357        }
358        return false;
359    }
360
361    @Override
362    public String toString() {
363        final StringBuilder builder = new StringBuilder();
364        toString(builder);
365        return builder.toString();
366    }
367
368    /**
369     * Helper for building string representation, leveraging the given
370     * {@link StringBuilder} to minimize allocations.
371     */
372    public void toString(StringBuilder builder) {
373        builder.append("{ ");
374        builder.append("IdColumn=");
375        builder.append(mIdColumn);
376        builder.append(", FromTemplate=");
377        builder.append(mFromTemplate);
378        builder.append(", ");
379        for (String key : this.keySet()) {
380            builder.append(key);
381            builder.append("=");
382            builder.append(this.getAsString(key));
383            builder.append(", ");
384        }
385        builder.append("}");
386    }
387
388    /**
389     * Check if the given {@link ValuesDelta} is both a subset of this
390     * object, and any defined keys have equal values.
391     */
392    public boolean subsetEquals(ValuesDelta other) {
393        for (String key : this.keySet()) {
394            final String ourValue = this.getAsString(key);
395            final String theirValue = other.getAsString(key);
396            if (ourValue == null) {
397                // If they have value when we're null, no match
398                if (theirValue != null) return false;
399            } else {
400                // If both values defined and aren't equal, no match
401                if (!ourValue.equals(theirValue)) return false;
402            }
403        }
404        // All values compared and matched
405        return true;
406    }
407
408    /**
409     * Build a {@link android.content.ContentProviderOperation} that will transform our
410     * "before" state into our "after" state, using insert, update, or
411     * delete as needed.
412     */
413    public ContentProviderOperation.Builder buildDiff(Uri targetUri) {
414        return buildDiffHelper(targetUri);
415    }
416
417    /**
418     * For compatibility purpose.
419     */
420    public BuilderWrapper buildDiffWrapper(Uri targetUri) {
421        final ContentProviderOperation.Builder builder = buildDiffHelper(targetUri);
422        BuilderWrapper bw = null;
423        if (isInsert()) {
424            bw = new BuilderWrapper(builder, CompatUtils.TYPE_INSERT);
425        } else if (isDelete()) {
426            bw = new BuilderWrapper(builder, CompatUtils.TYPE_DELETE);
427        } else if (isUpdate()) {
428            bw = new BuilderWrapper(builder, CompatUtils.TYPE_UPDATE);
429        }
430        return bw;
431    }
432
433    private ContentProviderOperation.Builder buildDiffHelper(Uri targetUri) {
434        ContentProviderOperation.Builder builder = null;
435        if (isInsert()) {
436            // Changed values are "insert" back-referenced to Contact
437            mAfter.remove(mIdColumn);
438            builder = ContentProviderOperation.newInsert(targetUri);
439            builder.withValues(mAfter);
440        } else if (isDelete()) {
441            // When marked for deletion and "before" exists, then "delete"
442            builder = ContentProviderOperation.newDelete(targetUri);
443            builder.withSelection(mIdColumn + "=" + getId(), null);
444        } else if (isUpdate()) {
445            // When has changes and "before" exists, then "update"
446            builder = ContentProviderOperation.newUpdate(targetUri);
447            builder.withSelection(mIdColumn + "=" + getId(), null);
448            builder.withValues(mAfter);
449        }
450        return builder;
451    }
452
453    /** {@inheritDoc} */
454    public int describeContents() {
455        // Nothing special about this parcel
456        return 0;
457    }
458
459    /** {@inheritDoc} */
460    public void writeToParcel(Parcel dest, int flags) {
461        dest.writeParcelable(mBefore, flags);
462        dest.writeParcelable(mAfter, flags);
463        dest.writeString(mIdColumn);
464    }
465
466    public void readFromParcel(Parcel source) {
467        final ClassLoader loader = getClass().getClassLoader();
468        mBefore = source.<ContentValues> readParcelable(loader);
469        mAfter = source.<ContentValues> readParcelable(loader);
470        mIdColumn = source.readString();
471    }
472
473    public static final Creator<ValuesDelta> CREATOR = new Creator<ValuesDelta>() {
474        public ValuesDelta createFromParcel(Parcel in) {
475            final ValuesDelta values = new ValuesDelta();
476            values.readFromParcel(in);
477            return values;
478        }
479
480        public ValuesDelta[] newArray(int size) {
481            return new ValuesDelta[size];
482        }
483    };
484
485    public void setGroupRowId(long groupId) {
486        put(ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID, groupId);
487    }
488
489    public Long getGroupRowId() {
490        return getAsLong(ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID);
491    }
492
493    public void setPhoto(byte[] value) {
494        put(ContactsContract.CommonDataKinds.Photo.PHOTO, value);
495    }
496
497    public byte[] getPhoto() {
498        return getAsByteArray(ContactsContract.CommonDataKinds.Photo.PHOTO);
499    }
500
501    public void setSuperPrimary(boolean val) {
502        if (val) {
503            put(ContactsContract.Data.IS_SUPER_PRIMARY, 1);
504        } else {
505            put(ContactsContract.Data.IS_SUPER_PRIMARY, 0);
506        }
507    }
508
509    public void setPhoneticFamilyName(String value) {
510        put(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_FAMILY_NAME, value);
511    }
512
513    public void setPhoneticMiddleName(String value) {
514        put(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_MIDDLE_NAME, value);
515    }
516
517    public void setPhoneticGivenName(String value) {
518        put(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_GIVEN_NAME, value);
519    }
520
521    public String getPhoneticFamilyName() {
522        return getAsString(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_FAMILY_NAME);
523    }
524
525    public String getPhoneticMiddleName() {
526        return getAsString(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_MIDDLE_NAME);
527    }
528
529    public String getPhoneticGivenName() {
530        return getAsString(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_GIVEN_NAME);
531    }
532
533    public String getDisplayName() {
534        return getAsString(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME);
535    }
536
537    public void setDisplayName(String name) {
538        if (name == null) {
539            putNull(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME);
540        } else {
541            put(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name);
542        }
543    }
544
545    public void copyStructuredNameFieldsFrom(ValuesDelta name) {
546        copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME);
547
548        copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME);
549        copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME);
550        copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.PREFIX);
551        copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME);
552        copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.SUFFIX);
553
554        copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.PHONETIC_GIVEN_NAME);
555        copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.PHONETIC_MIDDLE_NAME);
556        copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.PHONETIC_FAMILY_NAME);
557
558        copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.FULL_NAME_STYLE);
559        copyStringFrom(name, ContactsContract.Data.DATA11);
560    }
561
562    public String getPhoneNumber() {
563        return getAsString(ContactsContract.CommonDataKinds.Phone.NUMBER);
564    }
565
566    public String getPhoneNormalizedNumber() {
567        return getAsString(ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER);
568    }
569
570    public boolean hasPhoneType() {
571        return getPhoneType() != null;
572    }
573
574    public Integer getPhoneType() {
575        return getAsInteger(ContactsContract.CommonDataKinds.Phone.TYPE);
576    }
577
578    public String getPhoneLabel() {
579        return getAsString(ContactsContract.CommonDataKinds.Phone.LABEL);
580    }
581
582    public String getEmailData() {
583        return getAsString(ContactsContract.CommonDataKinds.Email.DATA);
584    }
585
586    public boolean hasEmailType() {
587        return getEmailType() != null;
588    }
589
590    public Integer getEmailType() {
591        return getAsInteger(ContactsContract.CommonDataKinds.Email.TYPE);
592    }
593
594    public String getEmailLabel() {
595        return getAsString(ContactsContract.CommonDataKinds.Email.LABEL);
596    }
597}
598