Merge change 24628 into eclair
* changes:
Fix an exception in Contacts when you select text backwards from the end.
diff --git a/res/layout-finger/set_primary_checkbox.xml b/res/layout-finger/set_primary_checkbox.xml
new file mode 100644
index 0000000..bba8cf9
--- /dev/null
+++ b/res/layout-finger/set_primary_checkbox.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="14dip"
+ android:paddingRight="15dip"
+ android:orientation="vertical">
+
+ <CheckBox
+ android:id="@+id/setPrimary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:focusable="true"
+ android:clickable="true"
+ android:text="@string/make_primary"/>
+</LinearLayout>
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/res/values/strings.xml b/res/values/strings.xml
index 46a4210..5974dfc 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -100,10 +100,10 @@
<string name="menu_deleteContact">Delete contact</string>
<!-- Menu item used to call a specific contact when viewing the details of that contact. -->
- <string name="menu_call">Call</string>
+ <string name="menu_call">Call contact</string>
<!-- Menu item used to send an SMS or MMS message to a specific phone number or a contacts default phone number -->
- <string name="menu_sendSMS">Send SMS/MMS</string>
+ <string name="menu_sendSMS">Text contact</string>
<!-- Menu item used to send an email message to a specific email address -->
<string name="menu_sendEmail">Send email</string>
@@ -822,6 +822,15 @@
<!-- Label for onscreen "Dial" button -->
<string name="dial_button_label">Dial</string>
+
+ <!-- Title for the call disambiguation dialog -->
+ <string name="call_disambig_title">Call using</string>
+
+ <!-- Title for the sms disambiguation dialog -->
+ <string name="sms_disambig_title">Text using</string>
+
+ <!-- Message next to disamgiguation dialog check box -->
+ <string name="make_primary">Remember this choice</string>
<!-- Shown as a toast when the user taps on a Fast-Track icon, and no application
was found that could perform the selected action -->
diff --git a/src/com/android/contacts/ContactsListActivity.java b/src/com/android/contacts/ContactsListActivity.java
index d81e20c..7d76531 100644
--- a/src/com/android/contacts/ContactsListActivity.java
+++ b/src/com/android/contacts/ContactsListActivity.java
@@ -100,6 +100,7 @@
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
@@ -310,6 +311,8 @@
private String mShortcutAction;
+ private int mScrollState;
+
/**
* Internal query type when in mode {@link #MODE_QUERY_PICK_TO_VIEW}.
*/
@@ -628,6 +631,7 @@
protected void onResume() {
super.onResume();
+ mScrollState = OnScrollListener.SCROLL_STATE_IDLE;
boolean runQuery = true;
Activity parent = getParent();
@@ -914,27 +918,13 @@
menu.add(0, MENU_ITEM_VIEW_CONTACT, 0, R.string.menu_viewContact)
.setIntent(new Intent(Intent.ACTION_VIEW, contactUri));
- /*
- // Calling contact
- long phoneId = cursor.getLong(PRIMARY_PHONE_ID_COLUMN_INDEX);
- if (phoneId > 0) {
- // Get the display label for the number
- CharSequence label = cursor.getString(PRIMARY_PHONE_LABEL_COLUMN_INDEX);
- int type = cursor.getInt(PRIMARY_PHONE_TYPE_COLUMN_INDEX);
- label = ContactsUtils.getDisplayLabel(
- this, CommonDataKinds.Phone.CONTENT_ITEM_TYPE, type, label);
- Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
- ContentUris.withAppendedId(Data.CONTENT_URI, id));
+ if (cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0) {
+ // Calling contact
menu.add(0, MENU_ITEM_CALL, 0,
- String.format(getString(R.string.menu_callNumber), label)).setIntent(intent);
-
+ getString(R.string.menu_call));
// Send SMS item
- menu.add(0, MENU_ITEM_SEND_SMS, 0, R.string.menu_sendSMS)
- .setIntent(new Intent(Intent.ACTION_SENDTO,
- Uri.fromParts("sms",
- cursor.getString(PRIMARY_PHONE_NUMBER_COLUMN_INDEX), null)));
+ menu.add(0, MENU_ITEM_SEND_SMS, 0, getString(R.string.menu_sendSMS));
}
- */
// Star toggling
int starState = cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX);
@@ -972,6 +962,16 @@
return true;
}
+ case MENU_ITEM_CALL: {
+ callContact(cursor);
+ return true;
+ }
+
+ case MENU_ITEM_SEND_SMS: {
+ smsContact(cursor);
+ return true;
+ }
+
case MENU_ITEM_DELETE: {
final Uri selectedUri = getContactUri(info.position);
doContactDelete(selectedUri);
@@ -1622,33 +1622,86 @@
ListView list = getListView();
if (list.hasFocus()) {
Cursor cursor = (Cursor) list.getSelectedItem();
- if (cursor != null) {
- boolean hasPhone = cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0;
- if (!hasPhone) {
- // There is no phone number.
- signalError();
- return false;
- }
+ return callContact(cursor);
+ }
+ return false;
+ }
- // TODO: transition to use lookup instead of strong id
- final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
- final String phone = ContactsUtils.querySuperPrimaryPhone(getContentResolver(),
- contactId);
- if (phone == null) {
- signalError();
- return false;
- }
+ boolean callContact(Cursor cursor) {
+ return callOrSmsContact(cursor, false /*call*/);
+ }
- Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
- Uri.fromParts(Constants.SCHEME_TEL, phone, null));
- startActivity(intent);
- return true;
+ boolean smsContact(Cursor cursor) {
+ return callOrSmsContact(cursor, true /*sms*/);
+ }
+
+ /**
+ * Calls the contact which the cursor is point to.
+ * @return true if the call was initiated, false otherwise
+ */
+ boolean callOrSmsContact(Cursor cursor, boolean sendSms) {
+ if (cursor != null) {
+ boolean hasPhone = cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0;
+ if (!hasPhone) {
+ // There is no phone number.
+ signalError();
+ return false;
}
+
+ String phone = null;
+ Cursor phonesCursor = null;
+ phonesCursor = queryPhoneNumbers(cursor.getLong(SUMMARY_ID_COLUMN_INDEX));
+ if (phonesCursor == null || phonesCursor.getCount() == 0) {
+ // No valid number
+ signalError();
+ return false;
+ } else if (phonesCursor.getCount() == 1) {
+ // only one number, call it.
+ phone = phonesCursor.getString(phonesCursor.getColumnIndex(Phone.NUMBER));
+ } else {
+ phonesCursor.moveToPosition(-1);
+ while (phonesCursor.moveToNext()) {
+ if (phonesCursor.getInt(phonesCursor.
+ getColumnIndex(Data.IS_SUPER_PRIMARY)) != 0) {
+ // Found super primary, call it.
+ phone = phonesCursor.
+ getString(phonesCursor.getColumnIndex(Phone.NUMBER));
+ break;
+ }
+ }
+ }
+
+ if (phone == null) {
+ // Display dialog to choose a number to call.
+ PhoneDisambigDialog phoneDialog = new PhoneDisambigDialog(
+ this, phonesCursor, sendSms);
+ phoneDialog.show();
+ } else {
+ if (sendSms) {
+ ContactsUtils.initiateSms(this, phone);
+ } else {
+ ContactsUtils.initiateCall(this, phone);
+ }
+ }
+ return true;
}
return false;
}
+ private Cursor queryPhoneNumbers(long contactId) {
+ Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
+ Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY);
+
+ Cursor c = getContentResolver().query(dataUri,
+ new String[] {Data._ID, Phone.NUMBER, Data.IS_SUPER_PRIMARY},
+ Data.MIMETYPE + "=?", new String[] {Phone.CONTENT_ITEM_TYPE}, null);
+ if (c != null && c.moveToFirst()) {
+ return c;
+ }
+ return null;
+ }
+
/**
* Signal an error to the user.
*/
@@ -1758,7 +1811,7 @@
private CharSequence[] mLocalizedLabels;
private boolean mDisplayPhotos = false;
private boolean mDisplayAdditionalData = true;
- private SparseArray<SoftReference<Bitmap>> mBitmapCache = null;
+ private HashMap<Long, SoftReference<Bitmap>> mBitmapCache = null;
private HashSet<ImageView> mItemsMissingImages = null;
private int mFrequentSeparatorPos = ListView.INVALID_POSITION;
private boolean mDisplaySectionHeaders = true;
@@ -1766,7 +1819,6 @@
private Cursor mSuggestionsCursor;
private int mSuggestionsCursorCount;
private ImageFetchHandler mHandler;
- private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE;
private static final int FETCH_IMAGE_MSG = 1;
public ContactItemListAdapter(Context context) {
@@ -1810,7 +1862,7 @@
if ((mMode & MODE_MASK_SHOW_PHOTOS) == MODE_MASK_SHOW_PHOTOS) {
mDisplayPhotos = true;
setViewResource(R.layout.contacts_list_item_photo);
- mBitmapCache = new SparseArray<SoftReference<Bitmap>>();
+ mBitmapCache = new HashMap<Long, SoftReference<Bitmap>>();
mItemsMissingImages = new HashSet<ImageView>();
}
@@ -1827,30 +1879,37 @@
return;
}
switch(message.what) {
- case FETCH_IMAGE_MSG:
+ case FETCH_IMAGE_MSG: {
ImageView imageView = (ImageView) message.obj;
- int pos = (Integer) imageView.getTag();
- Cursor cursor = (Cursor) getItem(pos);
+ long photoId = (Long) imageView.getTag();
+ if (photoId == 0) {
+ break;
+ }
- if (cursor != null && !cursor.isNull(SUMMARY_PHOTO_ID_COLUMN_INDEX)) {
- try {
- Bitmap photo = ContactsUtils.loadContactPhoto(
- mContext, cursor.getInt(SUMMARY_PHOTO_ID_COLUMN_INDEX),
- null);
- mBitmapCache.put(pos, new SoftReference<Bitmap>(photo));
- if (photo != null) {
- imageView.setImageBitmap(photo);
- }
- } catch (OutOfMemoryError e) {
- // Not enough memory for the photo, do nothing.
+ Bitmap photo = null;
+ try {
+ photo = ContactsUtils.loadContactPhoto(mContext, photoId, null);
+ } catch (OutOfMemoryError e) {
+ // Not enough memory for the photo, do nothing.
+ }
+
+ if (photo == null) {
+ break;
+ }
+
+ mBitmapCache.put(photoId, new SoftReference<Bitmap>(photo));
+
+ // Make sure the photoId on this image view has not changed
+ // while we were loading the image.
+ synchronized (imageView) {
+ long currentPhotoId = (Long) imageView.getTag();
+ if (currentPhotoId == photoId) {
+ imageView.setImageBitmap(photo);
+ mItemsMissingImages.remove(imageView);
}
}
-
- if (imageView.getDrawable() == null) {
- imageView.setImageResource(R.drawable.ic_contact_list_picture);
- }
- mItemsMissingImages.remove(imageView);
break;
+ }
}
}
@@ -2052,29 +2111,44 @@
// Set the photo, if requested
if (mDisplayPhotos) {
- int pos = cursor.getPosition();
- Bitmap photo = null;
- cache.photoView.setImageBitmap(null);
- cache.photoView.setTag(pos);
- // Look for the cached bitmap
- SoftReference<Bitmap> ref = mBitmapCache.get(pos);
- if (ref != null) {
- photo = ref.get();
+ long photoId = 0;
+ if (!cursor.isNull(SUMMARY_PHOTO_ID_COLUMN_INDEX)) {
+ photoId = cursor.getLong(SUMMARY_PHOTO_ID_COLUMN_INDEX);
}
- // Bind the photo, or use the fallback no photo resource
- if (photo != null) {
- cache.photoView.setImageBitmap(photo);
- } else {
- // Cache miss
+ cache.photoView.setTag(photoId);
+
+ if (photoId == 0) {
cache.photoView.setImageResource(R.drawable.ic_contact_list_picture);
- if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
- // Scrolling is idle, go get the image right now.
- sendFetchImageMessage(cache.photoView);
+ } else {
+
+ Bitmap photo = null;
+
+ // Look for the cached bitmap
+ SoftReference<Bitmap> ref = mBitmapCache.get(photoId);
+ if (ref != null) {
+ photo = ref.get();
+ if (photo == null) {
+ mBitmapCache.remove(photoId);
+ }
+ }
+
+ // Bind the photo, or use the fallback no photo resource
+ if (photo != null) {
+ cache.photoView.setImageBitmap(photo);
} else {
- // Add it to a set of images that will be populated when scrolling stops.
+ // Cache miss
+ cache.photoView.setImageResource(R.drawable.ic_contact_list_picture);
+
+ // Add it to a set of images that are populated asynchronously.
mItemsMissingImages.add(cache.photoView);
+
+ if (mScrollState != OnScrollListener.SCROLL_STATE_FLING) {
+
+ // Scrolling is idle or slow, go get the image right now.
+ sendFetchImageMessage(cache.photoView);
+ }
}
}
}
@@ -2361,8 +2435,8 @@
public void onScrollStateChanged(AbsListView view, int scrollState) {
mScrollState = scrollState;
- if (scrollState != OnScrollListener.SCROLL_STATE_IDLE) {
- // If we are not idle, stop loading images.
+ if (scrollState == OnScrollListener.SCROLL_STATE_FLING) {
+ // If we are in a fling, stop loading images.
clearImageFetching();
} else if (mDisplayPhotos) {
processMissingImageItems(view);
@@ -2371,7 +2445,6 @@
private void processMissingImageItems(AbsListView view) {
for (ImageView iv : mItemsMissingImages) {
- int pos = (Integer) iv.getTag();
sendFetchImageMessage(iv);
}
}
diff --git a/src/com/android/contacts/ContactsUtils.java b/src/com/android/contacts/ContactsUtils.java
index 7537d30..dea0bad 100644
--- a/src/com/android/contacts/ContactsUtils.java
+++ b/src/com/android/contacts/ContactsUtils.java
@@ -353,4 +353,22 @@
}
return createTabIndicatorView(parent, null, icon);
}
+
+ /**
+ * Kick off an intent to initiate a call.
+ */
+ public static void initiateCall(Context context, CharSequence phoneNumber) {
+ Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
+ Uri.fromParts("tel", phoneNumber.toString(), null));
+ context.startActivity(intent);
+ }
+
+ /**
+ * Kick off an intent to initiate an Sms/Mms message.
+ */
+ public static void initiateSms(Context context, CharSequence phoneNumber) {
+ Intent intent = new Intent(Intent.ACTION_SENDTO,
+ Uri.fromParts("sms", phoneNumber.toString(), null));
+ context.startActivity(intent);
+ }
}
diff --git a/src/com/android/contacts/PhoneDisambigDialog.java b/src/com/android/contacts/PhoneDisambigDialog.java
new file mode 100644
index 0000000..58d3721
--- /dev/null
+++ b/src/com/android/contacts/PhoneDisambigDialog.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2009 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;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+
+import android.app.AlertDialog;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.database.Cursor;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.SimpleCursorAdapter;
+
+/**
+ * Class used for displaying a dialog with a list of phone numbers of which
+ * one will be chosen to make a call or initiate an sms message.
+ */
+public class PhoneDisambigDialog implements DialogInterface.OnClickListener,
+ DialogInterface.OnDismissListener, CompoundButton.OnCheckedChangeListener{
+
+ private boolean mMakePrimary = false;
+ private Context mContext;
+ private AlertDialog mDialog;
+ private boolean mSendSms;
+ private Cursor mPhonesCursor;
+
+ public PhoneDisambigDialog(Context context, Cursor phonesCursor) {
+ this(context, phonesCursor, false /*make call*/);
+ }
+
+ public PhoneDisambigDialog(Context context, Cursor phonesCursor, boolean sendSms) {
+ mContext = context;
+ mSendSms = sendSms;
+ mPhonesCursor = phonesCursor;
+
+ LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ View setPrimaryView = inflater.
+ inflate(R.layout.set_primary_checkbox, null);
+ ((CheckBox) setPrimaryView.findViewById(R.id.setPrimary)).
+ setOnCheckedChangeListener(this);
+
+ // Need to show disambig dialogue.
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mContext).
+ setCursor(mPhonesCursor, this, Phone.NUMBER).
+ setTitle(sendSms ? R.string.sms_disambig_title : R.string.call_disambig_title).
+ setView(setPrimaryView);
+
+ mDialog = dialogBuilder.create();
+ }
+
+ /**
+ * Show the dialog.
+ */
+ public void show() {
+ mDialog.show();
+ }
+
+ public void onClick(DialogInterface dialog, int which) {
+ if (mPhonesCursor.moveToPosition(which)) {
+ long id = mPhonesCursor.getLong(mPhonesCursor.getColumnIndex(Data._ID));
+ String phone = mPhonesCursor.getString(mPhonesCursor.getColumnIndex(Phone.NUMBER));
+ if (mMakePrimary) {
+ ContentValues values = new ContentValues(1);
+ values.put(Data.IS_SUPER_PRIMARY, 1);
+ mContext.getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, id),
+ values, null, null);
+ }
+
+ if (mSendSms) {
+ ContactsUtils.initiateSms(mContext, phone);
+ } else {
+ ContactsUtils.initiateCall(mContext, phone);
+ }
+ } else {
+ dialog.dismiss();
+ }
+ }
+
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ mMakePrimary = isChecked;
+ }
+
+ public void onDismiss(DialogInterface dialog) {
+ mPhonesCursor.close();
+ }
+}
diff --git a/src/com/android/contacts/ViewContactActivity.java b/src/com/android/contacts/ViewContactActivity.java
index ddd0abb..d2328ee 100644
--- a/src/com/android/contacts/ViewContactActivity.java
+++ b/src/com/android/contacts/ViewContactActivity.java
@@ -614,17 +614,7 @@
}
// Update the primary values in the data record.
- ContentValues values = new ContentValues(2);
- values.put(Data.IS_PRIMARY, 1);
-
- if (entry.ids.size() > 0) {
- for (int i = 0; i < entry.ids.size(); i++) {
- getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI,
- entry.ids.get(i)),
- values, null, null);
- }
- }
-
+ ContentValues values = new ContentValues(1);
values.put(Data.IS_SUPER_PRIMARY, 1);
getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, entry.id),
values, null, null);
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 885a06b..fdac44c 100644
--- a/src/com/android/contacts/model/HardCodedSources.java
+++ b/src/com/android/contacts/model/HardCodedSources.java
@@ -28,7 +28,6 @@
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
-import android.provider.ContactsContract;
import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
@@ -44,8 +43,6 @@
import android.provider.ContactsContract.Contacts.Data;
import android.view.inputmethod.EditorInfo;
-import java.util.ArrayList;
-
/**
* Hard-coded definition of some {@link ContactsSource} constraints, since the
* XML language hasn't been finalized.
@@ -161,6 +158,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 +446,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);
}
@@ -556,6 +561,39 @@
}
{
+ // EXCHANGE: POSTAL
+ DataKind kind = new DataKind(StructuredPostal.CONTENT_ITEM_TYPE,
+ R.string.postalLabelsGroup, R.drawable.sym_action_map, 25, true);
+
+ kind.actionHeader = new ActionInflater(list.resPackageName, kind);
+ // TODO: build body from various structured fields
+ kind.actionBody = new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS);
+
+ kind.typeColumn = StructuredPostal.TYPE;
+ kind.typeList = Lists.newArrayList();
+ kind.typeList.add(new EditType(StructuredPostal.TYPE_WORK, R.string.type_work,
+ R.string.map_work).setSpecificMax(1));
+ kind.typeList.add(new EditType(StructuredPostal.TYPE_HOME, R.string.type_home,
+ R.string.map_home).setSpecificMax(1));
+ kind.typeList.add(new EditType(StructuredPostal.TYPE_OTHER, R.string.type_other,
+ R.string.map_other).setSpecificMax(1));
+
+ kind.fieldList = Lists.newArrayList();
+ kind.fieldList.add(new EditField(StructuredPostal.STREET, R.string.postal_street,
+ FLAGS_POSTAL));
+ kind.fieldList.add(new EditField(StructuredPostal.CITY, R.string.postal_city,
+ FLAGS_POSTAL));
+ kind.fieldList.add(new EditField(StructuredPostal.REGION, R.string.postal_region,
+ FLAGS_POSTAL));
+ kind.fieldList.add(new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode,
+ FLAGS_POSTAL));
+ kind.fieldList.add(new EditField(StructuredPostal.COUNTRY, R.string.postal_country,
+ FLAGS_POSTAL, true));
+
+ list.add(kind);
+ }
+
+ {
// EXCHANGE: NICKNAME
DataKind kind = new DataKind(Nickname.CONTENT_ITEM_TYPE,
R.string.nicknameLabelsGroup, -1, 115, true);
@@ -633,11 +671,53 @@
* Hard-coded instance of {@link ContactsSource} for Facebook.
*/
static void buildFacebook(Context context, ContactsSource list) {
- // Rely on the fallback source for now, it has a generic set of sources
- buildFallback(context, list);
-
list.accountType = ACCOUNT_TYPE_FACEBOOK;
list.readOnly = true;
+
+ {
+ // FACEBOOK: PHONE
+ DataKind kind = new DataKind(Phone.CONTENT_ITEM_TYPE,
+ R.string.phoneLabelsGroup, android.R.drawable.sym_action_call, 10, true);
+ kind.iconAltRes = R.drawable.sym_action_sms;
+
+ kind.actionHeader = new ActionInflater(list.resPackageName, kind);
+ kind.actionAltHeader = new ActionAltInflater(list.resPackageName, kind);
+ kind.actionBody = new SimpleInflater(Phone.NUMBER);
+
+ kind.typeColumn = Phone.TYPE;
+ kind.typeList = Lists.newArrayList();
+ kind.typeList.add(new EditType(Phone.TYPE_MOBILE, R.string.type_mobile,
+ R.string.call_mobile, R.string.sms_mobile));
+ kind.typeList.add(new EditType(Phone.TYPE_OTHER, R.string.type_other,
+ R.string.call_other, R.string.sms_other));
+
+ list.add(kind);
+ }
+
+ {
+ // FACEBOOK: EMAIL
+ DataKind kind = new DataKind(Email.CONTENT_ITEM_TYPE,
+ R.string.emailLabelsGroup, android.R.drawable.sym_action_email, 15, true);
+
+ kind.actionHeader = new ActionInflater(list.resPackageName, kind);
+ kind.actionBody = new SimpleInflater(Email.DATA);
+
+ kind.typeColumn = Email.TYPE;
+ kind.typeList = Lists.newArrayList();
+ kind.typeList
+ .add(new EditType(Email.TYPE_HOME, R.string.type_home, R.string.email_home));
+ kind.typeList
+ .add(new EditType(Email.TYPE_WORK, R.string.type_work, R.string.email_work));
+ kind.typeList.add(new EditType(Email.TYPE_OTHER, R.string.type_other,
+ R.string.email_other));
+ kind.typeList.add(new EditType(Email.TYPE_CUSTOM, R.string.type_custom,
+ R.string.email_home).setSecondary(true).setCustomColumn(Email.LABEL));
+
+ kind.fieldList = Lists.newArrayList();
+ kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+ 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());
+ }
+ }
}