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