blob: 2f281ef8aa27f396264abae7a7ea2cbf7fc4a3c4 [file] [log] [blame]
Yorke Lee2644d942013-10-28 11:05:43 -07001/*
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
17package com.android.contacts.common.model;
18
19import android.content.ContentProviderOperation;
20import android.content.ContentProviderOperation.Builder;
21import android.content.ContentResolver;
22import android.content.Context;
23import android.content.Entity;
24import android.content.EntityIterator;
25import android.net.Uri;
26import android.os.Parcel;
27import android.os.Parcelable;
28import android.provider.ContactsContract.AggregationExceptions;
29import android.provider.ContactsContract.Contacts;
30import android.provider.ContactsContract.RawContacts;
31import android.util.Log;
32
Wenyi Wang93fdd482015-12-07 14:26:54 -080033import com.android.contacts.common.compat.CompatUtils;
Wenyi Wang93fdd482015-12-07 14:26:54 -080034
Yorke Lee2644d942013-10-28 11:05:43 -070035import com.google.common.collect.Lists;
36
37import java.util.ArrayList;
38import java.util.Arrays;
39import 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 */
46public 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 ContentProviderOperation} 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<ContentProviderOperation> buildDiff() {
129 if (VERBOSE_LOGGING) {
130 Log.v(TAG, "buildDiff: list=" + toString());
131 }
132 final ArrayList<ContentProviderOperation> diff = 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.buildAssert(diff);
140 }
141
142 final int assertMark = diff.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 = diff.size();
150 final boolean isInsert = delta.isContactInsert();
151 backRefs[rawContactIndex++] = isInsert ? firstBatch : -1;
152
153 delta.buildDiff(diff);
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 diff.add(builder.build());
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 diff.add(builder.build());
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 diff.add(builder.build());
195 }
196 }
197
198 if (mSplitRawContacts) {
199 buildSplitContactDiff(diff, backRefs);
200 }
201
202 // No real changes if only left with asserts
203 if (diff.size() == assertMark) {
204 diff.clear();
205 }
206 if (VERBOSE_LOGGING) {
207 Log.v(TAG, "buildDiff: ops=" + diffToString(diff));
208 }
209 return diff;
210 }
211
Wenyi Wang93fdd482015-12-07 14:26:54 -0800212 /**
213 * For compatibility purpose, this method is copied from {@link #buildDiff} and returns an
214 * ArrayList of CPOWrapper.
215 */
216 public ArrayList<CPOWrapper> buildDiffWrapper() {
217 if (VERBOSE_LOGGING) {
218 Log.v(TAG, "buildDiffWrapper: list=" + toString());
219 }
220 final ArrayList<CPOWrapper> diffWrapper = Lists.newArrayList();
221
222 final long rawContactId = this.findRawContactId();
223 int firstInsertRow = -1;
224
225 // First pass enforces versions remain consistent
226 for (RawContactDelta delta : this) {
227 delta.buildAssertWrapper(diffWrapper);
228 }
229
230 final int assertMark = diffWrapper.size();
231 int backRefs[] = new int[size()];
232
233 int rawContactIndex = 0;
234
235 // Second pass builds actual operations
236 for (RawContactDelta delta : this) {
237 final int firstBatch = diffWrapper.size();
238 final boolean isInsert = delta.isContactInsert();
239 backRefs[rawContactIndex++] = isInsert ? firstBatch : -1;
240
241 delta.buildDiffWrapper(diffWrapper);
242
243 // If the user chose to join with some other existing raw contact(s) at save time,
244 // add aggregation exceptions for all those raw contacts.
245 if (mJoinWithRawContactIds != null) {
246 for (Long joinedRawContactId : mJoinWithRawContactIds) {
247 final Builder builder = beginKeepTogether();
248 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, joinedRawContactId);
249 if (rawContactId != -1) {
250 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId);
251 } else {
252 builder.withValueBackReference(
253 AggregationExceptions.RAW_CONTACT_ID2, firstBatch);
254 }
255 diffWrapper.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE));
256 }
257 }
258
259 // Only create rules for inserts
260 if (!isInsert) continue;
261
262 // If we are going to split all contacts, there is no point in first combining them
263 if (mSplitRawContacts) continue;
264
265 if (rawContactId != -1) {
266 // Has existing contact, so bind to it strongly
267 final Builder builder = beginKeepTogether();
268 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId);
269 builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, firstBatch);
270 diffWrapper.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE));
271
272 } else if (firstInsertRow == -1) {
273 // First insert case, so record row
274 firstInsertRow = firstBatch;
275
276 } else {
277 // Additional insert case, so point at first insert
278 final Builder builder = beginKeepTogether();
279 builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID1,
280 firstInsertRow);
281 builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, firstBatch);
282 diffWrapper.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE));
283 }
284 }
285
286 if (mSplitRawContacts) {
287 buildSplitContactDiffWrapper(diffWrapper, backRefs);
288 }
289
290 // No real changes if only left with asserts
291 if (diffWrapper.size() == assertMark) {
292 diffWrapper.clear();
293 }
294 if (VERBOSE_LOGGING) {
295 Log.v(TAG, "buildDiff: ops=" + diffToStringWrapper(diffWrapper));
296 }
297 return diffWrapper;
298 }
299
Yorke Lee2644d942013-10-28 11:05:43 -0700300 private static String diffToString(ArrayList<ContentProviderOperation> ops) {
Wenyi Wang93fdd482015-12-07 14:26:54 -0800301 final StringBuilder sb = new StringBuilder();
Yorke Lee2644d942013-10-28 11:05:43 -0700302 sb.append("[\n");
303 for (ContentProviderOperation op : ops) {
304 sb.append(op.toString());
305 sb.append(",\n");
306 }
307 sb.append("]\n");
308 return sb.toString();
309 }
310
311 /**
Wenyi Wang93fdd482015-12-07 14:26:54 -0800312 * For compatibility purpose.
313 */
314 private static String diffToStringWrapper(ArrayList<CPOWrapper> cpoWrappers) {
315 ArrayList<ContentProviderOperation> ops = Lists.newArrayList();
316 for (CPOWrapper cpoWrapper : cpoWrappers) {
317 ops.add(cpoWrapper.getOperation());
318 }
319 return diffToString(ops);
320 }
321
322 /**
Yorke Lee2644d942013-10-28 11:05:43 -0700323 * Start building a {@link ContentProviderOperation} that will keep two
324 * {@link RawContacts} together.
325 */
326 protected Builder beginKeepTogether() {
327 final Builder builder = ContentProviderOperation
328 .newUpdate(AggregationExceptions.CONTENT_URI);
329 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
330 return builder;
331 }
332
333 /**
334 * Builds {@link AggregationExceptions} to split all constituent raw contacts into
335 * separate contacts.
336 */
337 private void buildSplitContactDiff(final ArrayList<ContentProviderOperation> diff,
338 int[] backRefs) {
Wenyi Wang93fdd482015-12-07 14:26:54 -0800339 final int count = size();
Yorke Lee2644d942013-10-28 11:05:43 -0700340 for (int i = 0; i < count; i++) {
341 for (int j = 0; j < count; j++) {
Wenyi Wang93fdd482015-12-07 14:26:54 -0800342 if (i == j) {
343 continue;
344 }
345 final Builder builder = buildSplitContactDiffHelper(i, j, backRefs);
346 if (builder != null) {
347 diff.add(builder.build());
Yorke Lee2644d942013-10-28 11:05:43 -0700348 }
349 }
350 }
351 }
352
353 /**
Wenyi Wang93fdd482015-12-07 14:26:54 -0800354 * For compatibility purpose, this method is copied from {@link #buildSplitContactDiff} and
355 * takes an ArrayList of CPOWrapper as parameter.
Yorke Lee2644d942013-10-28 11:05:43 -0700356 */
Wenyi Wang93fdd482015-12-07 14:26:54 -0800357 private void buildSplitContactDiffWrapper(final ArrayList<CPOWrapper> diff, int[] backRefs) {
358 final int count = size();
359 for (int i = 0; i < count; i++) {
360 for (int j = 0; j < count; j++) {
361 if (i == j) {
362 continue;
363 }
364 final Builder builder = buildSplitContactDiffHelper(i, j, backRefs);
365 if (builder != null) {
366 diff.add(new CPOWrapper(builder.build(), CompatUtils.TYPE_UPDATE));
367 }
368 }
369 }
370 }
371
372 private Builder buildSplitContactDiffHelper(int index1, int index2, int[] backRefs) {
373 final Builder builder =
Yorke Lee2644d942013-10-28 11:05:43 -0700374 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
375 builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_SEPARATE);
376
377 Long rawContactId1 = get(index1).getValues().getAsLong(RawContacts._ID);
378 int backRef1 = backRefs[index1];
379 if (rawContactId1 != null && rawContactId1 >= 0) {
380 builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
381 } else if (backRef1 >= 0) {
382 builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID1, backRef1);
383 } else {
Wenyi Wang93fdd482015-12-07 14:26:54 -0800384 return null;
Yorke Lee2644d942013-10-28 11:05:43 -0700385 }
386
387 Long rawContactId2 = get(index2).getValues().getAsLong(RawContacts._ID);
388 int backRef2 = backRefs[index2];
389 if (rawContactId2 != null && rawContactId2 >= 0) {
390 builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
391 } else if (backRef2 >= 0) {
392 builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, backRef2);
393 } else {
Wenyi Wang93fdd482015-12-07 14:26:54 -0800394 return null;
Yorke Lee2644d942013-10-28 11:05:43 -0700395 }
Wenyi Wang93fdd482015-12-07 14:26:54 -0800396 return builder;
Yorke Lee2644d942013-10-28 11:05:43 -0700397 }
398
399 /**
400 * Search all contained {@link RawContactDelta} for the first one with an
401 * existing {@link RawContacts#_ID} value. Usually used when creating
402 * {@link AggregationExceptions} during an update.
403 */
404 public long findRawContactId() {
405 for (RawContactDelta delta : this) {
406 final Long rawContactId = delta.getValues().getAsLong(RawContacts._ID);
407 if (rawContactId != null && rawContactId >= 0) {
408 return rawContactId;
409 }
410 }
411 return -1;
412 }
413
414 /**
415 * Find {@link RawContacts#_ID} of the requested {@link RawContactDelta}.
416 */
417 public Long getRawContactId(int index) {
418 if (index >= 0 && index < this.size()) {
419 final RawContactDelta delta = this.get(index);
420 final ValuesDelta values = delta.getValues();
421 if (values.isVisible()) {
422 return values.getAsLong(RawContacts._ID);
423 }
424 }
425 return null;
426 }
427
428 /**
429 * Find the raw-contact (an {@link RawContactDelta}) with the specified ID.
430 */
431 public RawContactDelta getByRawContactId(Long rawContactId) {
432 final int index = this.indexOfRawContactId(rawContactId);
433 return (index == -1) ? null : this.get(index);
434 }
435
436 /**
437 * Find index of given {@link RawContacts#_ID} when present.
438 */
439 public int indexOfRawContactId(Long rawContactId) {
440 if (rawContactId == null) return -1;
441 final int size = this.size();
442 for (int i = 0; i < size; i++) {
443 final Long currentId = getRawContactId(i);
444 if (rawContactId.equals(currentId)) {
445 return i;
446 }
447 }
448 return -1;
449 }
450
451 /**
452 * Return the index of the first RawContactDelta corresponding to a writable raw-contact, or -1.
453 * */
454 public int indexOfFirstWritableRawContact(Context context) {
455 // Find the first writable entity.
456 int entityIndex = 0;
457 for (RawContactDelta delta : this) {
458 if (delta.getRawContactAccountType(context).areContactsWritable()) return entityIndex;
459 entityIndex++;
460 }
461 return -1;
462 }
463
464 /** Return the first RawContactDelta corresponding to a writable raw-contact, or null. */
465 public RawContactDelta getFirstWritableRawContact(Context context) {
466 final int index = indexOfFirstWritableRawContact(context);
467 return (index == -1) ? null : get(index);
468 }
469
470 public ValuesDelta getSuperPrimaryEntry(final String mimeType) {
471 ValuesDelta primary = null;
472 ValuesDelta randomEntry = null;
473 for (RawContactDelta delta : this) {
474 final ArrayList<ValuesDelta> mimeEntries = delta.getMimeEntries(mimeType);
475 if (mimeEntries == null) return null;
476
477 for (ValuesDelta entry : mimeEntries) {
478 if (entry.isSuperPrimary()) {
479 return entry;
480 } else if (primary == null && entry.isPrimary()) {
481 primary = entry;
482 } else if (randomEntry == null) {
483 randomEntry = entry;
484 }
485 }
486 }
487 // When no direct super primary, return something
488 if (primary != null) {
489 return primary;
490 }
491 return randomEntry;
492 }
493
494 /**
495 * Sets a flag that will split ("explode") the raw_contacts into seperate contacts
496 */
497 public void markRawContactsForSplitting() {
498 mSplitRawContacts = true;
499 }
500
501 public boolean isMarkedForSplitting() {
502 return mSplitRawContacts;
503 }
504
505 public void setJoinWithRawContacts(long[] rawContactIds) {
506 mJoinWithRawContactIds = rawContactIds;
507 }
508
509 public boolean isMarkedForJoining() {
510 return mJoinWithRawContactIds != null && mJoinWithRawContactIds.length > 0;
511 }
512
513 /** {@inheritDoc} */
514 @Override
515 public int describeContents() {
516 // Nothing special about this parcel
517 return 0;
518 }
519
520 /** {@inheritDoc} */
521 @Override
522 public void writeToParcel(Parcel dest, int flags) {
523 final int size = this.size();
524 dest.writeInt(size);
525 for (RawContactDelta delta : this) {
526 dest.writeParcelable(delta, flags);
527 }
528 dest.writeLongArray(mJoinWithRawContactIds);
529 dest.writeInt(mSplitRawContacts ? 1 : 0);
530 }
531
532 @SuppressWarnings("unchecked")
533 public void readFromParcel(Parcel source) {
534 final ClassLoader loader = getClass().getClassLoader();
535 final int size = source.readInt();
536 for (int i = 0; i < size; i++) {
537 this.add(source.<RawContactDelta> readParcelable(loader));
538 }
539 mJoinWithRawContactIds = source.createLongArray();
540 mSplitRawContacts = source.readInt() != 0;
541 }
542
543 public static final Parcelable.Creator<RawContactDeltaList> CREATOR =
544 new Parcelable.Creator<RawContactDeltaList>() {
545 @Override
546 public RawContactDeltaList createFromParcel(Parcel in) {
547 final RawContactDeltaList state = new RawContactDeltaList();
548 state.readFromParcel(in);
549 return state;
550 }
551
552 @Override
553 public RawContactDeltaList[] newArray(int size) {
554 return new RawContactDeltaList[size];
555 }
556 };
557
558 @Override
559 public String toString() {
560 StringBuilder sb = new StringBuilder();
561 sb.append("(");
562 sb.append("Split=");
563 sb.append(mSplitRawContacts);
564 sb.append(", Join=[");
565 sb.append(Arrays.toString(mJoinWithRawContactIds));
566 sb.append("], Values=");
567 sb.append(super.toString());
568 sb.append(")");
569 return sb.toString();
570 }
571}