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