/* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.contacts.common.model; import android.content.ContentProviderOperation; import android.content.ContentProviderOperation.Builder; import android.content.ContentResolver; import android.content.Context; import android.content.Entity; import android.content.EntityIterator; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import android.provider.ContactsContract.AggregationExceptions; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.RawContacts; import android.util.Log; import com.android.contacts.common.model.ValuesDelta; import com.google.common.collect.Lists; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; /** * Container for multiple {@link RawContactDelta} objects, usually when editing * together as an entire aggregate. Provides convenience methods for parceling * and applying another {@link RawContactDeltaList} over it. */ public class RawContactDeltaList extends ArrayList implements Parcelable { private static final String TAG = RawContactDeltaList.class.getSimpleName(); private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE); private boolean mSplitRawContacts; private long[] mJoinWithRawContactIds; public RawContactDeltaList() { } /** * Create an {@link RawContactDeltaList} based on {@link Contacts} specified by the * given query parameters. This closes the {@link EntityIterator} when * finished, so it doesn't subscribe to updates. */ public static RawContactDeltaList fromQuery(Uri entityUri, ContentResolver resolver, String selection, String[] selectionArgs, String sortOrder) { final EntityIterator iterator = RawContacts.newEntityIterator( resolver.query(entityUri, null, selection, selectionArgs, sortOrder)); try { return fromIterator(iterator); } finally { iterator.close(); } } /** * Create an {@link RawContactDeltaList} that contains the entities of the Iterator as before * values. This function can be passed an iterator of Entity objects or an iterator of * RawContact objects. */ public static RawContactDeltaList fromIterator(Iterator iterator) { final RawContactDeltaList state = new RawContactDeltaList(); state.addAll(iterator); return state; } public void addAll(Iterator iterator) { // Perform background query to pull contact details while (iterator.hasNext()) { // Read all contacts into local deltas to prepare for edits Object nextObject = iterator.next(); final RawContact before = nextObject instanceof Entity ? RawContact.createFrom((Entity) nextObject) : (RawContact) nextObject; final RawContactDelta rawContactDelta = RawContactDelta.fromBefore(before); add(rawContactDelta); } } /** * Merge the "after" values from the given {@link RawContactDeltaList}, discarding any * previous "after" states. This is typically used when re-parenting user * edits onto an updated {@link RawContactDeltaList}. */ public static RawContactDeltaList mergeAfter(RawContactDeltaList local, RawContactDeltaList remote) { if (local == null) local = new RawContactDeltaList(); // For each entity in the remote set, try matching over existing for (RawContactDelta remoteEntity : remote) { final Long rawContactId = remoteEntity.getValues().getId(); // Find or create local match and merge final RawContactDelta localEntity = local.getByRawContactId(rawContactId); final RawContactDelta merged = RawContactDelta.mergeAfter(localEntity, remoteEntity); if (localEntity == null && merged != null) { // No local entry before, so insert local.add(merged); } } return local; } /** * Build a list of {@link ContentProviderOperation} that will transform all * the "before" {@link Entity} states into the modified state which all * {@link RawContactDelta} objects represent. This method specifically creates * any {@link AggregationExceptions} rules needed to groups edits together. */ public ArrayList buildDiff() { if (VERBOSE_LOGGING) { Log.v(TAG, "buildDiff: list=" + toString()); } final ArrayList diff = Lists.newArrayList(); final long rawContactId = this.findRawContactId(); int firstInsertRow = -1; // First pass enforces versions remain consistent for (RawContactDelta delta : this) { delta.buildAssert(diff); } final int assertMark = diff.size(); int backRefs[] = new int[size()]; int rawContactIndex = 0; // Second pass builds actual operations for (RawContactDelta delta : this) { final int firstBatch = diff.size(); final boolean isInsert = delta.isContactInsert(); backRefs[rawContactIndex++] = isInsert ? firstBatch : -1; delta.buildDiff(diff); // If the user chose to join with some other existing raw contact(s) at save time, // add aggregation exceptions for all those raw contacts. if (mJoinWithRawContactIds != null) { for (Long joinedRawContactId : mJoinWithRawContactIds) { final Builder builder = beginKeepTogether(); builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, joinedRawContactId); if (rawContactId != -1) { builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId); } else { builder.withValueBackReference( AggregationExceptions.RAW_CONTACT_ID2, firstBatch); } diff.add(builder.build()); } } // Only create rules for inserts if (!isInsert) continue; // If we are going to split all contacts, there is no point in first combining them if (mSplitRawContacts) continue; if (rawContactId != -1) { // Has existing contact, so bind to it strongly final Builder builder = beginKeepTogether(); builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId); builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, firstBatch); diff.add(builder.build()); } else if (firstInsertRow == -1) { // First insert case, so record row firstInsertRow = firstBatch; } else { // Additional insert case, so point at first insert final Builder builder = beginKeepTogether(); builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID1, firstInsertRow); builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, firstBatch); diff.add(builder.build()); } } if (mSplitRawContacts) { buildSplitContactDiff(diff, backRefs); } // No real changes if only left with asserts if (diff.size() == assertMark) { diff.clear(); } if (VERBOSE_LOGGING) { Log.v(TAG, "buildDiff: ops=" + diffToString(diff)); } return diff; } private static String diffToString(ArrayList ops) { StringBuilder sb = new StringBuilder(); sb.append("[\n"); for (ContentProviderOperation op : ops) { sb.append(op.toString()); sb.append(",\n"); } sb.append("]\n"); return sb.toString(); } /** * Start building a {@link ContentProviderOperation} that will keep two * {@link RawContacts} together. */ protected Builder beginKeepTogether() { final Builder builder = ContentProviderOperation .newUpdate(AggregationExceptions.CONTENT_URI); builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER); return builder; } /** * Builds {@link AggregationExceptions} to split all constituent raw contacts into * separate contacts. */ private void buildSplitContactDiff(final ArrayList diff, int[] backRefs) { int count = size(); for (int i = 0; i < count; i++) { for (int j = 0; j < count; j++) { if (i != j) { buildSplitContactDiff(diff, i, j, backRefs); } } } } /** * Construct a {@link AggregationExceptions#TYPE_KEEP_SEPARATE}. */ private void buildSplitContactDiff(ArrayList diff, int index1, int index2, int[] backRefs) { Builder builder = ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI); builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_SEPARATE); Long rawContactId1 = get(index1).getValues().getAsLong(RawContacts._ID); int backRef1 = backRefs[index1]; if (rawContactId1 != null && rawContactId1 >= 0) { builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1); } else if (backRef1 >= 0) { builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID1, backRef1); } else { return; } Long rawContactId2 = get(index2).getValues().getAsLong(RawContacts._ID); int backRef2 = backRefs[index2]; if (rawContactId2 != null && rawContactId2 >= 0) { builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2); } else if (backRef2 >= 0) { builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, backRef2); } else { return; } diff.add(builder.build()); } /** * Search all contained {@link RawContactDelta} for the first one with an * existing {@link RawContacts#_ID} value. Usually used when creating * {@link AggregationExceptions} during an update. */ public long findRawContactId() { for (RawContactDelta delta : this) { final Long rawContactId = delta.getValues().getAsLong(RawContacts._ID); if (rawContactId != null && rawContactId >= 0) { return rawContactId; } } return -1; } /** * Find {@link RawContacts#_ID} of the requested {@link RawContactDelta}. */ public Long getRawContactId(int index) { if (index >= 0 && index < this.size()) { final RawContactDelta delta = this.get(index); final ValuesDelta values = delta.getValues(); if (values.isVisible()) { return values.getAsLong(RawContacts._ID); } } return null; } /** * Find the raw-contact (an {@link RawContactDelta}) with the specified ID. */ public RawContactDelta getByRawContactId(Long rawContactId) { final int index = this.indexOfRawContactId(rawContactId); return (index == -1) ? null : this.get(index); } /** * Find index of given {@link RawContacts#_ID} when present. */ public int indexOfRawContactId(Long rawContactId) { if (rawContactId == null) return -1; final int size = this.size(); for (int i = 0; i < size; i++) { final Long currentId = getRawContactId(i); if (rawContactId.equals(currentId)) { return i; } } return -1; } /** * Return the index of the first RawContactDelta corresponding to a writable raw-contact, or -1. * */ public int indexOfFirstWritableRawContact(Context context) { // Find the first writable entity. int entityIndex = 0; for (RawContactDelta delta : this) { if (delta.getRawContactAccountType(context).areContactsWritable()) return entityIndex; entityIndex++; } return -1; } /** Return the first RawContactDelta corresponding to a writable raw-contact, or null. */ public RawContactDelta getFirstWritableRawContact(Context context) { final int index = indexOfFirstWritableRawContact(context); return (index == -1) ? null : get(index); } public ValuesDelta getSuperPrimaryEntry(final String mimeType) { ValuesDelta primary = null; ValuesDelta randomEntry = null; for (RawContactDelta delta : this) { final ArrayList mimeEntries = delta.getMimeEntries(mimeType); if (mimeEntries == null) return null; for (ValuesDelta entry : mimeEntries) { if (entry.isSuperPrimary()) { return entry; } else if (primary == null && entry.isPrimary()) { primary = entry; } else if (randomEntry == null) { randomEntry = entry; } } } // When no direct super primary, return something if (primary != null) { return primary; } return randomEntry; } /** * Sets a flag that will split ("explode") the raw_contacts into seperate contacts */ public void markRawContactsForSplitting() { mSplitRawContacts = true; } public boolean isMarkedForSplitting() { return mSplitRawContacts; } public void setJoinWithRawContacts(long[] rawContactIds) { mJoinWithRawContactIds = rawContactIds; } public boolean isMarkedForJoining() { return mJoinWithRawContactIds != null && mJoinWithRawContactIds.length > 0; } /** {@inheritDoc} */ @Override public int describeContents() { // Nothing special about this parcel return 0; } /** {@inheritDoc} */ @Override public void writeToParcel(Parcel dest, int flags) { final int size = this.size(); dest.writeInt(size); for (RawContactDelta delta : this) { dest.writeParcelable(delta, flags); } dest.writeLongArray(mJoinWithRawContactIds); dest.writeInt(mSplitRawContacts ? 1 : 0); } @SuppressWarnings("unchecked") public void readFromParcel(Parcel source) { final ClassLoader loader = getClass().getClassLoader(); final int size = source.readInt(); for (int i = 0; i < size; i++) { this.add(source. readParcelable(loader)); } mJoinWithRawContactIds = source.createLongArray(); mSplitRawContacts = source.readInt() != 0; } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public RawContactDeltaList createFromParcel(Parcel in) { final RawContactDeltaList state = new RawContactDeltaList(); state.readFromParcel(in); return state; } @Override public RawContactDeltaList[] newArray(int size) { return new RawContactDeltaList[size]; } }; @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("("); sb.append("Split="); sb.append(mSplitRawContacts); sb.append(", Join=["); sb.append(Arrays.toString(mJoinWithRawContactIds)); sb.append("], Values="); sb.append(super.toString()); sb.append(")"); return sb.toString(); } }