Insert, update, delete photos. Trim empty fields.
Allow photo changes when the ContactsSource allows Photo
entries. This change also trims out any inserted or updated
fields that are now "empty" according to their respective
DataKind fields. Wrote unit tests for field trimming to
ensure wiping works. Fixes http://b/2050549
diff --git a/res/layout/item_photo_editor.xml b/res/layout/item_photo_editor.xml
index b2a5f33..7544439 100644
--- a/res/layout/item_photo_editor.xml
+++ b/res/layout/item_photo_editor.xml
@@ -21,6 +21,7 @@
android:clickable="true"
android:focusable="true"
android:src="@drawable/ic_menu_add_picture"
+ android:cropToPadding="true"
android:scaleType="center"
android:background="@drawable/btn_contact_picture"
android:gravity="center" />
diff --git a/src/com/android/contacts/model/Editor.java b/src/com/android/contacts/model/Editor.java
index 64c0952..d6f7003 100644
--- a/src/com/android/contacts/model/Editor.java
+++ b/src/com/android/contacts/model/Editor.java
@@ -34,6 +34,14 @@
* Called when the given {@link Editor} has been deleted.
*/
public void onDeleted(Editor editor);
+
+ /**
+ * Called when the given {@link Editor} has a request, for example it
+ * wants to select a photo.
+ */
+ public void onRequest(int request);
+
+ public static final int REQUEST_PICK_PHOTO = 1;
}
/**
diff --git a/src/com/android/contacts/model/EntityDelta.java b/src/com/android/contacts/model/EntityDelta.java
index f096cc7..f221247 100644
--- a/src/com/android/contacts/model/EntityDelta.java
+++ b/src/com/android/contacts/model/EntityDelta.java
@@ -551,6 +551,11 @@
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);
diff --git a/src/com/android/contacts/model/EntityModifier.java b/src/com/android/contacts/model/EntityModifier.java
index e904a8b..a0a54ef 100644
--- a/src/com/android/contacts/model/EntityModifier.java
+++ b/src/com/android/contacts/model/EntityModifier.java
@@ -17,6 +17,7 @@
package com.android.contacts.model;
import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.model.ContactsSource.EditField;
import com.android.contacts.model.ContactsSource.EditType;
import com.android.contacts.model.EntityDelta.ValuesDelta;
import com.google.android.collect.Lists;
@@ -34,6 +35,7 @@
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.Intents.Insert;
import android.text.TextUtils;
+import android.util.Log;
import android.util.SparseIntArray;
import java.util.ArrayList;
@@ -45,6 +47,8 @@
* new rows, or enforcing {@link ContactsSource}.
*/
public class EntityModifier {
+ private static final String TAG = "EntityModifier";
+
/**
* For the given {@link EntityDelta}, determine if the given
* {@link DataKind} could be inserted under specific
@@ -335,6 +339,48 @@
}
/**
+ * 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) {
+ // Walk through entries for each well-known kind
+ for (DataKind kind : source.getSortedDataKinds()) {
+ final String mimeType = kind.mimeType;
+ final ArrayList<ValuesDelta> entries = state.getMimeEntries(mimeType);
+ if (entries == null) continue;
+
+ for (ValuesDelta entry : entries) {
+ // Test and remove this row if empty
+ final boolean touched = entry.isInsert() || entry.isUpdate();
+ if (touched && EntityModifier.isEmpty(entry, kind)) {
+ // TODO: remove this verbose logging
+ Log.w(TAG, "Trimming: " + entry.toString());
+ entry.markDeleted();
+ }
+ }
+ }
+ }
+
+ /**
+ * Test if the given {@link ValuesDelta} would be considered "empty" in
+ * terms of {@link DataKind#fieldList}.
+ */
+ public static boolean isEmpty(ValuesDelta values, DataKind kind) {
+ boolean hasValues = false;
+ for (EditField field : kind.fieldList) {
+ // If any field has values, we're not empty
+ final String value = values.getAsString(field.column);
+ if (!TextUtils.isEmpty(value)) {
+ hasValues = true;
+ }
+ }
+
+ return !hasValues;
+ }
+
+ /**
* Parse the given {@link Bundle} into the given {@link EntityDelta} state,
* assuming the extras defined through {@link Intents}.
*/
diff --git a/src/com/android/contacts/model/EntitySet.java b/src/com/android/contacts/model/EntitySet.java
index f9425b9..b9a71e7 100644
--- a/src/com/android/contacts/model/EntitySet.java
+++ b/src/com/android/contacts/model/EntitySet.java
@@ -159,7 +159,7 @@
* existing {@link RawContacts#_ID} value. Usually used when creating
* {@link AggregationExceptions} during an update.
*/
- public long findRawContactId() {
+ protected long findRawContactId() {
for (EntityDelta delta : this) {
final Long rawContactId = delta.getValues().getAsLong(RawContacts._ID);
if (rawContactId != null && rawContactId >= 0) {
@@ -177,10 +177,24 @@
final EntityDelta delta = this.get(index);
return delta.getValues().getAsLong(RawContacts._ID);
} else {
- return -1;
+ return 0;
}
}
+ /**
+ * Find index of given {@link RawContacts#_ID} when present.
+ */
+ public int indexOfRawContactId(long rawContactId) {
+ final int size = this.size();
+ for (int i = 0; i < size; i++) {
+ final long currentId = getRawContactId(i);
+ if (currentId == rawContactId) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
/** {@inheritDoc} */
public int describeContents() {
// Nothing special about this parcel
diff --git a/src/com/android/contacts/model/HardCodedSources.java b/src/com/android/contacts/model/HardCodedSources.java
index 9f91a74..03dd226 100644
--- a/src/com/android/contacts/model/HardCodedSources.java
+++ b/src/com/android/contacts/model/HardCodedSources.java
@@ -161,6 +161,10 @@
{
// GOOGLE: PHOTO
DataKind kind = new DataKind(Photo.CONTENT_ITEM_TYPE, -1, -1, -1, true);
+
+ kind.fieldList = Lists.newArrayList();
+ kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));
+
list.add(kind);
}
@@ -445,6 +449,10 @@
// EXCHANGE: PHOTO
DataKind kind = new DataKind(Photo.CONTENT_ITEM_TYPE, -1, -1, -1, true);
kind.typeOverallMax = 1;
+
+ kind.fieldList = Lists.newArrayList();
+ kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));
+
list.add(kind);
}
diff --git a/src/com/android/contacts/ui/EditContactActivity.java b/src/com/android/contacts/ui/EditContactActivity.java
index 7e73568..eda1729 100644
--- a/src/com/android/contacts/ui/EditContactActivity.java
+++ b/src/com/android/contacts/ui/EditContactActivity.java
@@ -21,11 +21,13 @@
import com.android.contacts.ScrollingTabWidget;
import com.android.contacts.ViewContactActivity;
import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.Editor;
import com.android.contacts.model.EntityDelta;
import com.android.contacts.model.EntityModifier;
import com.android.contacts.model.EntitySet;
import com.android.contacts.model.HardCodedSources;
import com.android.contacts.model.Sources;
+import com.android.contacts.model.Editor.EditorListener;
import com.android.contacts.model.EntityDelta.ValuesDelta;
import com.android.contacts.ui.widget.ContactEditorView;
import com.android.contacts.util.EmptyService;
@@ -47,6 +49,7 @@
import android.content.Entity;
import android.content.Intent;
import android.content.OperationApplicationException;
+import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;
@@ -80,7 +83,8 @@
* Activity for editing or inserting a contact.
*/
public final class EditContactActivity extends Activity implements View.OnClickListener,
- ScrollingTabWidget.OnTabSelectionChangedListener, ContactHeaderWidget.ContactHeaderListener {
+ ScrollingTabWidget.OnTabSelectionChangedListener,
+ ContactHeaderWidget.ContactHeaderListener, EditorListener {
private static final String TAG = "EditContactActivity";
/** The launch code when picking a photo and the raw data is returned */
@@ -89,7 +93,8 @@
private static final int TOKEN_ENTITY = 41;
private static final String KEY_EDIT_STATE = "state";
- private static final String KEY_SELECTED_TAB = "tab";
+ 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";
@@ -128,7 +133,9 @@
mTabWidget = (ScrollingTabWidget)this.findViewById(R.id.tab_widget);
mTabWidget.setTabSelectionListener(this);
+ // Build editor and listen for photo requests
mEditor = (ContactEditorView)this.findViewById(android.R.id.tabcontent);
+ mEditor.getPhotoEditor().setEditorListener(this);
findViewById(R.id.btn_done).setOnClickListener(this);
findViewById(R.id.btn_discard).setOnClickListener(this);
@@ -140,7 +147,6 @@
} else if (Intent.ACTION_INSERT.equals(action) && icicle == null) {
// Trigger dialog to pick account type
doAddAction();
-
}
}
@@ -195,28 +201,11 @@
-// /**
-// * Instance state for {@link #mEditor} from a previous instance.
-// */
-// private SparseArray<Parcelable> mEditorState;
-//
-// /**
-// * Save state of the currently selected {@link #mEditor}, usually for
-// * passing across instance boundaries to restore later.
-// */
-// private SparseArray<Parcelable> buildEditorState() {
-// final SparseArray<Parcelable> state = new SparseArray<Parcelable>();
-// if (mEditor != null) {
-// mEditor.getView().saveHierarchyState(state);
-// }
-// return state;
-// }
-//
@Override
protected void onSaveInstanceState(Bundle outState) {
// Store entities with modifications
outState.putParcelable(KEY_EDIT_STATE, mState);
- outState.putInt(KEY_SELECTED_TAB, mTabWidget.getCurrentTab());
+ outState.putLong(KEY_SELECTED_RAW_CONTACT, getSelectedRawContactId());
// outState.putLong(KEY_SELECTED_TAB_ID, mSelectedRawContactId);
// outState.putLong(KEY_CONTACT_ID, mContactId);
@@ -236,13 +225,30 @@
bindTabs();
bindHeader();
- final int selectedTab = savedInstanceState.getInt(KEY_SELECTED_TAB);
- mTabWidget.setCurrentTab(selectedTab);
+ final long selectedId = savedInstanceState.getLong(KEY_SELECTED_RAW_CONTACT);
+ setSelectedRawContactId(selectedId);
// Restore selected tab and any focus
super.onRestoreInstanceState(savedInstanceState);
}
+ /**
+ * Return the {@link RawContacts#_ID} of the currently selected tab.
+ */
+ protected long getSelectedRawContactId() {
+ final int index = mTabWidget.getCurrentTab();
+ return mState.getRawContactId(index);
+ }
+
+ /**
+ * Set the selected tab based on the given {@link RawContacts#_ID}.
+ */
+ protected void setSelectedRawContactId(long rawContactId) {
+ final int index = mState.indexOfRawContactId(rawContactId);
+ mTabWidget.setCurrentTab(index);
+ }
+
+
/**
* Rebuild tabs to match our underlying {@link #mState} object, usually
@@ -357,16 +363,11 @@
switch (requestCode) {
case PHOTO_PICKED_WITH_DATA: {
- // TODO: pass back to requesting tab
-// final Bundle extras = data.getExtras();
-// if (extras != null) {
-// Bitmap photo = extras.getParcelable("data");
-// mPhoto = photo;
-// mPhotoChanged = true;
-// mPhotoImageView.setImageBitmap(photo);
-// setPhotoPresent(true);
-// }
-// break;
+ // When reaching this point, we've already inflated our tab
+ // state and returned to the last-visible tab.
+ final Bitmap photo = data.getParcelableExtra("data");
+ mEditor.setPhotoBitmap(photo);
+ break;
}
}
}
@@ -384,11 +385,11 @@
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
- // TODO: show or hide photo items based on current tab
- // hide photo stuff entirely if on read-only source
+ final boolean hasPhotoEditor = mEditor.hasPhotoEditor();
+ final boolean hasSetPhoto = mEditor.hasSetPhoto();
- menu.findItem(R.id.menu_photo_add).setVisible(false);
- menu.findItem(R.id.menu_photo_remove).setVisible(false);
+ menu.findItem(R.id.menu_photo_add).setVisible(hasPhotoEditor);
+ menu.findItem(R.id.menu_photo_remove).setVisible(hasSetPhoto);
return true;
}
@@ -567,27 +568,46 @@
return true;
}
-
/**
- * Pick a specific photo to be added under this contact.
+ * Pick a specific photo to be added under the currently selected tab.
*/
private boolean doPickPhotoAction() {
try {
+ // Launch picker to choose photo for selected contact
final Intent intent = ContactsUtils.getPhotoPickIntent();
startActivityForResult(intent, PHOTO_PICKED_WITH_DATA);
} catch (ActivityNotFoundException e) {
- new AlertDialog.Builder(EditContactActivity.this).setTitle(R.string.errorDialogTitle)
- .setMessage(R.string.photoPickerNotFoundText).setPositiveButton(
- android.R.string.ok, null).show();
+ Toast.makeText(this, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
}
return true;
}
+ /**
+ * Clear any existing photo under the currently selected tab.
+ */
public boolean doRemovePhotoAction() {
- // TODO: remove photo from current contact
+ // Remove photo from selected contact
+ mEditor.setPhotoBitmap(null);
return true;
}
+ /** {@inheritDoc} */
+ public void onDeleted(Editor editor) {
+ // Ignore any editor deletes
+ }
+
+ /** {@inheritDoc} */
+ public void onRequest(int request) {
+ switch (request) {
+ case EditorListener.REQUEST_PICK_PHOTO: {
+ doPickPhotoAction();
+ break;
+ }
+ }
+ }
+
+
+
diff --git a/src/com/android/contacts/ui/widget/ContactEditorView.java b/src/com/android/contacts/ui/widget/ContactEditorView.java
index 1fc935d..b35eb40 100644
--- a/src/com/android/contacts/ui/widget/ContactEditorView.java
+++ b/src/com/android/contacts/ui/widget/ContactEditorView.java
@@ -21,43 +21,27 @@
import com.android.contacts.model.EntityDelta;
import com.android.contacts.model.EntityModifier;
import com.android.contacts.model.ContactsSource.DataKind;
-import com.android.contacts.model.ContactsSource.EditField;
import com.android.contacts.model.ContactsSource.EditType;
import com.android.contacts.model.EntityDelta.ValuesDelta;
-import android.app.AlertDialog;
-import android.app.Dialog;
import android.content.Context;
-import android.content.DialogInterface;
import android.content.Entity;
import android.content.res.Resources;
import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
-import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
-import android.text.Editable;
-import android.text.TextUtils;
-import android.text.TextWatcher;
import android.util.AttributeSet;
-import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.View.OnClickListener;
-import android.view.inputmethod.EditorInfo;
-import android.widget.ArrayAdapter;
-import android.widget.EditText;
-import android.widget.ImageView;
-import android.widget.ListAdapter;
import android.widget.RelativeLayout;
import android.widget.TextView;
-import java.util.List;
-
/**
* Custom view that provides all the editor interaction for a specific
* {@link Contacts} represented through an {@link EntityDelta}. Callers can
@@ -75,6 +59,8 @@
private PhotoEditorView mPhoto;
private GenericEditorView mName;
+ private boolean mHasPhotoEditor = false;
+
private ViewGroup mGeneral;
private ViewGroup mSecondary;
@@ -94,6 +80,8 @@
/** {@inheritDoc} */
@Override
protected void onFinishInflate() {
+ super.onFinishInflate();
+
mInflater = (LayoutInflater)getContext().getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
@@ -118,6 +106,33 @@
this.setSecondaryVisible(false);
}
+ /**
+ * Assign the given {@link Bitmap} to the internal {@link PhotoEditorView}
+ * for the {@link EntityDelta} currently being edited.
+ */
+ public void setPhotoBitmap(Bitmap bitmap) {
+ mPhoto.setPhotoBitmap(bitmap);
+ }
+
+ /**
+ * Return true if the current {@link RawContacts} supports {@link Photo},
+ * which means that {@link PhotoEditorView} is enabled.
+ */
+ public boolean hasPhotoEditor() {
+ return mHasPhotoEditor;
+ }
+
+ /**
+ * Return true if internal {@link PhotoEditorView} has a {@link Photo} set.
+ */
+ public boolean hasSetPhoto() {
+ return mPhoto.hasSetPhoto();
+ }
+
+ public PhotoEditorView getPhotoEditor() {
+ return mPhoto;
+ }
+
/** {@inheritDoc} */
public void onClick(View v) {
// Toggle visibility of secondary kinds
@@ -150,6 +165,11 @@
// Make sure we have StructuredName
EntityModifier.ensureKindExists(state, source, StructuredName.CONTENT_ITEM_TYPE);
+ // Show photo editor when supported
+ EntityModifier.ensureKindExists(state, source, Photo.CONTENT_ITEM_TYPE);
+ mHasPhotoEditor = (source.getKindForMimetype(Photo.CONTENT_ITEM_TYPE) != null);
+ mPhoto.setVisibility(mHasPhotoEditor ? View.VISIBLE : View.GONE);
+
// Create editor sections for each possible data kind
for (DataKind kind : source.getSortedDataKinds()) {
// Skip kind of not editable
diff --git a/src/com/android/contacts/ui/widget/KindSectionView.java b/src/com/android/contacts/ui/widget/KindSectionView.java
index 14ec349..5a63992 100644
--- a/src/com/android/contacts/ui/widget/KindSectionView.java
+++ b/src/com/android/contacts/ui/widget/KindSectionView.java
@@ -50,7 +50,7 @@
private DataKind mKind;
private EntityDelta mState;
-
+
public KindSectionView(Context context) {
super(context);
}
@@ -76,11 +76,17 @@
mTitle = (TextView)findViewById(R.id.kind_title);
}
+ /** {@inheritDoc} */
public void onDeleted(Editor editor) {
this.updateAddEnabled();
this.updateEditorsVisible();
}
+ /** {@inheritDoc} */
+ public void onRequest(int request) {
+ // Ignore requests
+ }
+
public void setState(DataKind kind, EntityDelta state) {
mKind = kind;
mState = state;
@@ -125,7 +131,8 @@
final boolean canInsert = EntityModifier.canInsert(mState, mKind);
mAdd.setEnabled(canInsert);
}
-
+
+ /** {@inheritDoc} */
public void onClick(View v) {
// Insert a new child and rebuild
EntityModifier.insertChild(mState, mKind);
diff --git a/src/com/android/contacts/ui/widget/PhotoEditorView.java b/src/com/android/contacts/ui/widget/PhotoEditorView.java
index b88dc3b..cde314d 100644
--- a/src/com/android/contacts/ui/widget/PhotoEditorView.java
+++ b/src/com/android/contacts/ui/widget/PhotoEditorView.java
@@ -27,13 +27,24 @@
import android.graphics.BitmapFactory;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
import android.widget.ImageView;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
/**
* Simple editor for {@link Photo}.
*/
-public class PhotoEditorView extends ImageView implements Editor {
+public class PhotoEditorView extends ImageView implements Editor, OnClickListener {
+ private static final String TAG = "PhotoEditorView";
+
private ValuesDelta mEntry;
+ private EditorListener mListener;
+
+ private boolean mHasSetPhoto = false;
public PhotoEditorView(Context context) {
super(context);
@@ -44,10 +55,25 @@
}
/** {@inheritDoc} */
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ this.setOnClickListener(this);
+ }
+
+ /** {@inheritDoc} */
+ public void onClick(View v) {
+ if (mListener != null) {
+ mListener.onRequest(EditorListener.REQUEST_PICK_PHOTO);
+ }
+ }
+
+ /** {@inheritDoc} */
public void onFieldChanged(String column, String value) {
throw new UnsupportedOperationException("Photos don't support direct field changes");
}
+ /** {@inheritDoc} */
public void setValues(DataKind kind, ValuesDelta values, EntityDelta state) {
mEntry = values;
if (values != null) {
@@ -59,6 +85,7 @@
setScaleType(ImageView.ScaleType.CENTER_CROP);
setImageBitmap(photo);
+ mHasSetPhoto = true;
} else {
resetDefault();
}
@@ -67,12 +94,50 @@
}
}
- protected void resetDefault() {
- // Invalid photo, show default "add photo" placeholder
- setScaleType(ImageView.ScaleType.CENTER);
- setImageResource(R.drawable.ic_menu_add_picture);
+ /**
+ * Return true if a valid {@link Photo} has been set.
+ */
+ public boolean hasSetPhoto() {
+ return mHasSetPhoto;
}
+ /**
+ * Assign the given {@link Bitmap} as the new value, updating UI and
+ * readying for persisting through {@link ValuesDelta}.
+ */
+ public void setPhotoBitmap(Bitmap photo) {
+ if (photo == null) {
+ // Clear any existing photo and return
+ mEntry.put(Photo.PHOTO, (byte[])null);
+ resetDefault();
+ return;
+ }
+
+ final int size = photo.getWidth() * photo.getHeight() * 4;
+ final ByteArrayOutputStream out = new ByteArrayOutputStream(size);
+
+ try {
+ photo.compress(Bitmap.CompressFormat.PNG, 100, out);
+ out.flush();
+ out.close();
+
+ mEntry.put(Photo.PHOTO, out.toByteArray());
+ setImageBitmap(photo);
+ mHasSetPhoto = true;
+ } catch (IOException e) {
+ Log.w(TAG, "Unable to serialize photo: " + e.toString());
+ }
+ }
+
+ protected void resetDefault() {
+ // Invalid photo, show default "add photo" place-holder
+ setScaleType(ImageView.ScaleType.CENTER);
+ setImageResource(R.drawable.ic_menu_add_picture);
+ mHasSetPhoto = false;
+ }
+
+ /** {@inheritDoc} */
public void setEditorListener(EditorListener listener) {
+ mListener = listener;
}
}
diff --git a/tests/src/com/android/contacts/EntityModifierTests.java b/tests/src/com/android/contacts/EntityModifierTests.java
index b72ee19..9d0c1be 100644
--- a/tests/src/com/android/contacts/EntityModifierTests.java
+++ b/tests/src/com/android/contacts/EntityModifierTests.java
@@ -16,20 +16,31 @@
package com.android.contacts;
-import com.android.contacts.model.EntityDelta;
+import static android.content.ContentProviderOperation.TYPE_DELETE;
+import static android.content.ContentProviderOperation.TYPE_INSERT;
+import static android.content.ContentProviderOperation.TYPE_UPDATE;
+
import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.EntityDelta;
import com.android.contacts.model.EntityModifier;
import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.model.ContactsSource.EditField;
import com.android.contacts.model.ContactsSource.EditType;
+import com.android.contacts.model.EntityDelta.ValuesDelta;
import com.google.android.collect.Lists;
+import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.content.Entity;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
/**
@@ -40,6 +51,9 @@
public class EntityModifierTests extends AndroidTestCase {
public static final String TAG = "EntityModifierTests";
+ private static final long TEST_ID = 4;
+ private static final String TEST_PHONE = "218-555-1212";
+
public EntityModifierTests() {
super();
}
@@ -69,6 +83,10 @@
kind.typeList.add(new EditType(Phone.TYPE_FAX_WORK, -1).setSecondary(true));
kind.typeList.add(new EditType(Phone.TYPE_OTHER, -1));
+ kind.fieldList = Lists.newArrayList();
+ kind.fieldList.add(new EditField(Phone.NUMBER, -1, -1));
+ kind.fieldList.add(new EditField(Phone.LABEL, -1, -1));
+
list.add(kind);
}
@@ -78,9 +96,13 @@
/**
* Build an {@link Entity} with the requested set of phone numbers.
*/
- protected EntityDelta getEntity() {
+ protected EntityDelta getEntity(ContentValues... entries) {
final ContentValues contact = new ContentValues();
+ contact.put(RawContacts._ID, TEST_ID);
final Entity before = new Entity(contact);
+ for (ContentValues values : entries) {
+ before.addSubValue(Data.CONTENT_URI, values);
+ }
return EntityDelta.fromBefore(before);
}
@@ -212,4 +234,154 @@
suggested = EntityModifier.getBestValidType(state, kindPhone, false, Integer.MIN_VALUE);
assertEquals("Unexpected suggestion", typeOther, suggested);
}
+
+ public void testIsEmptyEmpty() {
+ final ContactsSource source = getSource();
+ final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+
+ // Test entirely empty row
+ final ContentValues after = new ContentValues();
+ final ValuesDelta values = ValuesDelta.fromAfter(after);
+
+ assertTrue("Expected empty", EntityModifier.isEmpty(values, kindPhone));
+ }
+
+ public void testIsEmptyDirectFields() {
+ final ContactsSource source = getSource();
+ final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+ 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 ValuesDelta values = EntityModifier.insertChild(state, kindPhone, typeHome);
+
+ assertTrue("Expected empty", EntityModifier.isEmpty(values, kindPhone));
+
+ // Insert some data to trigger non-empty state
+ values.put(Phone.NUMBER, TEST_PHONE);
+
+ assertFalse("Expected non-empty", EntityModifier.isEmpty(values, kindPhone));
+ }
+
+ public void testTrimEmptySingle() {
+ final ContactsSource source = getSource();
+ final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+ 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 ValuesDelta values = EntityModifier.insertChild(state, kindPhone, typeHome);
+
+ // Build diff, expecting insert for data row and update enforcement
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ 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_INSERT, 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());
+ }
+
+ // Trim empty rows and try again, expecting no changes
+ EntityModifier.trimEmpty(source, state);
+ diff.clear();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 0, diff.size());
+ }
+
+ public void testTrimEmptyUntouched() {
+ final ContactsSource source = getSource();
+ final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+ final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+ // Build "before" that has empty row
+ final EntityDelta state = getEntity();
+ final ContentValues before = new ContentValues();
+ before.put(Data._ID, TEST_ID);
+ before.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ state.addEntry(ValuesDelta.fromBefore(before));
+
+ // Build diff, expecting no changes
+ final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 0, diff.size());
+
+ // Try trimming existing empty, which we shouldn't touch
+ EntityModifier.trimEmpty(source, state);
+ diff.clear();
+ state.buildDiff(diff);
+ assertEquals("Unexpected operations", 0, diff.size());
+ }
+
+ public void testTrimEmptyAfterUpdate() {
+ final ContactsSource source = getSource();
+ final DataKind kindPhone = source.getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+ final EditType typeHome = EntityModifier.getType(kindPhone, Phone.TYPE_HOME);
+
+ // Build "before" that has row with some phone number
+ final ContentValues before = new ContentValues();
+ before.put(Data._ID, TEST_ID);
+ 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);
+
+ // 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(source, state);
+ 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_DELETE, 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());
+ }
+ }
}