/*
* 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 cn.edu.tsinghua.hpc.tcontacts.model;
import java.lang.reflect.Method;
import java.util.ArrayList;
import android.content.ContentProviderOperation;
import android.content.ContentProviderOperation.Builder;
import android.content.ContentResolver;
import android.content.Entity;
import android.content.EntityIterator;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
import android.provider.ContactsContract.AggregationExceptions;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.RawContacts;
import android.util.Log;
import cn.edu.tsinghua.hpc.tcontacts.model.EntityDelta.ValuesDelta;
import cn.edu.tsinghua.hpc.tcontacts.util.TContactsContract.TAggregationExceptions;
import cn.edu.tsinghua.hpc.tcontacts.util.TContactsContract.TRawContacts;
import com.google.android.collect.Lists;
/**
* Container for multiple {@link EntityDelta} objects, usually when editing
* together as an entire aggregate. Provides convenience methods for parceling
* and applying another {@link EntitySet} over it.
*/
public class EntitySet extends ArrayList<EntityDelta> implements Parcelable {
private boolean mSplitRawContacts;
private EntitySet() {
}
/**
* Create an {@link EntitySet} that contains the given {@link EntityDelta},
* usually when inserting a new {@link Contacts} entry.
*/
public static EntitySet fromSingle(EntityDelta delta) {
final EntitySet state = new EntitySet();
state.add(delta);
return state;
}
/**
* Create an {@link EntitySet} 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 EntitySet fromQuery(ContentResolver resolver, String selection,
String[] selectionArgs, String sortOrder) {
EntityIterator iterator = null;
final EntitySet state = new EntitySet();
try {
// Perform background query to pull contact details
Method queryEntitiesMethod = Class.forName(
"android.content.ContentResolver").getMethod(
"queryEntities",
new Class[] { Uri.class, String.class, String[].class,
String.class });
queryEntitiesMethod.setAccessible(true);
iterator = (EntityIterator) queryEntitiesMethod.invoke(resolver,
new Object[] { TRawContacts.CONTENT_URI, selection,
selectionArgs, sortOrder });
// iterator = resolver.queryEntities(TRawContacts.CONTENT_URI,
// selection, selectionArgs,
// sortOrder);
while (iterator.hasNext()) {
// Read all contacts into local deltas to prepare for edits
final Entity before = iterator.next();
final EntityDelta entity = EntityDelta.fromBefore(before);
state.add(entity);
}
} catch (RemoteException e) {
throw new IllegalStateException("Problem querying contact details",
e);
} catch (Exception e) {
Log.d("TContact", e.getMessage());
} finally {
if (iterator != null) {
iterator.close();
}
}
return state;
}
/**
* Merge the "after" values from the given {@link EntitySet}, discarding any
* previous "after" states. This is typically used when re-parenting user
* edits onto an updated {@link EntitySet}.
*/
public static EntitySet mergeAfter(EntitySet local, EntitySet remote) {
if (local == null) local = new EntitySet();
// For each entity in the remote set, try matching over existing
for (EntityDelta remoteEntity : remote) {
final Long rawContactId = remoteEntity.getValues().getId();
// Find or create local match and merge
final EntityDelta localEntity = local.getByRawContactId(rawContactId);
final EntityDelta merged = EntityDelta.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 EntityDelta} objects represent. This method specifically creates
* any {@link AggregationExceptions} rules needed to groups edits together.
*/
public ArrayList<ContentProviderOperation> buildDiff() {
final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
final long rawContactId = this.findRawContactId();
int firstInsertRow = -1;
// First pass enforces versions remain consistent
for (EntityDelta delta : this) {
delta.buildAssert(diff);
}
final int assertMark = diff.size();
int backRefs[] = new int[size()];
int rawContactIndex = 0;
// Second pass builds actual operations
for (EntityDelta delta : this) {
final int firstBatch = diff.size();
backRefs[rawContactIndex++] = firstBatch;
delta.buildDiff(diff);
// Only create rules for inserts
if (!delta.isContactInsert()) 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();
}
return diff;
}
/**
* Start building a {@link ContentProviderOperation} that will keep two
* {@link RawContacts} together.
*/
protected Builder beginKeepTogether() {
// final Builder builder = ContentProviderOperation
// .newUpdate(AggregationExceptions.CONTENT_URI);
//sdd by chenqiang
final Builder builder = ContentProviderOperation
.newUpdate(TAggregationExceptions.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) {
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<ContentProviderOperation> diff, int index1,
int index2, int[] backRefs) {
// Builder builder =
// ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
//add by chenqiang
Builder builder =
ContentProviderOperation.newUpdate(TAggregationExceptions.CONTENT_URI);
builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_SEPARATE);
Long rawContactId1 = get(index1).getValues().getAsLong(RawContacts._ID);
if (rawContactId1 != null && rawContactId1 >= 0) {
builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
} else {
builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID1, backRefs[index1]);
}
Long rawContactId2 = get(index2).getValues().getAsLong(RawContacts._ID);
if (rawContactId2 != null && rawContactId2 >= 0) {
builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
} else {
builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, backRefs[index2]);
}
diff.add(builder.build());
}
/**
* Search all contained {@link EntityDelta} for the first one with an
* existing {@link RawContacts#_ID} value. Usually used when creating
* {@link AggregationExceptions} during an update.
*/
public long findRawContactId() {
for (EntityDelta 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 EntityDelta}.
*/
public Long getRawContactId(int index) {
if (index >= 0 && index < this.size()) {
final EntityDelta delta = this.get(index);
final ValuesDelta values = delta.getValues();
if (values.isVisible()) {
return values.getAsLong(RawContacts._ID);
}
}
return null;
}
public EntityDelta 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;
}
public ValuesDelta getSuperPrimaryEntry(final String mimeType) {
ValuesDelta primary = null;
ValuesDelta randomEntry = null;
for (EntityDelta 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;
}
public void splitRawContacts() {
mSplitRawContacts = true;
}
/** {@inheritDoc} */
public int describeContents() {
// Nothing special about this parcel
return 0;
}
/** {@inheritDoc} */
public void writeToParcel(Parcel dest, int flags) {
final int size = this.size();
dest.writeInt(size);
for (EntityDelta delta : this) {
dest.writeParcelable(delta, flags);
}
}
public void readFromParcel(Parcel source) {
final int size = source.readInt();
for (int i = 0; i < size; i++) {
this.add(source.<EntityDelta> readParcelable(null));
}
}
public static final Parcelable.Creator<EntitySet> CREATOR = new Parcelable.Creator<EntitySet>() {
public EntitySet createFromParcel(Parcel in) {
final EntitySet state = new EntitySet();
state.readFromParcel(in);
return state;
}
public EntitySet[] newArray(int size) {
return new EntitySet[size];
}
};
}