Home | History | Annotate | Download | only in model
      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.Data;
     28 import android.provider.ContactsContract.Profile;
     29 import android.provider.ContactsContract.RawContacts;
     30 import android.util.Log;
     31 
     32 import com.android.contacts.common.model.AccountTypeManager;
     33 import com.android.contacts.common.model.ValuesDelta;
     34 import com.android.contacts.common.model.account.AccountType;
     35 import com.android.contacts.common.test.NeededForTesting;
     36 import com.google.common.collect.Lists;
     37 import com.google.common.collect.Maps;
     38 
     39 import java.util.ArrayList;
     40 import java.util.HashMap;
     41 
     42 /**
     43  * Contains a {@link RawContact} and records any modifications separately so the
     44  * original {@link RawContact} can be swapped out with a newer version and the
     45  * changes still cleanly applied.
     46  * <p>
     47  * One benefit of this approach is that we can build changes entirely on an
     48  * empty {@link RawContact}, which then becomes an insert {@link RawContacts} case.
     49  * <p>
     50  * When applying modifications over an {@link RawContact}, we try finding the
     51  * original {@link Data#_ID} rows where the modifications took place. If those
     52  * rows are missing from the new {@link RawContact}, we know the original data must
     53  * be deleted, but to preserve the user modifications we treat as an insert.
     54  */
     55 public class RawContactDelta implements Parcelable {
     56     // TODO: optimize by using contentvalues pool, since we allocate so many of them
     57 
     58     private static final String TAG = "EntityDelta";
     59     private static final boolean LOGV = false;
     60 
     61     /**
     62      * Direct values from {@link Entity#getEntityValues()}.
     63      */
     64     private ValuesDelta mValues;
     65 
     66     /**
     67      * URI used for contacts queries, by default it is set to query raw contacts.
     68      * It can be set to query the profile's raw contact(s).
     69      */
     70     private Uri mContactsQueryUri = RawContacts.CONTENT_URI;
     71 
     72     /**
     73      * Internal map of children values from {@link Entity#getSubValues()}, which
     74      * we store here sorted into {@link Data#MIMETYPE} bins.
     75      */
     76     private final HashMap<String, ArrayList<ValuesDelta>> mEntries = Maps.newHashMap();
     77 
     78     public RawContactDelta() {
     79     }
     80 
     81     public RawContactDelta(ValuesDelta values) {
     82         mValues = values;
     83     }
     84 
     85     /**
     86      * Build an {@link RawContactDelta} using the given {@link RawContact} as a
     87      * starting point; the "before" snapshot.
     88      */
     89     public static RawContactDelta fromBefore(RawContact before) {
     90         final RawContactDelta rawContactDelta = new RawContactDelta();
     91         rawContactDelta.mValues = ValuesDelta.fromBefore(before.getValues());
     92         rawContactDelta.mValues.setIdColumn(RawContacts._ID);
     93         for (final ContentValues values : before.getContentValues()) {
     94             rawContactDelta.addEntry(ValuesDelta.fromBefore(values));
     95         }
     96         return rawContactDelta;
     97     }
     98 
     99     /**
    100      * Merge the "after" values from the given {@link RawContactDelta} onto the
    101      * "before" state represented by this {@link RawContactDelta}, discarding any
    102      * existing "after" states. This is typically used when re-parenting changes
    103      * onto an updated {@link Entity}.
    104      */
    105     public static RawContactDelta mergeAfter(RawContactDelta local, RawContactDelta remote) {
    106         // Bail early if trying to merge delete with missing local
    107         final ValuesDelta remoteValues = remote.mValues;
    108         if (local == null && (remoteValues.isDelete() || remoteValues.isTransient())) return null;
    109 
    110         // Create local version if none exists yet
    111         if (local == null) local = new RawContactDelta();
    112 
    113         if (LOGV) {
    114             final Long localVersion = (local.mValues == null) ? null : local.mValues
    115                     .getAsLong(RawContacts.VERSION);
    116             final Long remoteVersion = remote.mValues.getAsLong(RawContacts.VERSION);
    117             Log.d(TAG, "Re-parenting from original version " + remoteVersion + " to "
    118                     + localVersion);
    119         }
    120 
    121         // Create values if needed, and merge "after" changes
    122         local.mValues = ValuesDelta.mergeAfter(local.mValues, remote.mValues);
    123 
    124         // Find matching local entry for each remote values, or create
    125         for (ArrayList<ValuesDelta> mimeEntries : remote.mEntries.values()) {
    126             for (ValuesDelta remoteEntry : mimeEntries) {
    127                 final Long childId = remoteEntry.getId();
    128 
    129                 // Find or create local match and merge
    130                 final ValuesDelta localEntry = local.getEntry(childId);
    131                 final ValuesDelta merged = ValuesDelta.mergeAfter(localEntry, remoteEntry);
    132 
    133                 if (localEntry == null && merged != null) {
    134                     // No local entry before, so insert
    135                     local.addEntry(merged);
    136                 }
    137             }
    138         }
    139 
    140         return local;
    141     }
    142 
    143     public ValuesDelta getValues() {
    144         return mValues;
    145     }
    146 
    147     public boolean isContactInsert() {
    148         return mValues.isInsert();
    149     }
    150 
    151     /**
    152      * Get the {@link ValuesDelta} child marked as {@link Data#IS_PRIMARY},
    153      * which may return null when no entry exists.
    154      */
    155     public ValuesDelta getPrimaryEntry(String mimeType) {
    156         final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false);
    157         if (mimeEntries == null) return null;
    158 
    159         for (ValuesDelta entry : mimeEntries) {
    160             if (entry.isPrimary()) {
    161                 return entry;
    162             }
    163         }
    164 
    165         // When no direct primary, return something
    166         return mimeEntries.size() > 0 ? mimeEntries.get(0) : null;
    167     }
    168 
    169     /**
    170      * calls {@link #getSuperPrimaryEntry(String, boolean)} with true
    171      * @see #getSuperPrimaryEntry(String, boolean)
    172      */
    173     public ValuesDelta getSuperPrimaryEntry(String mimeType) {
    174         return getSuperPrimaryEntry(mimeType, true);
    175     }
    176 
    177     /**
    178      * Returns the super-primary entry for the given mime type
    179      * @param forceSelection if true, will try to return some value even if a super-primary
    180      *     doesn't exist (may be a primary, or just a random item
    181      * @return
    182      */
    183     @NeededForTesting
    184     public ValuesDelta getSuperPrimaryEntry(String mimeType, boolean forceSelection) {
    185         final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType, false);
    186         if (mimeEntries == null) return null;
    187 
    188         ValuesDelta primary = null;
    189         for (ValuesDelta entry : mimeEntries) {
    190             if (entry.isSuperPrimary()) {
    191                 return entry;
    192             } else if (entry.isPrimary()) {
    193                 primary = entry;
    194             }
    195         }
    196 
    197         if (!forceSelection) {
    198             return null;
    199         }
    200 
    201         // When no direct super primary, return something
    202         if (primary != null) {
    203             return primary;
    204         }
    205         return mimeEntries.size() > 0 ? mimeEntries.get(0) : null;
    206     }
    207 
    208     /**
    209      * Return the AccountType that this raw-contact belongs to.
    210      */
    211     public AccountType getRawContactAccountType(Context context) {
    212         ContentValues entityValues = getValues().getCompleteValues();
    213         String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
    214         String dataSet = entityValues.getAsString(RawContacts.DATA_SET);
    215         return AccountTypeManager.getInstance(context).getAccountType(type, dataSet);
    216     }
    217 
    218     public Long getRawContactId() {
    219         return getValues().getAsLong(RawContacts._ID);
    220     }
    221 
    222     public String getAccountName() {
    223         return getValues().getAsString(RawContacts.ACCOUNT_NAME);
    224     }
    225 
    226     public String getAccountType() {
    227         return getValues().getAsString(RawContacts.ACCOUNT_TYPE);
    228     }
    229 
    230     public String getDataSet() {
    231         return getValues().getAsString(RawContacts.DATA_SET);
    232     }
    233 
    234     public AccountType getAccountType(AccountTypeManager manager) {
    235         return manager.getAccountType(getAccountType(), getDataSet());
    236     }
    237 
    238     public boolean isVisible() {
    239         return getValues().isVisible();
    240     }
    241 
    242     /**
    243      * Return the list of child {@link ValuesDelta} from our optimized map,
    244      * creating the list if requested.
    245      */
    246     private ArrayList<ValuesDelta> getMimeEntries(String mimeType, boolean lazyCreate) {
    247         ArrayList<ValuesDelta> mimeEntries = mEntries.get(mimeType);
    248         if (mimeEntries == null && lazyCreate) {
    249             mimeEntries = Lists.newArrayList();
    250             mEntries.put(mimeType, mimeEntries);
    251         }
    252         return mimeEntries;
    253     }
    254 
    255     public ArrayList<ValuesDelta> getMimeEntries(String mimeType) {
    256         return getMimeEntries(mimeType, false);
    257     }
    258 
    259     public int getMimeEntriesCount(String mimeType, boolean onlyVisible) {
    260         final ArrayList<ValuesDelta> mimeEntries = getMimeEntries(mimeType);
    261         if (mimeEntries == null) return 0;
    262 
    263         int count = 0;
    264         for (ValuesDelta child : mimeEntries) {
    265             // Skip deleted items when requesting only visible
    266             if (onlyVisible && !child.isVisible()) continue;
    267             count++;
    268         }
    269         return count;
    270     }
    271 
    272     public boolean hasMimeEntries(String mimeType) {
    273         return mEntries.containsKey(mimeType);
    274     }
    275 
    276     public ValuesDelta addEntry(ValuesDelta entry) {
    277         final String mimeType = entry.getMimetype();
    278         getMimeEntries(mimeType, true).add(entry);
    279         return entry;
    280     }
    281 
    282     public ArrayList<ContentValues> getContentValues() {
    283         ArrayList<ContentValues> values = Lists.newArrayList();
    284         for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
    285             for (ValuesDelta entry : mimeEntries) {
    286                 if (!entry.isDelete()) {
    287                     values.add(entry.getCompleteValues());
    288                 }
    289             }
    290         }
    291         return values;
    292     }
    293 
    294     /**
    295      * Find entry with the given {@link BaseColumns#_ID} value.
    296      */
    297     public ValuesDelta getEntry(Long childId) {
    298         if (childId == null) {
    299             // Requesting an "insert" entry, which has no "before"
    300             return null;
    301         }
    302 
    303         // Search all children for requested entry
    304         for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
    305             for (ValuesDelta entry : mimeEntries) {
    306                 if (childId.equals(entry.getId())) {
    307                     return entry;
    308                 }
    309             }
    310         }
    311         return null;
    312     }
    313 
    314     /**
    315      * Return the total number of {@link ValuesDelta} contained.
    316      */
    317     public int getEntryCount(boolean onlyVisible) {
    318         int count = 0;
    319         for (String mimeType : mEntries.keySet()) {
    320             count += getMimeEntriesCount(mimeType, onlyVisible);
    321         }
    322         return count;
    323     }
    324 
    325     @Override
    326     public boolean equals(Object object) {
    327         if (object instanceof RawContactDelta) {
    328             final RawContactDelta other = (RawContactDelta)object;
    329 
    330             // Equality failed if parent values different
    331             if (!other.mValues.equals(mValues)) return false;
    332 
    333             for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
    334                 for (ValuesDelta child : mimeEntries) {
    335                     // Equality failed if any children unmatched
    336                     if (!other.containsEntry(child)) return false;
    337                 }
    338             }
    339 
    340             // Passed all tests, so equal
    341             return true;
    342         }
    343         return false;
    344     }
    345 
    346     private boolean containsEntry(ValuesDelta entry) {
    347         for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
    348             for (ValuesDelta child : mimeEntries) {
    349                 // Contained if we find any child that matches
    350                 if (child.equals(entry)) return true;
    351             }
    352         }
    353         return false;
    354     }
    355 
    356     /**
    357      * Mark this entire object deleted, including any {@link ValuesDelta}.
    358      */
    359     public void markDeleted() {
    360         this.mValues.markDeleted();
    361         for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
    362             for (ValuesDelta child : mimeEntries) {
    363                 child.markDeleted();
    364             }
    365         }
    366     }
    367 
    368     @Override
    369     public String toString() {
    370         final StringBuilder builder = new StringBuilder();
    371         builder.append("\n(");
    372         builder.append("Uri=");
    373         builder.append(mContactsQueryUri);
    374         builder.append(", Values=");
    375         builder.append(mValues != null ? mValues.toString() : "null");
    376         builder.append(", Entries={");
    377         for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
    378             for (ValuesDelta child : mimeEntries) {
    379                 builder.append("\n\t");
    380                 child.toString(builder);
    381             }
    382         }
    383         builder.append("\n})\n");
    384         return builder.toString();
    385     }
    386 
    387     /**
    388      * Consider building the given {@link ContentProviderOperation.Builder} and
    389      * appending it to the given list, which only happens if builder is valid.
    390      */
    391     private void possibleAdd(ArrayList<ContentProviderOperation> diff,
    392             ContentProviderOperation.Builder builder) {
    393         if (builder != null) {
    394             diff.add(builder.build());
    395         }
    396     }
    397 
    398     /**
    399      * Build a list of {@link ContentProviderOperation} that will assert any
    400      * "before" state hasn't changed. This is maintained separately so that all
    401      * asserts can take place before any updates occur.
    402      */
    403     public void buildAssert(ArrayList<ContentProviderOperation> buildInto) {
    404         final boolean isContactInsert = mValues.isInsert();
    405         if (!isContactInsert) {
    406             // Assert version is consistent while persisting changes
    407             final Long beforeId = mValues.getId();
    408             final Long beforeVersion = mValues.getAsLong(RawContacts.VERSION);
    409             if (beforeId == null || beforeVersion == null) return;
    410 
    411             final ContentProviderOperation.Builder builder = ContentProviderOperation
    412                     .newAssertQuery(mContactsQueryUri);
    413             builder.withSelection(RawContacts._ID + "=" + beforeId, null);
    414             builder.withValue(RawContacts.VERSION, beforeVersion);
    415             buildInto.add(builder.build());
    416         }
    417     }
    418 
    419     /**
    420      * Build a list of {@link ContentProviderOperation} that will transform the
    421      * current "before" {@link Entity} state into the modified state which this
    422      * {@link RawContactDelta} represents.
    423      */
    424     public void buildDiff(ArrayList<ContentProviderOperation> buildInto) {
    425         final int firstIndex = buildInto.size();
    426 
    427         final boolean isContactInsert = mValues.isInsert();
    428         final boolean isContactDelete = mValues.isDelete();
    429         final boolean isContactUpdate = !isContactInsert && !isContactDelete;
    430 
    431         final Long beforeId = mValues.getId();
    432 
    433         Builder builder;
    434 
    435         if (isContactInsert) {
    436             // TODO: for now simply disabling aggregation when a new contact is
    437             // created on the phone.  In the future, will show aggregation suggestions
    438             // after saving the contact.
    439             mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_SUSPENDED);
    440         }
    441 
    442         // Build possible operation at Contact level
    443         builder = mValues.buildDiff(mContactsQueryUri);
    444         possibleAdd(buildInto, builder);
    445 
    446         // Build operations for all children
    447         for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
    448             for (ValuesDelta child : mimeEntries) {
    449                 // Ignore children if parent was deleted
    450                 if (isContactDelete) continue;
    451 
    452                 // Use the profile data URI if the contact is the profile.
    453                 if (mContactsQueryUri.equals(Profile.CONTENT_RAW_CONTACTS_URI)) {
    454                     builder = child.buildDiff(Uri.withAppendedPath(Profile.CONTENT_URI,
    455                             RawContacts.Data.CONTENT_DIRECTORY));
    456                 } else {
    457                     builder = child.buildDiff(Data.CONTENT_URI);
    458                 }
    459 
    460                 if (child.isInsert()) {
    461                     if (isContactInsert) {
    462                         // Parent is brand new insert, so back-reference _id
    463                         builder.withValueBackReference(Data.RAW_CONTACT_ID, firstIndex);
    464                     } else {
    465                         // Inserting under existing, so fill with known _id
    466                         builder.withValue(Data.RAW_CONTACT_ID, beforeId);
    467                     }
    468                 } else if (isContactInsert && builder != null) {
    469                     // Child must be insert when Contact insert
    470                     throw new IllegalArgumentException("When parent insert, child must be also");
    471                 }
    472                 possibleAdd(buildInto, builder);
    473             }
    474         }
    475 
    476         final boolean addedOperations = buildInto.size() > firstIndex;
    477         if (addedOperations && isContactUpdate) {
    478             // Suspend aggregation while persisting updates
    479             builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_SUSPENDED);
    480             buildInto.add(firstIndex, builder.build());
    481 
    482             // Restore aggregation mode as last operation
    483             builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_DEFAULT);
    484             buildInto.add(builder.build());
    485         } else if (isContactInsert) {
    486             // Restore aggregation mode as last operation
    487             builder = ContentProviderOperation.newUpdate(mContactsQueryUri);
    488             builder.withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT);
    489             builder.withSelection(RawContacts._ID + "=?", new String[1]);
    490             builder.withSelectionBackReference(0, firstIndex);
    491             buildInto.add(builder.build());
    492         }
    493     }
    494 
    495     /**
    496      * Build a {@link ContentProviderOperation} that changes
    497      * {@link RawContacts#AGGREGATION_MODE} to the given value.
    498      */
    499     protected Builder buildSetAggregationMode(Long beforeId, int mode) {
    500         Builder builder = ContentProviderOperation.newUpdate(mContactsQueryUri);
    501         builder.withValue(RawContacts.AGGREGATION_MODE, mode);
    502         builder.withSelection(RawContacts._ID + "=" + beforeId, null);
    503         return builder;
    504     }
    505 
    506     /** {@inheritDoc} */
    507     public int describeContents() {
    508         // Nothing special about this parcel
    509         return 0;
    510     }
    511 
    512     /** {@inheritDoc} */
    513     public void writeToParcel(Parcel dest, int flags) {
    514         final int size = this.getEntryCount(false);
    515         dest.writeInt(size);
    516         dest.writeParcelable(mValues, flags);
    517         dest.writeParcelable(mContactsQueryUri, flags);
    518         for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
    519             for (ValuesDelta child : mimeEntries) {
    520                 dest.writeParcelable(child, flags);
    521             }
    522         }
    523     }
    524 
    525     public void readFromParcel(Parcel source) {
    526         final ClassLoader loader = getClass().getClassLoader();
    527         final int size = source.readInt();
    528         mValues = source.<ValuesDelta> readParcelable(loader);
    529         mContactsQueryUri = source.<Uri> readParcelable(loader);
    530         for (int i = 0; i < size; i++) {
    531             final ValuesDelta child = source.<ValuesDelta> readParcelable(loader);
    532             this.addEntry(child);
    533         }
    534     }
    535 
    536     /**
    537      * Used to set the query URI to the profile URI to store profiles.
    538      */
    539     public void setProfileQueryUri() {
    540         mContactsQueryUri = Profile.CONTENT_RAW_CONTACTS_URI;
    541     }
    542 
    543     public static final Parcelable.Creator<RawContactDelta> CREATOR =
    544             new Parcelable.Creator<RawContactDelta>() {
    545         public RawContactDelta createFromParcel(Parcel in) {
    546             final RawContactDelta state = new RawContactDelta();
    547             state.readFromParcel(in);
    548             return state;
    549         }
    550 
    551         public RawContactDelta[] newArray(int size) {
    552             return new RawContactDelta[size];
    553         }
    554     };
    555 
    556 }
    557