blob: 69646436d67a9a32a09fe3ed9336182bfc06f3b3 [file] [log] [blame]
/*
* 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.compat.CompatUtils;
import com.android.contacts.common.model.CPOWrapper;
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<RawContactDelta> 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<ContentProviderOperation> buildDiff() {
if (VERBOSE_LOGGING) {
Log.v(TAG, "buildDiff: list=" + toString());
}
final ArrayList<ContentProviderOperation> 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;
}
/**
* For compatibility purpose, this method is copied from {@link #buildDiff} and returns an
* ArrayList of CPOWrapper.
*/
public ArrayList<CPOWrapper> buildDiffWrapper() {
if (VERBOSE_LOGGING) {
Log.v(TAG, "buildDiffWrapper: list=" + toString());
}
final ArrayList<CPOWrapper> diffWrapper = Lists.newArrayList();
final long rawContactId = this.findRawContactId();
int firstInsertRow = -1;
// First pass enforces versions remain consistent
for (RawContactDelta delta : this) {
delta.buildAssertWrapper(diffWrapper);
}
final int assertMark = diffWrapper.size();
int backRefs[] = new int[size()];
int rawContactIndex = 0;
// Second pass builds actual operations
for (RawContactDelta delta : this) {
final int firstBatch = diffWrapper.size();
final boolean isInsert = delta.isContactInsert();
backRefs[rawContactIndex++] = isInsert ? firstBatch : -1;
delta.buildDiffWrapper(diffWrapper);
// 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);
}
diffWrapper.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE));
}
}
// 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);
diffWrapper.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE));
} 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);
diffWrapper.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE));
}
}
if (mSplitRawContacts) {
buildSplitContactDiffWrapper(diffWrapper, backRefs);
}
// No real changes if only left with asserts
if (diffWrapper.size() == assertMark) {
diffWrapper.clear();
}
if (VERBOSE_LOGGING) {
Log.v(TAG, "buildDiff: ops=" + diffToStringWrapper(diffWrapper));
}
return diffWrapper;
}
private static String diffToString(ArrayList<ContentProviderOperation> ops) {
final StringBuilder sb = new StringBuilder();
sb.append("[\n");
for (ContentProviderOperation op : ops) {
sb.append(op.toString());
sb.append(",\n");
}
sb.append("]\n");
return sb.toString();
}
/**
* For compatibility purpose.
*/
private static String diffToStringWrapper(ArrayList<CPOWrapper> cpoWrappers) {
ArrayList<ContentProviderOperation> ops = Lists.newArrayList();
for (CPOWrapper cpoWrapper : cpoWrappers) {
ops.add(cpoWrapper.getOperation());
}
return diffToString(ops);
}
/**
* 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<ContentProviderOperation> diff,
int[] backRefs) {
final int count = size();
for (int i = 0; i < count; i++) {
for (int j = 0; j < count; j++) {
if (i == j) {
continue;
}
final Builder builder = buildSplitContactDiffHelper(i, j, backRefs);
if (builder != null) {
diff.add(builder.build());
}
}
}
}
/**
* For compatibility purpose, this method is copied from {@link #buildSplitContactDiff} and
* takes an ArrayList of CPOWrapper as parameter.
*/
private void buildSplitContactDiffWrapper(final ArrayList<CPOWrapper> diff, int[] backRefs) {
final int count = size();
for (int i = 0; i < count; i++) {
for (int j = 0; j < count; j++) {
if (i == j) {
continue;
}
final Builder builder = buildSplitContactDiffHelper(i, j, backRefs);
if (builder != null) {
diff.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE));
}
}
}
}
private Builder buildSplitContactDiffHelper(int index1, int index2, int[] backRefs) {
final 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 null;
}
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 null;
}
return builder;
}
/**
* 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<ValuesDelta> 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.<RawContactDelta> readParcelable(loader));
}
mJoinWithRawContactIds = source.createLongArray();
mSplitRawContacts = source.readInt() != 0;
}
public static final Parcelable.Creator<RawContactDeltaList> CREATOR =
new Parcelable.Creator<RawContactDeltaList>() {
@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();
}
}