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.ContentResolver;
     21 import android.content.Entity;
     22 import android.content.EntityIterator;
     23 import android.content.ContentProviderOperation.Builder;
     24 import android.net.Uri;
     25 import android.os.Parcel;
     26 import android.os.Parcelable;
     27 import android.provider.ContactsContract.AggregationExceptions;
     28 import android.provider.ContactsContract.Contacts;
     29 import android.provider.ContactsContract.RawContacts;
     30 import android.provider.ContactsContract.RawContactsEntity;
     31 
     32 import com.google.android.collect.Lists;
     33 
     34 import com.android.contacts.model.EntityDelta.ValuesDelta;
     35 
     36 import java.util.ArrayList;
     37 import java.util.Iterator;
     38 import java.util.List;
     39 
     40 /**
     41  * Container for multiple {@link EntityDelta} objects, usually when editing
     42  * together as an entire aggregate. Provides convenience methods for parceling
     43  * and applying another {@link EntityDeltaList} over it.
     44  */
     45 public class EntityDeltaList extends ArrayList<EntityDelta> implements Parcelable {
     46     private boolean mSplitRawContacts;
     47     private long[] mJoinWithRawContactIds;
     48 
     49     private EntityDeltaList() {
     50     }
     51 
     52     /**
     53      * Create an {@link EntityDeltaList} that contains the given {@link EntityDelta},
     54      * usually when inserting a new {@link Contacts} entry.
     55      */
     56     public static EntityDeltaList fromSingle(EntityDelta delta) {
     57         final EntityDeltaList state = new EntityDeltaList();
     58         state.add(delta);
     59         return state;
     60     }
     61 
     62     /**
     63      * Create an {@link EntityDeltaList} based on {@link Contacts} specified by the
     64      * given query parameters. This closes the {@link EntityIterator} when
     65      * finished, so it doesn't subscribe to updates.
     66      */
     67     public static EntityDeltaList fromQuery(Uri entityUri, ContentResolver resolver,
     68             String selection, String[] selectionArgs, String sortOrder) {
     69         final EntityIterator iterator = RawContacts.newEntityIterator(resolver.query(
     70                 entityUri, null, selection, selectionArgs,
     71                 sortOrder));
     72         try {
     73             return fromIterator(iterator);
     74         } finally {
     75             iterator.close();
     76         }
     77     }
     78 
     79     /**
     80      * Create an {@link EntityDeltaList} that contains the entities of the Iterator as before
     81      * values.
     82      */
     83     public static EntityDeltaList fromIterator(Iterator<Entity> iterator) {
     84         final EntityDeltaList state = new EntityDeltaList();
     85         // Perform background query to pull contact details
     86         while (iterator.hasNext()) {
     87             // Read all contacts into local deltas to prepare for edits
     88             final Entity before = iterator.next();
     89             final EntityDelta entity = EntityDelta.fromBefore(before);
     90             state.add(entity);
     91         }
     92         return state;
     93     }
     94 
     95     /**
     96      * Merge the "after" values from the given {@link EntityDeltaList}, discarding any
     97      * previous "after" states. This is typically used when re-parenting user
     98      * edits onto an updated {@link EntityDeltaList}.
     99      */
    100     public static EntityDeltaList mergeAfter(EntityDeltaList local, EntityDeltaList remote) {
    101         if (local == null) local = new EntityDeltaList();
    102 
    103         // For each entity in the remote set, try matching over existing
    104         for (EntityDelta remoteEntity : remote) {
    105             final Long rawContactId = remoteEntity.getValues().getId();
    106 
    107             // Find or create local match and merge
    108             final EntityDelta localEntity = local.getByRawContactId(rawContactId);
    109             final EntityDelta merged = EntityDelta.mergeAfter(localEntity, remoteEntity);
    110 
    111             if (localEntity == null && merged != null) {
    112                 // No local entry before, so insert
    113                 local.add(merged);
    114             }
    115         }
    116 
    117         return local;
    118     }
    119 
    120     /**
    121      * Build a list of {@link ContentProviderOperation} that will transform all
    122      * the "before" {@link Entity} states into the modified state which all
    123      * {@link EntityDelta} objects represent. This method specifically creates
    124      * any {@link AggregationExceptions} rules needed to groups edits together.
    125      */
    126     public ArrayList<ContentProviderOperation> buildDiff() {
    127         final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
    128 
    129         final long rawContactId = this.findRawContactId();
    130         int firstInsertRow = -1;
    131 
    132         // First pass enforces versions remain consistent
    133         for (EntityDelta delta : this) {
    134             delta.buildAssert(diff);
    135         }
    136 
    137         final int assertMark = diff.size();
    138         int backRefs[] = new int[size()];
    139 
    140         int rawContactIndex = 0;
    141 
    142         // Second pass builds actual operations
    143         for (EntityDelta delta : this) {
    144             final int firstBatch = diff.size();
    145             final boolean isInsert = delta.isContactInsert();
    146             backRefs[rawContactIndex++] = isInsert ? firstBatch : -1;
    147 
    148             delta.buildDiff(diff);
    149 
    150             // If the user chose to join with some other existing raw contact(s) at save time,
    151             // add aggregation exceptions for all those raw contacts.
    152             if (mJoinWithRawContactIds != null) {
    153                 for (Long joinedRawContactId : mJoinWithRawContactIds) {
    154                     final Builder builder = beginKeepTogether();
    155                     builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, joinedRawContactId);
    156                     if (rawContactId != -1) {
    157                         builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId);
    158                     } else {
    159                         builder.withValueBackReference(
    160                                 AggregationExceptions.RAW_CONTACT_ID2, firstBatch);
    161                     }
    162                     diff.add(builder.build());
    163                 }
    164             }
    165 
    166             // Only create rules for inserts
    167             if (!isInsert) continue;
    168 
    169             // If we are going to split all contacts, there is no point in first combining them
    170             if (mSplitRawContacts) continue;
    171 
    172             if (rawContactId != -1) {
    173                 // Has existing contact, so bind to it strongly
    174                 final Builder builder = beginKeepTogether();
    175                 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId);
    176                 builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, firstBatch);
    177                 diff.add(builder.build());
    178 
    179             } else if (firstInsertRow == -1) {
    180                 // First insert case, so record row
    181                 firstInsertRow = firstBatch;
    182 
    183             } else {
    184                 // Additional insert case, so point at first insert
    185                 final Builder builder = beginKeepTogether();
    186                 builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID1,
    187                         firstInsertRow);
    188                 builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, firstBatch);
    189                 diff.add(builder.build());
    190             }
    191         }
    192 
    193         if (mSplitRawContacts) {
    194             buildSplitContactDiff(diff, backRefs);
    195         }
    196 
    197         // No real changes if only left with asserts
    198         if (diff.size() == assertMark) {
    199             diff.clear();
    200         }
    201 
    202         return diff;
    203     }
    204 
    205     /**
    206      * Start building a {@link ContentProviderOperation} that will keep two
    207      * {@link RawContacts} together.
    208      */
    209     protected Builder beginKeepTogether() {
    210         final Builder builder = ContentProviderOperation
    211                 .newUpdate(AggregationExceptions.CONTENT_URI);
    212         builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
    213         return builder;
    214     }
    215 
    216     /**
    217      * Builds {@link AggregationExceptions} to split all constituent raw contacts into
    218      * separate contacts.
    219      */
    220     private void buildSplitContactDiff(final ArrayList<ContentProviderOperation> diff,
    221             int[] backRefs) {
    222         int count = size();
    223         for (int i = 0; i < count; i++) {
    224             for (int j = 0; j < count; j++) {
    225                 if (i != j) {
    226                     buildSplitContactDiff(diff, i, j, backRefs);
    227                 }
    228             }
    229         }
    230     }
    231 
    232     /**
    233      * Construct a {@link AggregationExceptions#TYPE_KEEP_SEPARATE}.
    234      */
    235     private void buildSplitContactDiff(ArrayList<ContentProviderOperation> diff, int index1,
    236             int index2, int[] backRefs) {
    237         Builder builder =
    238                 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
    239         builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_SEPARATE);
    240 
    241         Long rawContactId1 = get(index1).getValues().getAsLong(RawContacts._ID);
    242         int backRef1 = backRefs[index1];
    243         if (rawContactId1 != null && rawContactId1 >= 0) {
    244             builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
    245         } else if (backRef1 >= 0) {
    246             builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID1, backRef1);
    247         } else {
    248             return;
    249         }
    250 
    251         Long rawContactId2 = get(index2).getValues().getAsLong(RawContacts._ID);
    252         int backRef2 = backRefs[index2];
    253         if (rawContactId2 != null && rawContactId2 >= 0) {
    254             builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
    255         } else if (backRef2 >= 0) {
    256             builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, backRef2);
    257         } else {
    258             return;
    259         }
    260 
    261         diff.add(builder.build());
    262     }
    263 
    264     /**
    265      * Search all contained {@link EntityDelta} for the first one with an
    266      * existing {@link RawContacts#_ID} value. Usually used when creating
    267      * {@link AggregationExceptions} during an update.
    268      */
    269     public long findRawContactId() {
    270         for (EntityDelta delta : this) {
    271             final Long rawContactId = delta.getValues().getAsLong(RawContacts._ID);
    272             if (rawContactId != null && rawContactId >= 0) {
    273                 return rawContactId;
    274             }
    275         }
    276         return -1;
    277     }
    278 
    279     /**
    280      * Find {@link RawContacts#_ID} of the requested {@link EntityDelta}.
    281      */
    282     public Long getRawContactId(int index) {
    283         if (index >= 0 && index < this.size()) {
    284             final EntityDelta delta = this.get(index);
    285             final ValuesDelta values = delta.getValues();
    286             if (values.isVisible()) {
    287                 return values.getAsLong(RawContacts._ID);
    288             }
    289         }
    290         return null;
    291     }
    292 
    293     public EntityDelta getByRawContactId(Long rawContactId) {
    294         final int index = this.indexOfRawContactId(rawContactId);
    295         return (index == -1) ? null : this.get(index);
    296     }
    297 
    298     /**
    299      * Find index of given {@link RawContacts#_ID} when present.
    300      */
    301     public int indexOfRawContactId(Long rawContactId) {
    302         if (rawContactId == null) return -1;
    303         final int size = this.size();
    304         for (int i = 0; i < size; i++) {
    305             final Long currentId = getRawContactId(i);
    306             if (rawContactId.equals(currentId)) {
    307                 return i;
    308             }
    309         }
    310         return -1;
    311     }
    312 
    313     public ValuesDelta getSuperPrimaryEntry(final String mimeType) {
    314         ValuesDelta primary = null;
    315         ValuesDelta randomEntry = null;
    316         for (EntityDelta delta : this) {
    317             final ArrayList<ValuesDelta> mimeEntries = delta.getMimeEntries(mimeType);
    318             if (mimeEntries == null) return null;
    319 
    320             for (ValuesDelta entry : mimeEntries) {
    321                 if (entry.isSuperPrimary()) {
    322                     return entry;
    323                 } else if (primary == null && entry.isPrimary()) {
    324                     primary = entry;
    325                 } else if (randomEntry == null) {
    326                     randomEntry = entry;
    327                 }
    328             }
    329         }
    330         // When no direct super primary, return something
    331         if (primary != null) {
    332             return primary;
    333         }
    334         return randomEntry;
    335     }
    336 
    337     /**
    338      * Sets a flag that will split ("explode") the raw_contacts into seperate contacts
    339      */
    340     public void markRawContactsForSplitting() {
    341         mSplitRawContacts = true;
    342     }
    343 
    344     public boolean isMarkedForSplitting() {
    345         return mSplitRawContacts;
    346     }
    347 
    348     public void setJoinWithRawContacts(long[] rawContactIds) {
    349         mJoinWithRawContactIds = rawContactIds;
    350     }
    351 
    352     public boolean isMarkedForJoining() {
    353         return mJoinWithRawContactIds != null && mJoinWithRawContactIds.length > 0;
    354     }
    355 
    356     /** {@inheritDoc} */
    357     public int describeContents() {
    358         // Nothing special about this parcel
    359         return 0;
    360     }
    361 
    362     /** {@inheritDoc} */
    363     public void writeToParcel(Parcel dest, int flags) {
    364         final int size = this.size();
    365         dest.writeInt(size);
    366         for (EntityDelta delta : this) {
    367             dest.writeParcelable(delta, flags);
    368         }
    369         dest.writeLongArray(mJoinWithRawContactIds);
    370         dest.writeInt(mSplitRawContacts ? 1 : 0);
    371     }
    372 
    373     @SuppressWarnings("unchecked")
    374     public void readFromParcel(Parcel source) {
    375         final ClassLoader loader = getClass().getClassLoader();
    376         final int size = source.readInt();
    377         for (int i = 0; i < size; i++) {
    378             this.add(source.<EntityDelta> readParcelable(loader));
    379         }
    380         mJoinWithRawContactIds = source.createLongArray();
    381         mSplitRawContacts = source.readInt() != 0;
    382     }
    383 
    384     public static final Parcelable.Creator<EntityDeltaList> CREATOR =
    385             new Parcelable.Creator<EntityDeltaList>() {
    386         public EntityDeltaList createFromParcel(Parcel in) {
    387             final EntityDeltaList state = new EntityDeltaList();
    388             state.readFromParcel(in);
    389             return state;
    390         }
    391 
    392         public EntityDeltaList[] newArray(int size) {
    393             return new EntityDeltaList[size];
    394         }
    395     };
    396 }
    397