Merge change 24599 into eclair
* changes:
[Issue 2112887] Fixing picture display in the manual contact join UI
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());
+ }
+ }
}