Merge change 25180 into eclair
* changes:
Prevent unstateful edits, empty trimming, INSERT edge cases.
diff --git a/src/com/android/contacts/model/ContactsSource.java b/src/com/android/contacts/model/ContactsSource.java
index e623084..1e797d4 100644
--- a/src/com/android/contacts/model/ContactsSource.java
+++ b/src/com/android/contacts/model/ContactsSource.java
@@ -115,6 +115,11 @@
return mInflatedLevel >= inflateLevel;
}
+ /** @hide exposed for unit tests */
+ public void setInflatedLevel(int inflateLevel) {
+ mInflatedLevel = inflateLevel;
+ }
+
/**
* Ensure that the constraint rules behind this {@link ContactsSource} have
* been inflated. Because this may involve parsing meta-data from
diff --git a/src/com/android/contacts/model/EntityDelta.java b/src/com/android/contacts/model/EntityDelta.java
index f221247..f51ea34 100644
--- a/src/com/android/contacts/model/EntityDelta.java
+++ b/src/com/android/contacts/model/EntityDelta.java
@@ -16,6 +16,8 @@
package com.android.contacts.model;
+import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.model.ContactsSource.EditField;
import com.google.android.collect.Lists;
import com.google.android.collect.Maps;
import com.google.android.collect.Sets;
@@ -31,6 +33,8 @@
import android.provider.BaseColumns;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+import android.util.Log;
import android.view.View;
import java.util.ArrayList;
@@ -294,6 +298,7 @@
// Assert version is consistent while persisting changes
final Long beforeId = mValues.getId();
final Long beforeVersion = mValues.getAsLong(RawContacts.VERSION);
+ if (beforeId == null || beforeVersion == null) return;
final ContentProviderOperation.Builder builder = ContentProviderOperation
.newAssertQuery(RawContacts.CONTENT_URI);
@@ -525,7 +530,7 @@
public boolean isUpdate() {
// When "after" has some changes, action is "update"
- return beforeExists() && (mAfter.size() > 0);
+ return beforeExists() && (mAfter != null && mAfter.size() > 0);
}
public boolean isInsert() {
diff --git a/src/com/android/contacts/model/EntityModifier.java b/src/com/android/contacts/model/EntityModifier.java
index 61c0c75..b941b38 100644
--- a/src/com/android/contacts/model/EntityModifier.java
+++ b/src/com/android/contacts/model/EntityModifier.java
@@ -28,6 +28,8 @@
import android.os.Bundle;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Intents;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Im;
import android.provider.ContactsContract.CommonDataKinds.Phone;
@@ -339,12 +341,29 @@
}
/**
+ * Processing to trim any empty {@link ValuesDelta} and {@link EntityDelta}
+ * from the given {@link EntitySet}, assuming the given {@link Sources}
+ * dictates the structure for various fields. This method ignores rows not
+ * described by the {@link ContactsSource}.
+ */
+ public static void trimEmpty(EntitySet set, Sources sources) {
+ for (EntityDelta state : set) {
+ final String accountType = state.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+ final ContactsSource source = sources.getInflatedSource(accountType,
+ ContactsSource.LEVEL_MIMETYPES);
+ trimEmpty(state, source);
+ }
+ }
+
+ /**
* Processing to trim any empty {@link ValuesDelta} rows from the given
* {@link EntityDelta}, assuming the given {@link ContactsSource} dictates
* the structure for various fields. This method ignores rows not described
* by the {@link ContactsSource}.
*/
- public static void trimEmpty(ContactsSource source, EntityDelta state) {
+ public static void trimEmpty(EntityDelta state, ContactsSource source) {
+ boolean hasValues = false;
+
// Walk through entries for each well-known kind
for (DataKind kind : source.getSortedDataKinds()) {
final String mimeType = kind.mimeType;
@@ -352,15 +371,28 @@
if (entries == null) continue;
for (ValuesDelta entry : entries) {
- // Test and remove this row if empty
+ // Skip any values that haven't been touched
final boolean touched = entry.isInsert() || entry.isUpdate();
- if (touched && EntityModifier.isEmpty(entry, kind)) {
+ if (!touched) {
+ hasValues = true;
+ continue;
+ }
+
+ // Test and remove this row if empty
+ if (EntityModifier.isEmpty(entry, kind)) {
// TODO: remove this verbose logging
Log.w(TAG, "Trimming: " + entry.toString());
entry.markDeleted();
+ } else {
+ hasValues = true;
}
}
}
+
+ if (!hasValues) {
+ // Trim overall entity if no children exist
+ state.markDeleted();
+ }
}
/**
@@ -454,7 +486,10 @@
*/
public static void parseExtras(EntityDelta state, DataKind kind, Bundle extras,
String typeExtra, String valueExtra, String valueColumn) {
- final String value = extras.getString(valueExtra);
+ final CharSequence value = extras.getCharSequence(valueExtra);
+
+ // Bail early if source doesn't handle this type
+ if (kind == null) return;
// Bail when can't insert type, or value missing
final boolean canInsert = EntityModifier.canInsert(state, kind);
@@ -462,16 +497,17 @@
if (!validValue || !canInsert) return;
// Find exact type, or otherwise best type
- final int typeValue = extras.getInt(typeExtra, Integer.MIN_VALUE);
+ final int typeValue = extras.getInt(typeExtra, BaseTypes.TYPE_CUSTOM);
final EditType editType = EntityModifier.getBestValidType(state, kind, true, typeValue);
// Create data row and fill with value
final ValuesDelta child = EntityModifier.insertChild(state, kind, editType);
- child.put(valueColumn, value);
+ child.put(valueColumn, value.toString());
if (editType != null && editType.customColumn != null) {
// Write down label when custom type picked
- child.put(editType.customColumn, extras.getString(typeExtra));
+ final CharSequence customType = extras.getCharSequence(typeExtra);
+ child.put(editType.customColumn, customType.toString());
}
}
}
diff --git a/src/com/android/contacts/model/Sources.java b/src/com/android/contacts/model/Sources.java
index c0f49c6..71607c7 100644
--- a/src/com/android/contacts/model/Sources.java
+++ b/src/com/android/contacts/model/Sources.java
@@ -66,14 +66,20 @@
}
/**
- * Internal constructor that only performs initial parsing. Obtain a
- * {@link android.provider.ContactsContract.RawContacts#ACCOUNT_TYPE}.
+ * Internal constructor that only performs initial parsing.
*/
private Sources(Context context) {
mContext = context;
loadAccounts();
}
+ /** @hide exposed for unit tests */
+ public Sources(ContactsSource... sources) {
+ for (ContactsSource source : sources) {
+ mSources.put(source.accountType, source);
+ }
+ }
+
/**
* Blocking call to load all {@link AuthenticatorDescription} known by the
* {@link AccountManager} on the system.
diff --git a/src/com/android/contacts/ui/EditContactActivity.java b/src/com/android/contacts/ui/EditContactActivity.java
index baf3fb0..2354a7f 100644
--- a/src/com/android/contacts/ui/EditContactActivity.java
+++ b/src/com/android/contacts/ui/EditContactActivity.java
@@ -62,7 +62,6 @@
import android.provider.ContactsContract.Contacts.Data;
import android.util.Log;
import android.view.ContextThemeWrapper;
-import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -90,19 +89,9 @@
/** The launch code when picking a photo and the raw data is returned */
private static final int PHOTO_PICKED_WITH_DATA = 3021;
- private static final int TOKEN_ENTITY = 41;
-
private static final String KEY_EDIT_STATE = "state";
private static final String KEY_SELECTED_RAW_CONTACT = "selected";
-// private static final String KEY_SELECTED_TAB_ID = "tabId";
-// private static final String KEY_CONTACT_ID = "contactId";
-
-// private int mSelectedTab = -1;
-
-// private long mSelectedRawContactId = -1;
-// private long mContactId = -1;
-
private String mQuerySelection;
private ScrollingTabWidget mTabWidget;
@@ -188,6 +177,20 @@
target.mQuerySelection = selection;
target.mState = EntitySet.fromQuery(resolver, selection, null, null);
+
+ // Handle any incoming values that should be inserted
+ final Bundle extras = intent.getExtras();
+ final boolean hasExtras = extras.size() > 0;
+ final boolean hasState = target.mState.size() > 0;
+ if (hasExtras && hasState) {
+ // Find source defining the first RawContact found
+ final EntityDelta state = target.mState.get(0);
+ final String accountType = state.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+ final ContactsSource source = sources.getInflatedSource(accountType,
+ ContactsSource.LEVEL_CONSTRAINTS);
+ EntityModifier.parseExtras(context, source, state, extras);
+ }
+
return null;
}
@@ -203,11 +206,11 @@
@Override
protected void onSaveInstanceState(Bundle outState) {
- // Store entities with modifications
- outState.putParcelable(KEY_EDIT_STATE, mState);
- outState.putLong(KEY_SELECTED_RAW_CONTACT, getSelectedRawContactId());
-// outState.putLong(KEY_SELECTED_TAB_ID, mSelectedRawContactId);
-// outState.putLong(KEY_CONTACT_ID, mContactId);
+ if (hasValidState()) {
+ // Store entities with modifications
+ outState.putParcelable(KEY_EDIT_STATE, mState);
+ outState.putLong(KEY_SELECTED_RAW_CONTACT, getSelectedRawContactId());
+ }
super.onSaveInstanceState(outState);
}
@@ -217,16 +220,13 @@
// Read modifications from instance
mState = savedInstanceState.<EntitySet> getParcelable(KEY_EDIT_STATE);
-// mSelectedRawContactId = savedInstanceState.getLong(KEY_SELECTED_TAB_ID);
-// mContactId = savedInstanceState.getLong(KEY_CONTACT_ID);
-
- Log.d(TAG, "onrestoreinstancestate");
-
bindTabs();
bindHeader();
- final long selectedId = savedInstanceState.getLong(KEY_SELECTED_RAW_CONTACT);
- setSelectedRawContactId(selectedId);
+ if (hasValidState()) {
+ final long selectedId = savedInstanceState.getLong(KEY_SELECTED_RAW_CONTACT);
+ setSelectedRawContactId(selectedId);
+ }
// Restore selected tab and any focus
super.onRestoreInstanceState(savedInstanceState);
@@ -248,6 +248,14 @@
mTabWidget.setCurrentTab(index);
}
+ /**
+ * Check if our internal {@link #mState} is valid, usually checked before
+ * performing user actions.
+ */
+ protected boolean hasValidState() {
+ return mState != null && mState.size() > 0;
+ }
+
/**
@@ -256,6 +264,8 @@
* {@link RawContacts}.
*/
protected void bindTabs() {
+ if (!hasValidState()) return;
+
final Sources sources = Sources.getInstance(this);
int selectedTab = 0;
@@ -287,6 +297,8 @@
* primary {@link Data} change.
*/
protected void bindHeader() {
+ if (!hasValidState()) return;
+
// TODO: rebuild header widget based on internal entities
// TODO: fill header bar with newly parsed data for speed
@@ -304,8 +316,8 @@
/** {@inheritDoc} */
public void onTabSelectionChanged(int tabIndex, boolean clicked) {
- boolean validTab = mState != null && tabIndex >= 0 && tabIndex < mState.size();
- if (!validTab) return;
+ if (!hasValidState()) return;
+ if (tabIndex < 0 || tabIndex >= mState.size()) return;
// Find entity and source for selected tab
final EntityDelta entity = mState.get(tabIndex);
@@ -321,11 +333,13 @@
/** {@inheritDoc} */
public void onDisplayNameLongClick(View view) {
+ if (!hasValidState()) return;
this.createNameDialog().show();
}
/** {@inheritDoc} */
public void onPhotoLongClick(View view) {
+ if (!hasValidState()) return;
this.createPhotoDialog().show();
}
@@ -347,12 +361,8 @@
/** {@inheritDoc} */
@Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_BACK:
- return doSaveAction();
- }
- return super.onKeyDown(keyCode, event);
+ public void onBackPressed() {
+ doSaveAction();
}
/** {@inheritDoc} */
@@ -456,11 +466,18 @@
/** {@inheritDoc} */
@Override
protected Integer doInBackground(EditContactActivity target, EntitySet... params) {
- final ContentResolver resolver = target.getContentResolver();
+ final Context context = target;
+ final ContentResolver resolver = context.getContentResolver();
+ EntitySet state = params[0];
+
+ // Trim any empty fields, and RawContacts, before persisting
+ final Sources sources = Sources.getInstance(context);
+ EntityModifier.trimEmpty(state, sources);
+
+ // Attempt to persist changes
int tries = 0;
Integer result = RESULT_FAILURE;
- EntitySet state = params[0];
while (tries < PERSIST_TRIES) {
try {
// Build operations and try applying
@@ -515,8 +532,7 @@
* finishes the activity.
*/
private boolean doSaveAction() {
- // Bail early if nothing to save
- if (mState == null || mState.size() == 0) return true;
+ if (!hasValidState()) return false;
// Pass back last-selected contact
final int selectedTab = mTabWidget.getCurrentTab();
@@ -558,6 +574,7 @@
* {@link EntityDelta} under the currently edited {@link Contacts}.
*/
private boolean doAddAction() {
+ // Adding is okay when missing state
new AddContactTask(this).execute();
return true;
}
@@ -567,6 +584,8 @@
* user confirmation before continuing.
*/
private boolean doDeleteAction() {
+ if (!hasValidState()) return false;
+
this.createDeleteDialog().show();
return true;
}
@@ -575,6 +594,8 @@
* Pick a specific photo to be added under the currently selected tab.
*/
private boolean doPickPhotoAction() {
+ if (!hasValidState()) return false;
+
try {
// Launch picker to choose photo for selected contact
final Intent intent = ContactsUtils.getPhotoPickIntent();
@@ -589,6 +610,8 @@
* Clear any existing photo under the currently selected tab.
*/
public boolean doRemovePhotoAction() {
+ if (!hasValidState()) return false;
+
// Remove photo from selected contact
mEditor.setPhotoBitmap(null);
return true;
@@ -601,6 +624,8 @@
/** {@inheritDoc} */
public void onRequest(int request) {
+ if (!hasValidState()) return;
+
switch (request) {
case EditorListener.REQUEST_PICK_PHOTO: {
doPickPhotoAction();
@@ -704,7 +729,7 @@
final DialogInterface.OnCancelListener cancelListener = new DialogInterface.OnCancelListener() {
public void onCancel(DialogInterface dialog) {
// If nothing remains, close activity
- if (target.mState == null || target.mState.size() == 0) {
+ if (!target.hasValidState()) {
target.finish();
}
}
@@ -739,6 +764,8 @@
delta.markDeleted();
// TODO: trigger task to update tabs (doesnt need to be background)
+ bindTabs();
+ bindHeader();
}
});
builder.setNegativeButton(android.R.string.cancel, null);
diff --git a/tests/src/com/android/contacts/EntityModifierTests.java b/tests/src/com/android/contacts/EntityModifierTests.java
index 9d0c1be..dd40634 100644
--- a/tests/src/com/android/contacts/EntityModifierTests.java
+++ b/tests/src/com/android/contacts/EntityModifierTests.java
@@ -23,6 +23,8 @@
import com.android.contacts.model.ContactsSource;
import com.android.contacts.model.EntityDelta;
import com.android.contacts.model.EntityModifier;
+import com.android.contacts.model.EntitySet;
+import com.android.contacts.model.Sources;
import com.android.contacts.model.ContactsSource.DataKind;
import com.android.contacts.model.ContactsSource.EditField;
import com.android.contacts.model.ContactsSource.EditType;
@@ -40,7 +42,6 @@
import android.util.Log;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
/**
@@ -54,6 +55,9 @@
private static final long TEST_ID = 4;
private static final String TEST_PHONE = "218-555-1212";
+ private static final String TEST_ACCOUNT_NAME = "unittest@example.com";
+ private static final String TEST_ACCOUNT_TYPE = "com.example.unittest";
+
public EntityModifierTests() {
super();
}
@@ -69,6 +73,8 @@
*/
protected ContactsSource getSource() {
final ContactsSource list = new ContactsSource();
+ list.accountType = TEST_ACCOUNT_TYPE;
+ list.setInflatedLevel(ContactsSource.LEVEL_CONSTRAINTS);
{
// Phone allows maximum 2 home, 1 work, and unlimited other, with
@@ -94,11 +100,23 @@
}
/**
+ * Build {@link Sources} instance.
+ */
+ protected Sources getSources(ContactsSource... sources) {
+ return new Sources(sources);
+ }
+
+ /**
* Build an {@link Entity} with the requested set of phone numbers.
*/
- protected EntityDelta getEntity(ContentValues... entries) {
+ protected EntityDelta getEntity(Long existingId, ContentValues... entries) {
final ContentValues contact = new ContentValues();
- contact.put(RawContacts._ID, TEST_ID);
+ if (existingId != null) {
+ contact.put(RawContacts._ID, existingId);
+ }
+ contact.put(RawContacts.ACCOUNT_NAME, TEST_ACCOUNT_NAME);
+ contact.put(RawContacts.ACCOUNT_TYPE, TEST_ACCOUNT_TYPE);
+
final Entity before = new Entity(contact);
for (ContentValues values : entries) {
before.addSubValue(Data.CONTENT_URI, values);
@@ -135,7 +153,7 @@
List<EditType> validTypes;
// Add first home, first work
- final EntityDelta state = getEntity();
+ final EntityDelta state = getEntity(TEST_ID);
EntityModifier.insertChild(state, kindPhone, typeHome);
EntityModifier.insertChild(state, kindPhone, typeWork);
@@ -178,7 +196,7 @@
final EditType typeOther = EntityModifier.getType(kindPhone, Phone.TYPE_OTHER);
// Add first home, first work
- final EntityDelta state = getEntity();
+ final EntityDelta state = getEntity(TEST_ID);
EntityModifier.insertChild(state, kindPhone, typeHome);
EntityModifier.insertChild(state, kindPhone, typeWork);
assertTrue("Unable to insert", EntityModifier.canInsert(state, kindPhone));
@@ -210,7 +228,7 @@
EditType suggested;
// Default suggestion should be home
- final EntityDelta state = getEntity();
+ final EntityDelta state = getEntity(TEST_ID);
suggested = EntityModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
assertEquals("Unexpected suggestion", typeHome, suggested);
@@ -252,7 +270,7 @@
final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
// Test row that has type values, but core fields are empty
- final EntityDelta state = getEntity();
+ final EntityDelta state = getEntity(TEST_ID);
final ValuesDelta values = EntityModifier.insertChild(state, kindPhone, typeHome);
assertTrue("Expected empty", EntityModifier.isEmpty(values, kindPhone));
@@ -269,7 +287,7 @@
final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
// Test row that has type values, but core fields are empty
- final EntityDelta state = getEntity();
+ final EntityDelta state = getEntity(TEST_ID);
final ValuesDelta values = EntityModifier.insertChild(state, kindPhone, typeHome);
// Build diff, expecting insert for data row and update enforcement
@@ -292,11 +310,16 @@
assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
}
- // Trim empty rows and try again, expecting no changes
- EntityModifier.trimEmpty(source, state);
+ // Trim empty rows and try again, expecting delete of overall contact
+ EntityModifier.trimEmpty(state, source);
diff.clear();
state.buildDiff(diff);
- assertEquals("Unexpected operations", 0, diff.size());
+ assertEquals("Unexpected operations", 1, diff.size());
+ {
+ final ContentProviderOperation oper = diff.get(0);
+ assertEquals("Incorrect type", TYPE_DELETE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
}
public void testTrimEmptyUntouched() {
@@ -305,7 +328,7 @@
final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
// Build "before" that has empty row
- final EntityDelta state = getEntity();
+ final EntityDelta state = getEntity(TEST_ID);
final ContentValues before = new ContentValues();
before.put(Data._ID, TEST_ID);
before.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
@@ -317,7 +340,7 @@
assertEquals("Unexpected operations", 0, diff.size());
// Try trimming existing empty, which we shouldn't touch
- EntityModifier.trimEmpty(source, state);
+ EntityModifier.trimEmpty(state, source);
diff.clear();
state.buildDiff(diff);
assertEquals("Unexpected operations", 0, diff.size());
@@ -334,7 +357,7 @@
before.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
before.put(kindPhone.typeColumn, typeHome.rawValue);
before.put(Phone.NUMBER, TEST_PHONE);
- final EntityDelta state = getEntity(before);
+ final EntityDelta state = getEntity(TEST_ID, before);
// Build diff, expecting no changes
final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
@@ -364,7 +387,128 @@
}
// Now run trim, which should turn that update into delete
- EntityModifier.trimEmpty(source, state);
+ EntityModifier.trimEmpty(state, source);
+ diff.clear();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 1, diff.size());
+ {
+ final ContentProviderOperation oper = diff.get(0);
+ assertEquals("Incorrect type", TYPE_DELETE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+ }
+
+ public void testTrimInsertEmpty() {
+ final ContactsSource source = getSource();
+ final Sources sources = getSources(source);
+ final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+ final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+ // Try creating a contact without any child entries
+ final EntityDelta state = getEntity(null);
+ final EntitySet set = EntitySet.fromSingle(state);
+
+ // Build diff, expecting single insert
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 1, diff.size());
+ {
+ final ContentProviderOperation oper = diff.get(0);
+ assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+
+ // Trim empty rows and try again, expecting no insert
+ EntityModifier.trimEmpty(set, sources);
+ diff.clear();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 0, diff.size());
+ }
+
+ public void testTrimInsertInsert() {
+ final ContactsSource source = getSource();
+ final Sources sources = getSources(source);
+ final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+ final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+ // Try creating a contact with single empty entry
+ final EntityDelta state = getEntity(null);
+ final ValuesDelta values = EntityModifier.insertChild(state, kindPhone, typeHome);
+ final EntitySet set = EntitySet.fromSingle(state);
+
+ // Build diff, expecting two insert operations
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 2, diff.size());
+ {
+ final ContentProviderOperation oper = diff.get(0);
+ assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(1);
+ assertEquals("Incorrect type", TYPE_INSERT, oper.getType());
+ assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+ }
+
+ // Trim empty rows and try again, expecting silence
+ EntityModifier.trimEmpty(set, sources);
+ diff.clear();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 0, diff.size());
+ }
+
+ public void testTrimUpdateRemain() {
+ final ContactsSource source = getSource();
+ final Sources sources = getSources(source);
+ final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+ final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+ // Build "before" with two phone numbers
+ final ContentValues first = new ContentValues();
+ first.put(Data._ID, TEST_ID);
+ first.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ first.put(kindPhone.typeColumn, typeHome.rawValue);
+ first.put(Phone.NUMBER, TEST_PHONE);
+
+ final ContentValues second = new ContentValues();
+ second.put(Data._ID, TEST_ID);
+ second.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ second.put(kindPhone.typeColumn, typeHome.rawValue);
+ second.put(Phone.NUMBER, TEST_PHONE);
+
+ final EntityDelta state = getEntity(TEST_ID, first, second);
+ final EntitySet set = EntitySet.fromSingle(state);
+
+ // Build diff, expecting no changes
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 0, diff.size());
+
+ // Now update row by changing number to empty string, expecting single update
+ final ValuesDelta child = state.getEntry(TEST_ID);
+ child.put(Phone.NUMBER, "");
+ diff.clear();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 3, diff.size());
+ {
+ final ContentProviderOperation oper = diff.get(0);
+ assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(1);
+ assertEquals("Incorrect type", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(2);
+ assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+
+ // Now run trim, which should turn that update into delete
+ EntityModifier.trimEmpty(set, sources);
diff.clear();
state.buildDiff(diff);
assertEquals("Unexpected operations", 3, diff.size());
@@ -384,4 +528,59 @@
assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
}
}
+
+ public void testTrimUpdateUpdate() {
+ final ContactsSource source = getSource();
+ final Sources sources = getSources(source);
+ final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+ final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+ // Build "before" with two phone numbers
+ final ContentValues first = new ContentValues();
+ first.put(Data._ID, TEST_ID);
+ first.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ first.put(kindPhone.typeColumn, typeHome.rawValue);
+ first.put(Phone.NUMBER, TEST_PHONE);
+
+ final EntityDelta state = getEntity(TEST_ID, first);
+ final EntitySet set = EntitySet.fromSingle(state);
+
+ // Build diff, expecting no changes
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 0, diff.size());
+
+ // Now update row by changing number to empty string, expecting single update
+ final ValuesDelta child = state.getEntry(TEST_ID);
+ child.put(Phone.NUMBER, "");
+ diff.clear();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 3, diff.size());
+ {
+ final ContentProviderOperation oper = diff.get(0);
+ assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(1);
+ assertEquals("Incorrect type", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", Data.CONTENT_URI, oper.getUri());
+ }
+ {
+ final ContentProviderOperation oper = diff.get(2);
+ assertEquals("Expected aggregation mode change", TYPE_UPDATE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+
+ // Now run trim, which should turn into deleting the whole contact
+ EntityModifier.trimEmpty(set, sources);
+ diff.clear();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 1, diff.size());
+ {
+ final ContentProviderOperation oper = diff.get(0);
+ assertEquals("Incorrect type", TYPE_DELETE, oper.getType());
+ assertEquals("Incorrect target", RawContacts.CONTENT_URI, oper.getUri());
+ }
+ }
}