blob: d6e08a8fbf1932d7ae65b0ca9d2fbfd3b8c02b6a [file] [log] [blame]
Chiao Cheng429b9112012-11-30 12:04:14 -08001/*
2 * Copyright (C) 2012 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.ContentValues;
21import android.net.Uri;
22import android.os.Parcel;
23import android.os.Parcelable;
24import android.provider.BaseColumns;
25import android.provider.ContactsContract;
Gary Mai698cee72016-09-19 16:09:54 -070026import android.provider.ContactsContract.CommonDataKinds.StructuredName;
27import android.text.TextUtils;
Chiao Cheng429b9112012-11-30 12:04:14 -080028
Wenyi Wang93fdd482015-12-07 14:26:54 -080029import com.android.contacts.common.compat.CompatUtils;
Gary Mai698cee72016-09-19 16:09:54 -070030
Chiao Cheng429b9112012-11-30 12:04:14 -080031import com.google.common.collect.Sets;
32
33import java.util.HashSet;
34import java.util.Map;
35import java.util.Set;
36
37/**
38 * Type of {@link android.content.ContentValues} that maintains both an original state and a
39 * modified version of that state. This allows us to build insert, update,
40 * or delete operations based on a "before" {@link Entity} snapshot.
41 */
42public class ValuesDelta implements Parcelable {
43 protected ContentValues mBefore;
44 protected ContentValues mAfter;
45 protected String mIdColumn = BaseColumns._ID;
46 private boolean mFromTemplate;
47
48 /**
49 * Next value to assign to {@link #mIdColumn} when building an insert
50 * operation through {@link #fromAfter(android.content.ContentValues)}. This is used so
51 * we can concretely reference this {@link ValuesDelta} before it has
52 * been persisted.
53 */
54 protected static int sNextInsertId = -1;
55
56 protected ValuesDelta() {
57 }
58
59 /**
60 * Create {@link ValuesDelta}, using the given object as the
61 * "before" state, usually from an {@link Entity}.
62 */
63 public static ValuesDelta fromBefore(ContentValues before) {
64 final ValuesDelta entry = new ValuesDelta();
65 entry.mBefore = before;
66 entry.mAfter = new ContentValues();
67 return entry;
68 }
69
70 /**
71 * Create {@link ValuesDelta}, using the given object as the "after"
72 * state, usually when we are inserting a row instead of updating.
73 */
74 public static ValuesDelta fromAfter(ContentValues after) {
75 final ValuesDelta entry = new ValuesDelta();
76 entry.mBefore = null;
77 entry.mAfter = after;
78
79 // Assign temporary id which is dropped before insert.
80 entry.mAfter.put(entry.mIdColumn, sNextInsertId--);
81 return entry;
82 }
83
Chiao Cheng429b9112012-11-30 12:04:14 -080084 public ContentValues getAfter() {
85 return mAfter;
86 }
87
Gary Mai698cee72016-09-19 16:09:54 -070088 public ContentValues getBefore() {
89 return mBefore;
90 }
91
Chiao Cheng429b9112012-11-30 12:04:14 -080092 public boolean containsKey(String key) {
93 return ((mAfter != null && mAfter.containsKey(key)) ||
94 (mBefore != null && mBefore.containsKey(key)));
95 }
96
97 public String getAsString(String key) {
98 if (mAfter != null && mAfter.containsKey(key)) {
99 return mAfter.getAsString(key);
100 } else if (mBefore != null && mBefore.containsKey(key)) {
101 return mBefore.getAsString(key);
102 } else {
103 return null;
104 }
105 }
106
107 public byte[] getAsByteArray(String key) {
108 if (mAfter != null && mAfter.containsKey(key)) {
109 return mAfter.getAsByteArray(key);
110 } else if (mBefore != null && mBefore.containsKey(key)) {
111 return mBefore.getAsByteArray(key);
112 } else {
113 return null;
114 }
115 }
116
117 public Long getAsLong(String key) {
118 if (mAfter != null && mAfter.containsKey(key)) {
119 return mAfter.getAsLong(key);
120 } else if (mBefore != null && mBefore.containsKey(key)) {
121 return mBefore.getAsLong(key);
122 } else {
123 return null;
124 }
125 }
126
127 public Integer getAsInteger(String key) {
128 return getAsInteger(key, null);
129 }
130
131 public Integer getAsInteger(String key, Integer defaultValue) {
132 if (mAfter != null && mAfter.containsKey(key)) {
133 return mAfter.getAsInteger(key);
134 } else if (mBefore != null && mBefore.containsKey(key)) {
135 return mBefore.getAsInteger(key);
136 } else {
137 return defaultValue;
138 }
139 }
140
141 public boolean isChanged(String key) {
142 if (mAfter == null || !mAfter.containsKey(key)) {
143 return false;
144 }
145
146 Object newValue = mAfter.get(key);
147 Object oldValue = mBefore.get(key);
148
149 if (oldValue == null) {
150 return newValue != null;
151 }
152
153 return !oldValue.equals(newValue);
154 }
155
156 public String getMimetype() {
157 return getAsString(ContactsContract.Data.MIMETYPE);
158 }
159
160 public Long getId() {
161 return getAsLong(mIdColumn);
162 }
163
164 public void setIdColumn(String idColumn) {
165 mIdColumn = idColumn;
166 }
167
168 public boolean isPrimary() {
169 final Long isPrimary = getAsLong(ContactsContract.Data.IS_PRIMARY);
170 return isPrimary == null ? false : isPrimary != 0;
171 }
172
173 public void setFromTemplate(boolean isFromTemplate) {
174 mFromTemplate = isFromTemplate;
175 }
176
177 public boolean isFromTemplate() {
178 return mFromTemplate;
179 }
180
181 public boolean isSuperPrimary() {
182 final Long isSuperPrimary = getAsLong(ContactsContract.Data.IS_SUPER_PRIMARY);
183 return isSuperPrimary == null ? false : isSuperPrimary != 0;
184 }
185
186 public boolean beforeExists() {
187 return (mBefore != null && mBefore.containsKey(mIdColumn));
188 }
189
190 /**
191 * When "after" is present, then visible
192 */
193 public boolean isVisible() {
194 return (mAfter != null);
195 }
196
197 /**
198 * When "after" is wiped, action is "delete"
199 */
200 public boolean isDelete() {
201 return beforeExists() && (mAfter == null);
202 }
203
204 /**
205 * When no "before" or "after", is transient
206 */
207 public boolean isTransient() {
208 return (mBefore == null) && (mAfter == null);
209 }
210
211 /**
212 * When "after" has some changes, action is "update"
213 */
214 public boolean isUpdate() {
215 if (!beforeExists() || mAfter == null || mAfter.size() == 0) {
216 return false;
217 }
218 for (String key : mAfter.keySet()) {
219 Object newValue = mAfter.get(key);
220 Object oldValue = mBefore.get(key);
221 if (oldValue == null) {
222 if (newValue != null) {
223 return true;
224 }
225 } else if (!oldValue.equals(newValue)) {
226 return true;
227 }
228 }
229 return false;
230 }
231
232 /**
233 * When "after" has no changes, action is no-op
234 */
235 public boolean isNoop() {
236 return beforeExists() && (mAfter != null && mAfter.size() == 0);
237 }
238
239 /**
240 * When no "before" id, and has "after", action is "insert"
241 */
242 public boolean isInsert() {
243 return !beforeExists() && (mAfter != null);
244 }
245
246 public void markDeleted() {
247 mAfter = null;
248 }
249
250 /**
251 * Ensure that our internal structure is ready for storing updates.
252 */
253 private void ensureUpdate() {
254 if (mAfter == null) {
255 mAfter = new ContentValues();
256 }
257 }
258
259 public void put(String key, String value) {
260 ensureUpdate();
261 mAfter.put(key, value);
262 }
263
264 public void put(String key, byte[] value) {
265 ensureUpdate();
266 mAfter.put(key, value);
267 }
268
269 public void put(String key, int value) {
270 ensureUpdate();
271 mAfter.put(key, value);
272 }
273
274 public void put(String key, long value) {
275 ensureUpdate();
276 mAfter.put(key, value);
277 }
278
279 public void putNull(String key) {
280 ensureUpdate();
281 mAfter.putNull(key);
282 }
283
284 public void copyStringFrom(ValuesDelta from, String key) {
285 ensureUpdate();
Jay Shrauneraa35e932015-07-30 12:12:02 -0700286 if (containsKey(key) || from.containsKey(key)) {
287 put(key, from.getAsString(key));
288 }
Chiao Cheng429b9112012-11-30 12:04:14 -0800289 }
290
291 /**
292 * Return set of all keys defined through this object.
293 */
294 public Set<String> keySet() {
295 final HashSet<String> keys = Sets.newHashSet();
296
297 if (mBefore != null) {
298 for (Map.Entry<String, Object> entry : mBefore.valueSet()) {
299 keys.add(entry.getKey());
300 }
301 }
302
303 if (mAfter != null) {
304 for (Map.Entry<String, Object> entry : mAfter.valueSet()) {
305 keys.add(entry.getKey());
306 }
307 }
308
309 return keys;
310 }
311
312 /**
313 * Return complete set of "before" and "after" values mixed together,
314 * giving full state regardless of edits.
315 */
316 public ContentValues getCompleteValues() {
317 final ContentValues values = new ContentValues();
318 if (mBefore != null) {
319 values.putAll(mBefore);
320 }
321 if (mAfter != null) {
322 values.putAll(mAfter);
323 }
324 if (values.containsKey(ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID)) {
325 // Clear to avoid double-definitions, and prefer rows
326 values.remove(ContactsContract.CommonDataKinds.GroupMembership.GROUP_SOURCE_ID);
327 }
328
329 return values;
330 }
331
332 /**
333 * Merge the "after" values from the given {@link ValuesDelta},
334 * discarding any existing "after" state. This is typically used when
335 * re-parenting changes onto an updated {@link Entity}.
336 */
337 public static ValuesDelta mergeAfter(ValuesDelta local, ValuesDelta remote) {
338 // Bail early if trying to merge delete with missing local
339 if (local == null && (remote.isDelete() || remote.isTransient())) return null;
340
341 // Create local version if none exists yet
342 if (local == null) local = new ValuesDelta();
343
344 if (!local.beforeExists()) {
345 // Any "before" record is missing, so take all values as "insert"
346 local.mAfter = remote.getCompleteValues();
347 } else {
348 // Existing "update" with only "after" values
349 local.mAfter = remote.mAfter;
350 }
351
352 return local;
353 }
354
355 @Override
356 public boolean equals(Object object) {
357 if (object instanceof ValuesDelta) {
358 // Only exactly equal with both are identical subsets
359 final ValuesDelta other = (ValuesDelta)object;
360 return this.subsetEquals(other) && other.subsetEquals(this);
361 }
362 return false;
363 }
364
365 @Override
366 public String toString() {
367 final StringBuilder builder = new StringBuilder();
368 toString(builder);
369 return builder.toString();
370 }
371
372 /**
373 * Helper for building string representation, leveraging the given
374 * {@link StringBuilder} to minimize allocations.
375 */
376 public void toString(StringBuilder builder) {
377 builder.append("{ ");
378 builder.append("IdColumn=");
379 builder.append(mIdColumn);
380 builder.append(", FromTemplate=");
381 builder.append(mFromTemplate);
382 builder.append(", ");
383 for (String key : this.keySet()) {
384 builder.append(key);
385 builder.append("=");
386 builder.append(this.getAsString(key));
387 builder.append(", ");
388 }
389 builder.append("}");
390 }
391
392 /**
393 * Check if the given {@link ValuesDelta} is both a subset of this
394 * object, and any defined keys have equal values.
395 */
396 public boolean subsetEquals(ValuesDelta other) {
397 for (String key : this.keySet()) {
398 final String ourValue = this.getAsString(key);
399 final String theirValue = other.getAsString(key);
400 if (ourValue == null) {
401 // If they have value when we're null, no match
402 if (theirValue != null) return false;
403 } else {
404 // If both values defined and aren't equal, no match
405 if (!ourValue.equals(theirValue)) return false;
406 }
407 }
408 // All values compared and matched
409 return true;
410 }
411
412 /**
413 * Build a {@link android.content.ContentProviderOperation} that will transform our
414 * "before" state into our "after" state, using insert, update, or
415 * delete as needed.
416 */
417 public ContentProviderOperation.Builder buildDiff(Uri targetUri) {
Wenyi Wang93fdd482015-12-07 14:26:54 -0800418 return buildDiffHelper(targetUri);
419 }
420
421 /**
422 * For compatibility purpose.
423 */
424 public BuilderWrapper buildDiffWrapper(Uri targetUri) {
425 final ContentProviderOperation.Builder builder = buildDiffHelper(targetUri);
426 BuilderWrapper bw = null;
427 if (isInsert()) {
428 bw = new BuilderWrapper(builder, CompatUtils.TYPE_INSERT);
429 } else if (isDelete()) {
430 bw = new BuilderWrapper(builder, CompatUtils.TYPE_DELETE);
431 } else if (isUpdate()) {
432 bw = new BuilderWrapper(builder, CompatUtils.TYPE_UPDATE);
433 }
434 return bw;
435 }
436
437 private ContentProviderOperation.Builder buildDiffHelper(Uri targetUri) {
Chiao Cheng429b9112012-11-30 12:04:14 -0800438 ContentProviderOperation.Builder builder = null;
439 if (isInsert()) {
440 // Changed values are "insert" back-referenced to Contact
441 mAfter.remove(mIdColumn);
442 builder = ContentProviderOperation.newInsert(targetUri);
443 builder.withValues(mAfter);
444 } else if (isDelete()) {
445 // When marked for deletion and "before" exists, then "delete"
446 builder = ContentProviderOperation.newDelete(targetUri);
447 builder.withSelection(mIdColumn + "=" + getId(), null);
448 } else if (isUpdate()) {
449 // When has changes and "before" exists, then "update"
450 builder = ContentProviderOperation.newUpdate(targetUri);
451 builder.withSelection(mIdColumn + "=" + getId(), null);
452 builder.withValues(mAfter);
453 }
454 return builder;
455 }
456
457 /** {@inheritDoc} */
458 public int describeContents() {
459 // Nothing special about this parcel
460 return 0;
461 }
462
463 /** {@inheritDoc} */
464 public void writeToParcel(Parcel dest, int flags) {
465 dest.writeParcelable(mBefore, flags);
466 dest.writeParcelable(mAfter, flags);
467 dest.writeString(mIdColumn);
468 }
469
470 public void readFromParcel(Parcel source) {
471 final ClassLoader loader = getClass().getClassLoader();
472 mBefore = source.<ContentValues> readParcelable(loader);
473 mAfter = source.<ContentValues> readParcelable(loader);
474 mIdColumn = source.readString();
475 }
476
477 public static final Creator<ValuesDelta> CREATOR = new Creator<ValuesDelta>() {
478 public ValuesDelta createFromParcel(Parcel in) {
479 final ValuesDelta values = new ValuesDelta();
480 values.readFromParcel(in);
481 return values;
482 }
483
484 public ValuesDelta[] newArray(int size) {
485 return new ValuesDelta[size];
486 }
487 };
488
489 public void setGroupRowId(long groupId) {
490 put(ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID, groupId);
491 }
492
493 public Long getGroupRowId() {
494 return getAsLong(ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID);
495 }
496
497 public void setPhoto(byte[] value) {
498 put(ContactsContract.CommonDataKinds.Photo.PHOTO, value);
499 }
500
501 public byte[] getPhoto() {
502 return getAsByteArray(ContactsContract.CommonDataKinds.Photo.PHOTO);
503 }
504
505 public void setSuperPrimary(boolean val) {
506 if (val) {
507 put(ContactsContract.Data.IS_SUPER_PRIMARY, 1);
508 } else {
509 put(ContactsContract.Data.IS_SUPER_PRIMARY, 0);
510 }
511 }
512
513 public void setPhoneticFamilyName(String value) {
514 put(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_FAMILY_NAME, value);
515 }
516
517 public void setPhoneticMiddleName(String value) {
518 put(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_MIDDLE_NAME, value);
519 }
520
521 public void setPhoneticGivenName(String value) {
522 put(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_GIVEN_NAME, value);
523 }
524
525 public String getPhoneticFamilyName() {
526 return getAsString(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_FAMILY_NAME);
527 }
528
529 public String getPhoneticMiddleName() {
530 return getAsString(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_MIDDLE_NAME);
531 }
532
533 public String getPhoneticGivenName() {
534 return getAsString(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_GIVEN_NAME);
535 }
536
537 public String getDisplayName() {
538 return getAsString(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME);
539 }
540
541 public void setDisplayName(String name) {
542 if (name == null) {
543 putNull(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME);
544 } else {
545 put(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name);
546 }
547 }
548
549 public void copyStructuredNameFieldsFrom(ValuesDelta name) {
550 copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME);
551
552 copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME);
553 copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME);
554 copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.PREFIX);
555 copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME);
556 copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.SUFFIX);
557
558 copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.PHONETIC_GIVEN_NAME);
559 copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.PHONETIC_MIDDLE_NAME);
560 copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.PHONETIC_FAMILY_NAME);
561
562 copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.FULL_NAME_STYLE);
Jay Shrauneraa35e932015-07-30 12:12:02 -0700563 copyStringFrom(name, ContactsContract.Data.DATA11);
Chiao Cheng429b9112012-11-30 12:04:14 -0800564 }
565
566 public String getPhoneNumber() {
567 return getAsString(ContactsContract.CommonDataKinds.Phone.NUMBER);
568 }
569
570 public String getPhoneNormalizedNumber() {
571 return getAsString(ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER);
572 }
573
Jay Shrauner26d51ed2015-09-29 14:52:55 -0700574 public boolean hasPhoneType() {
575 return getPhoneType() != null;
Chiao Cheng429b9112012-11-30 12:04:14 -0800576 }
577
Jay Shrauner26d51ed2015-09-29 14:52:55 -0700578 public Integer getPhoneType() {
Chiao Cheng429b9112012-11-30 12:04:14 -0800579 return getAsInteger(ContactsContract.CommonDataKinds.Phone.TYPE);
580 }
581
582 public String getPhoneLabel() {
583 return getAsString(ContactsContract.CommonDataKinds.Phone.LABEL);
584 }
585
586 public String getEmailData() {
587 return getAsString(ContactsContract.CommonDataKinds.Email.DATA);
588 }
589
Jay Shrauner26d51ed2015-09-29 14:52:55 -0700590 public boolean hasEmailType() {
591 return getEmailType() != null;
Chiao Cheng429b9112012-11-30 12:04:14 -0800592 }
593
Jay Shrauner26d51ed2015-09-29 14:52:55 -0700594 public Integer getEmailType() {
Chiao Cheng429b9112012-11-30 12:04:14 -0800595 return getAsInteger(ContactsContract.CommonDataKinds.Email.TYPE);
596 }
597
598 public String getEmailLabel() {
599 return getAsString(ContactsContract.CommonDataKinds.Email.LABEL);
600 }
601}