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