Home | History | Annotate | Download | only in model
      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 
     17 package com.android.contacts.common.model;
     18 
     19 import android.content.ContentProviderOperation;
     20 import android.content.ContentValues;
     21 import android.net.Uri;
     22 import android.os.Parcel;
     23 import android.os.Parcelable;
     24 import android.provider.BaseColumns;
     25 import android.provider.ContactsContract;
     26 
     27 import com.android.contacts.common.compat.CompatUtils;
     28 import com.android.contacts.common.model.BuilderWrapper;
     29 import com.android.contacts.common.testing.NeededForTesting;
     30 import com.google.common.collect.Sets;
     31 
     32 import java.util.HashSet;
     33 import java.util.Map;
     34 import 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  */
     41 public 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