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