/* * Copyright (C) 2012 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.ContentValues; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import android.provider.BaseColumns; import android.provider.ContactsContract; import com.android.contacts.common.test.NeededForTesting; import com.google.common.collect.Sets; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * Type of {@link android.content.ContentValues} that maintains both an original state and a * modified version of that state. This allows us to build insert, update, * or delete operations based on a "before" {@link Entity} snapshot. */ public class ValuesDelta implements Parcelable { protected ContentValues mBefore; protected ContentValues mAfter; protected String mIdColumn = BaseColumns._ID; private boolean mFromTemplate; /** * Next value to assign to {@link #mIdColumn} when building an insert * operation through {@link #fromAfter(android.content.ContentValues)}. This is used so * we can concretely reference this {@link ValuesDelta} before it has * been persisted. */ protected static int sNextInsertId = -1; protected ValuesDelta() { } /** * Create {@link ValuesDelta}, using the given object as the * "before" state, usually from an {@link Entity}. */ public static ValuesDelta fromBefore(ContentValues before) { final ValuesDelta entry = new ValuesDelta(); entry.mBefore = before; entry.mAfter = new ContentValues(); return entry; } /** * Create {@link ValuesDelta}, using the given object as the "after" * state, usually when we are inserting a row instead of updating. */ public static ValuesDelta fromAfter(ContentValues after) { final ValuesDelta entry = new ValuesDelta(); entry.mBefore = null; entry.mAfter = after; // Assign temporary id which is dropped before insert. entry.mAfter.put(entry.mIdColumn, sNextInsertId--); return entry; } @NeededForTesting public ContentValues getAfter() { return mAfter; } public boolean containsKey(String key) { return ((mAfter != null && mAfter.containsKey(key)) || (mBefore != null && mBefore.containsKey(key))); } public String getAsString(String key) { if (mAfter != null && mAfter.containsKey(key)) { return mAfter.getAsString(key); } else if (mBefore != null && mBefore.containsKey(key)) { return mBefore.getAsString(key); } else { return null; } } public byte[] getAsByteArray(String key) { if (mAfter != null && mAfter.containsKey(key)) { return mAfter.getAsByteArray(key); } else if (mBefore != null && mBefore.containsKey(key)) { return mBefore.getAsByteArray(key); } else { return null; } } public Long getAsLong(String key) { if (mAfter != null && mAfter.containsKey(key)) { return mAfter.getAsLong(key); } else if (mBefore != null && mBefore.containsKey(key)) { return mBefore.getAsLong(key); } else { return null; } } public Integer getAsInteger(String key) { return getAsInteger(key, null); } public Integer getAsInteger(String key, Integer defaultValue) { if (mAfter != null && mAfter.containsKey(key)) { return mAfter.getAsInteger(key); } else if (mBefore != null && mBefore.containsKey(key)) { return mBefore.getAsInteger(key); } else { return defaultValue; } } public boolean isChanged(String key) { if (mAfter == null || !mAfter.containsKey(key)) { return false; } Object newValue = mAfter.get(key); Object oldValue = mBefore.get(key); if (oldValue == null) { return newValue != null; } return !oldValue.equals(newValue); } public String getMimetype() { return getAsString(ContactsContract.Data.MIMETYPE); } public Long getId() { return getAsLong(mIdColumn); } public void setIdColumn(String idColumn) { mIdColumn = idColumn; } public boolean isPrimary() { final Long isPrimary = getAsLong(ContactsContract.Data.IS_PRIMARY); return isPrimary == null ? false : isPrimary != 0; } public void setFromTemplate(boolean isFromTemplate) { mFromTemplate = isFromTemplate; } public boolean isFromTemplate() { return mFromTemplate; } public boolean isSuperPrimary() { final Long isSuperPrimary = getAsLong(ContactsContract.Data.IS_SUPER_PRIMARY); return isSuperPrimary == null ? false : isSuperPrimary != 0; } public boolean beforeExists() { return (mBefore != null && mBefore.containsKey(mIdColumn)); } /** * When "after" is present, then visible */ public boolean isVisible() { return (mAfter != null); } /** * When "after" is wiped, action is "delete" */ public boolean isDelete() { return beforeExists() && (mAfter == null); } /** * When no "before" or "after", is transient */ public boolean isTransient() { return (mBefore == null) && (mAfter == null); } /** * When "after" has some changes, action is "update" */ public boolean isUpdate() { if (!beforeExists() || mAfter == null || mAfter.size() == 0) { return false; } for (String key : mAfter.keySet()) { Object newValue = mAfter.get(key); Object oldValue = mBefore.get(key); if (oldValue == null) { if (newValue != null) { return true; } } else if (!oldValue.equals(newValue)) { return true; } } return false; } /** * When "after" has no changes, action is no-op */ public boolean isNoop() { return beforeExists() && (mAfter != null && mAfter.size() == 0); } /** * When no "before" id, and has "after", action is "insert" */ public boolean isInsert() { return !beforeExists() && (mAfter != null); } public void markDeleted() { mAfter = null; } /** * Ensure that our internal structure is ready for storing updates. */ private void ensureUpdate() { if (mAfter == null) { mAfter = new ContentValues(); } } public void put(String key, String value) { ensureUpdate(); mAfter.put(key, value); } public void put(String key, byte[] value) { ensureUpdate(); mAfter.put(key, value); } public void put(String key, int value) { ensureUpdate(); mAfter.put(key, value); } public void put(String key, long value) { ensureUpdate(); mAfter.put(key, value); } public void putNull(String key) { ensureUpdate(); mAfter.putNull(key); } public void copyStringFrom(ValuesDelta from, String key) { ensureUpdate(); put(key, from.getAsString(key)); } /** * Return set of all keys defined through this object. */ public Set keySet() { final HashSet keys = Sets.newHashSet(); if (mBefore != null) { for (Map.Entry entry : mBefore.valueSet()) { keys.add(entry.getKey()); } } if (mAfter != null) { for (Map.Entry entry : mAfter.valueSet()) { keys.add(entry.getKey()); } } return keys; } /** * Return complete set of "before" and "after" values mixed together, * giving full state regardless of edits. */ public ContentValues getCompleteValues() { final ContentValues values = new ContentValues(); if (mBefore != null) { values.putAll(mBefore); } if (mAfter != null) { values.putAll(mAfter); } if (values.containsKey(ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID)) { // Clear to avoid double-definitions, and prefer rows values.remove(ContactsContract.CommonDataKinds.GroupMembership.GROUP_SOURCE_ID); } return values; } /** * Merge the "after" values from the given {@link ValuesDelta}, * discarding any existing "after" state. This is typically used when * re-parenting changes onto an updated {@link Entity}. */ public static ValuesDelta mergeAfter(ValuesDelta local, ValuesDelta remote) { // Bail early if trying to merge delete with missing local if (local == null && (remote.isDelete() || remote.isTransient())) return null; // Create local version if none exists yet if (local == null) local = new ValuesDelta(); if (!local.beforeExists()) { // Any "before" record is missing, so take all values as "insert" local.mAfter = remote.getCompleteValues(); } else { // Existing "update" with only "after" values local.mAfter = remote.mAfter; } return local; } @Override public boolean equals(Object object) { if (object instanceof ValuesDelta) { // Only exactly equal with both are identical subsets final ValuesDelta other = (ValuesDelta)object; return this.subsetEquals(other) && other.subsetEquals(this); } return false; } @Override public String toString() { final StringBuilder builder = new StringBuilder(); toString(builder); return builder.toString(); } /** * Helper for building string representation, leveraging the given * {@link StringBuilder} to minimize allocations. */ public void toString(StringBuilder builder) { builder.append("{ "); builder.append("IdColumn="); builder.append(mIdColumn); builder.append(", FromTemplate="); builder.append(mFromTemplate); builder.append(", "); for (String key : this.keySet()) { builder.append(key); builder.append("="); builder.append(this.getAsString(key)); builder.append(", "); } builder.append("}"); } /** * Check if the given {@link ValuesDelta} is both a subset of this * object, and any defined keys have equal values. */ public boolean subsetEquals(ValuesDelta other) { for (String key : this.keySet()) { final String ourValue = this.getAsString(key); final String theirValue = other.getAsString(key); if (ourValue == null) { // If they have value when we're null, no match if (theirValue != null) return false; } else { // If both values defined and aren't equal, no match if (!ourValue.equals(theirValue)) return false; } } // All values compared and matched return true; } /** * Build a {@link android.content.ContentProviderOperation} that will transform our * "before" state into our "after" state, using insert, update, or * delete as needed. */ public ContentProviderOperation.Builder buildDiff(Uri targetUri) { ContentProviderOperation.Builder builder = null; if (isInsert()) { // Changed values are "insert" back-referenced to Contact mAfter.remove(mIdColumn); builder = ContentProviderOperation.newInsert(targetUri); builder.withValues(mAfter); } else if (isDelete()) { // When marked for deletion and "before" exists, then "delete" builder = ContentProviderOperation.newDelete(targetUri); builder.withSelection(mIdColumn + "=" + getId(), null); } else if (isUpdate()) { // When has changes and "before" exists, then "update" builder = ContentProviderOperation.newUpdate(targetUri); builder.withSelection(mIdColumn + "=" + getId(), null); builder.withValues(mAfter); } return builder; } /** {@inheritDoc} */ public int describeContents() { // Nothing special about this parcel return 0; } /** {@inheritDoc} */ public void writeToParcel(Parcel dest, int flags) { dest.writeParcelable(mBefore, flags); dest.writeParcelable(mAfter, flags); dest.writeString(mIdColumn); } public void readFromParcel(Parcel source) { final ClassLoader loader = getClass().getClassLoader(); mBefore = source. readParcelable(loader); mAfter = source. readParcelable(loader); mIdColumn = source.readString(); } public static final Creator CREATOR = new Creator() { public ValuesDelta createFromParcel(Parcel in) { final ValuesDelta values = new ValuesDelta(); values.readFromParcel(in); return values; } public ValuesDelta[] newArray(int size) { return new ValuesDelta[size]; } }; public void setGroupRowId(long groupId) { put(ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID, groupId); } public Long getGroupRowId() { return getAsLong(ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID); } public void setPhoto(byte[] value) { put(ContactsContract.CommonDataKinds.Photo.PHOTO, value); } public byte[] getPhoto() { return getAsByteArray(ContactsContract.CommonDataKinds.Photo.PHOTO); } public void setSuperPrimary(boolean val) { if (val) { put(ContactsContract.Data.IS_SUPER_PRIMARY, 1); } else { put(ContactsContract.Data.IS_SUPER_PRIMARY, 0); } } public void setPhoneticFamilyName(String value) { put(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_FAMILY_NAME, value); } public void setPhoneticMiddleName(String value) { put(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_MIDDLE_NAME, value); } public void setPhoneticGivenName(String value) { put(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_GIVEN_NAME, value); } public String getPhoneticFamilyName() { return getAsString(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_FAMILY_NAME); } public String getPhoneticMiddleName() { return getAsString(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_MIDDLE_NAME); } public String getPhoneticGivenName() { return getAsString(ContactsContract.CommonDataKinds.StructuredName.PHONETIC_GIVEN_NAME); } public String getDisplayName() { return getAsString(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME); } public void setDisplayName(String name) { if (name == null) { putNull(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME); } else { put(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name); } } public void copyStructuredNameFieldsFrom(ValuesDelta name) { copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME); copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME); copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME); copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.PREFIX); copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME); copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.SUFFIX); copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.PHONETIC_GIVEN_NAME); copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.PHONETIC_MIDDLE_NAME); copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.PHONETIC_FAMILY_NAME); copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.FULL_NAME_STYLE); copyStringFrom(name, ContactsContract.CommonDataKinds.StructuredName.PHONETIC_NAME_STYLE); } public String getPhoneNumber() { return getAsString(ContactsContract.CommonDataKinds.Phone.NUMBER); } public String getPhoneNormalizedNumber() { return getAsString(ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER); } public boolean phoneHasType() { return containsKey(ContactsContract.CommonDataKinds.Phone.TYPE); } public int getPhoneType() { return getAsInteger(ContactsContract.CommonDataKinds.Phone.TYPE); } public String getPhoneLabel() { return getAsString(ContactsContract.CommonDataKinds.Phone.LABEL); } public String getEmailData() { return getAsString(ContactsContract.CommonDataKinds.Email.DATA); } public boolean emailHasType() { return containsKey(ContactsContract.CommonDataKinds.Email.TYPE); } public int getEmailType() { return getAsInteger(ContactsContract.CommonDataKinds.Email.TYPE); } public String getEmailLabel() { return getAsString(ContactsContract.CommonDataKinds.Email.LABEL); } }