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