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