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.test.NeededForTesting;
     28 import com.google.common.collect.Sets;
     29 
     30 import java.util.HashSet;
     31 import java.util.Map;
     32 import 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  */
     39 public 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