Checkpoint of new edit contact UI, work in progress.
This change introduces several new concepts which are
summarized below. One major change is a MMVC approach that
has two models: the structured Contacts data, and the
data-source constraints model. Another is augmenting an
Entity using a specific set of actions.
First, each data source is defined through a ContactsSource
that describe how it handles data, both for rendering and
editing cases, such as the Data.MIMETYPE it handles, what
types are allowed, and the fields required for editing. In
this change, ContactsSource objects for Google and Exchange
are hard-coded, but an initial XML version will need to be
finalized for supporting third-party apps that show custom
icons and strings.
Second, AugmentedEntity allows us to keep the edit changes
separate from the initial data and build a "diff" between
the current Entity state and the desired changes, which is
represented as a set of ContentProviderOperations. If the
data changed while the user was editing, we can easily swap
in the new Entity and apply the edits on top. In the worst
case, this may end up creating duplicated data, but won't
lose the users changes.
Finally, this change starts splitting the UI and modeling
code into different sub-packages. The UI is split into
multiple ViewHolders to mirror the structure on screen.
There are dozens of TODOs littered throughout the code,
which I'm following up on shortly. This is a checkpoint to
start a code review on the core structure.
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index c4a43df..b37ba3a 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -285,7 +285,7 @@
</activity>
<!-- Edits the details of a single contact -->
- <activity android:name="EditContactActivity"
+ <activity android:name=".ui.EditContactActivity"
android:windowSoftInputMode="stateVisible|adjustResize">
<intent-filter android:label="@string/editContactDescription">
<action android:name="android.intent.action.EDIT" />
diff --git a/res/layout/act_edit.xml b/res/layout/act_edit.xml
new file mode 100644
index 0000000..fe79160
--- /dev/null
+++ b/res/layout/act_edit.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 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="fill_parent"
+ android:orientation="vertical"
+ android:fillViewport="true">
+
+ <!-- TODO: insert aggregate summary widget -->
+ <!-- TODO: insert contact tab widget -->
+
+ <ScrollView
+ android:id="@android:id/tabcontent"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_weight="1"
+ android:fillViewport="true" />
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ style="@android:style/ButtonBar">
+
+ <Button android:id="@+id/btn_done"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/menu_done" />
+
+ <Button android:id="@+id/btn_discard"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:text="@string/menu_doNotSave" />
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/res/layout/act_edit_contact.xml b/res/layout/act_edit_contact.xml
new file mode 100644
index 0000000..b052671
--- /dev/null
+++ b/res/layout/act_edit_contact.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 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.
+-->
+
+<!-- placed inside act_edit as tabcontent -->
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:fillViewport="true">
+
+ <TextView
+ android:id="@+id/text_summary"
+android:background="#f0f0"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content" />
+
+ <FrameLayout
+ android:id="@+id/hook_photo"
+android:background="#f00f"
+ android:layout_width="76dip"
+ android:layout_height="76dip"
+ android:layout_below="@+id/text_summary" />
+
+ <FrameLayout
+ android:id="@+id/hook_displayname"
+android:background="#fff0"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignTop="@+id/hook_photo"
+ android:layout_alignBottom="@+id/hook_photo"
+ android:layout_toRightOf="@+id/hook_photo" />
+
+ <LinearLayout
+ android:id="@+id/sect_general"
+android:background="#f0ff"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/hook_photo"
+ android:orientation="vertical" />
+
+ <!-- TODO: make secondary section collapsable -->
+ <TextView
+android:background="#ff00"
+ android:id="@+id/head_secondary"
+ android:layout_width="fill_parent"
+ android:layout_height="10dip"
+ android:layout_below="@+id/sect_general" />
+
+ <LinearLayout
+android:background="#ff0f"
+ android:id="@+id/sect_secondary"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/head_secondary"
+ android:orientation="vertical" />
+
+</RelativeLayout>
diff --git a/res/layout/item_edit_kind.xml b/res/layout/item_edit_kind.xml
new file mode 100644
index 0000000..e01f160
--- /dev/null
+++ b/res/layout/item_edit_kind.xml
@@ -0,0 +1,63 @@
+<?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.
+-->
+
+<!-- the body surrounding all editors for a specific kind -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/kind_header"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="14dip"
+ android:layout_marginTop="3dip"
+ android:layout_marginBottom="1dip"
+ android:layout_marginRight="?android:attr/scrollbarSize"
+ android:orientation="horizontal"
+ android:gravity="bottom"
+ android:focusable="true"
+ android:clickable="true">
+
+ <TextView
+ android:id="@+id/kind_title"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginBottom="8dip"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal" />
+
+ <ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:duplicateParentState="true"
+ style="@style/PlusButton" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/kind_editors"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" />
+
+</LinearLayout>
diff --git a/res/layout/item_editor.xml b/res/layout/item_editor.xml
new file mode 100644
index 0000000..8d46bcc
--- /dev/null
+++ b/res/layout/item_editor.xml
@@ -0,0 +1,48 @@
+<?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:orientation="horizontal"
+ android:baselineAligned="false"
+ android:gravity="top"
+ android:paddingRight="?android:attr/scrollbarSize">
+
+ <Button
+ android:id="@+id/edit_label"
+ android:layout_width="100dip"
+ android:layout_height="wrap_content"
+ android:gravity="left|center_vertical" />
+
+ <LinearLayout
+ android:id="@+id/edit_fields"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="4dip"
+ android:orientation="vertical"
+ android:baselineAligned="false"
+ android:gravity="center_vertical" />
+
+ <ImageButton
+ android:id="@+id/edit_delete"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="@style/MinusButton" />
+
+</LinearLayout>
diff --git a/res/layout/item_editor_displayname.xml b/res/layout/item_editor_displayname.xml
new file mode 100644
index 0000000..ae6d815
--- /dev/null
+++ b/res/layout/item_editor_displayname.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 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:orientation="vertical">
+
+ <EditText
+ android:id="@+id/name"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="?android:attr/scrollbarSize"
+ android:gravity="center_vertical"
+ android:inputType="textPersonName|textCapWords"
+ android:hint="@string/ghostData_name" />
+
+ <!-- "Phonetic name" entry widget, visible only in certain locales -->
+ <include layout="@layout/edit_phonetic_name"/>
+
+</LinearLayout>
diff --git a/res/layout/item_editor_field.xml b/res/layout/item_editor_field.xml
new file mode 100644
index 0000000..1e77068
--- /dev/null
+++ b/res/layout/item_editor_field.xml
@@ -0,0 +1,20 @@
+<?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.
+-->
+
+<EditText
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content" />
diff --git a/res/layout/item_editor_photo.xml b/res/layout/item_editor_photo.xml
new file mode 100644
index 0000000..d4fcf13
--- /dev/null
+++ b/res/layout/item_editor_photo.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 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.
+-->
+
+<ImageView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:clickable="true"
+ android:focusable="true"
+ android:src="@drawable/ic_menu_add_picture"
+ android:scaleType="center"
+ android:background="@drawable/btn_contact_picture" />
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c6d1401..93724fb 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -658,4 +658,44 @@
-->
<string name="description_image_button_pound">pound</string>
+<!-- TODO: add comments to each of these strings to prepare for translation -->
+<!-- TODO: split out separate strings for "Home" versus "Call home", see http://b/2029022 -->
+<string name="nameLabelsGroup">Name</string>
+<string name="nicknameLabelsGroup">Nickname</string>
+<string name="organizationLabelsGroup">Organization</string>
+<string name="websiteLabelsGroup">Website</string>
+
+<string name="type_home">Home</string>
+<string name="type_mobile">Mobile</string>
+<string name="type_work">Work</string>
+<string name="type_fax_work">Work Fax</string>
+<string name="type_fax_home">Home Fax</string>
+<string name="type_pager">Pager</string>
+<string name="type_other">Other</string>
+<string name="type_custom">Custom</string>
+
+<!-- exchange specific -->
+<string name="type_home_2">Home 2</string>
+<string name="type_work_2">Work 2</string>
+<string name="type_car">Car</string>
+<string name="type_company_main">Company Main</string>
+<string name="type_mms">MMS</string>
+<string name="type_radio">Radio</string>
+<string name="type_assistant">Assistant</string>
+
+<string name="type_email_1">Email 1</string>
+<string name="type_email_2">Email 2</string>
+<string name="type_email_3">Email 3</string>
+
+
+<string name="type_im_aim">AIM</string>
+<string name="type_im_live">Windows Live</string>
+<string name="type_im_yahoo">Yahoo</string>
+<string name="type_im_skype">Skype</string>
+<string name="type_im_qq">QQ</string>
+<string name="type_im_talk">Google Talk</string>
+<string name="type_im_icq">ICQ</string>
+<string name="type_im_jabber">Jabber</string>
+
+
</resources>
diff --git a/src/com/android/contacts/ContactsListActivity.java b/src/com/android/contacts/ContactsListActivity.java
index 6ce5f55..2b878bc 100644
--- a/src/com/android/contacts/ContactsListActivity.java
+++ b/src/com/android/contacts/ContactsListActivity.java
@@ -17,6 +17,7 @@
package com.android.contacts;
import com.android.contacts.DisplayGroupsActivity.Prefs;
+import com.android.contacts.ui.EditContactActivity;
import android.app.Activity;
import android.app.AlertDialog;
@@ -1322,22 +1323,24 @@
mAlphabet = context.getString(com.android.internal.R.string.fast_scroll_alphabet);
mUnknownNameText = context.getText(android.R.string.unknownName);
- switch (mMode) {
- case MODE_PICK_POSTAL:
- mLocalizedLabels = EditContactActivity.getLabelsForMimetype(mContext,
- CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE);
- mDisplaySectionHeaders = false;
- break;
- case MODE_PICK_PHONE:
- mLocalizedLabels = EditContactActivity.getLabelsForMimetype(mContext,
- CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
- mDisplaySectionHeaders = false;
- break;
- default:
- mLocalizedLabels = EditContactActivity.getLabelsForMimetype(mContext,
- CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
- break;
- }
+ // TODO: use a different method of finding labels
+// switch (mMode) {
+// case MODE_PICK_POSTAL:
+// mLocalizedLabels = EditContactActivity.getLabelsForMimetype(mContext,
+// CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE);
+// mDisplaySectionHeaders = false;
+// break;
+// case MODE_PICK_PHONE:
+// mLocalizedLabels = EditContactActivity.getLabelsForMimetype(mContext,
+// CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
+// mDisplaySectionHeaders = false;
+// break;
+// default:
+ mLocalizedLabels = context.getResources().getStringArray(android.R.array.phoneTypes);
+// EditContactActivity.getLabelsForMimetype(mContext,
+// CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
+// break;
+// }
// Do not display the second line of text if in a specific SEARCH query mode, usually for
// matching a specific E-mail or phone number. Any contact details
diff --git a/src/com/android/contacts/EditContactActivity.java b/src/com/android/contacts/EditContactActivity.java
deleted file mode 100644
index f10ff93..0000000
--- a/src/com/android/contacts/EditContactActivity.java
+++ /dev/null
@@ -1,2233 +0,0 @@
-/*
- * Copyright (C) 2007 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 static com.android.contacts.ContactEntryAdapter.CONTACT_PROJECTION;
-import static com.android.contacts.ContactEntryAdapter.DATA_1_COLUMN;
-import static com.android.contacts.ContactEntryAdapter.DATA_2_COLUMN;
-import static com.android.contacts.ContactEntryAdapter.DATA_3_COLUMN;
-import static com.android.contacts.ContactEntryAdapter.DATA_4_COLUMN;
-import static com.android.contacts.ContactEntryAdapter.DATA_5_COLUMN;
-import static com.android.contacts.ContactEntryAdapter.DATA_9_COLUMN;
-import static com.android.contacts.ContactEntryAdapter.DATA_ID_COLUMN;
-import static com.android.contacts.ContactEntryAdapter.DATA_IS_SUPER_PRIMARY_COLUMN;
-import static com.android.contacts.ContactEntryAdapter.DATA_MIMETYPE_COLUMN;
-
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.content.ActivityNotFoundException;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.res.ColorStateList;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.provider.ContactsContract.CommonDataKinds;
-import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
-import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.Im;
-import android.provider.ContactsContract.CommonDataKinds.Note;
-import android.provider.ContactsContract.CommonDataKinds.Organization;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.Photo;
-import android.provider.ContactsContract.CommonDataKinds.StructuredName;
-import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
-import android.telephony.PhoneNumberFormattingTextWatcher;
-import android.text.Editable;
-import android.text.TextUtils;
-import android.text.TextWatcher;
-import android.text.method.TextKeyListener;
-import android.text.method.TextKeyListener.Capitalize;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewParent;
-import android.view.inputmethod.EditorInfo;
-import android.widget.Button;
-import android.widget.CheckBox;
-import android.widget.EditText;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import java.io.ByteArrayOutputStream;
-import java.util.ArrayList;
-
-// TODO: Much of this class has been commented out as a starting place for transition to new data
-// model. It will be added back as we progress.
-
-/**
- * Activity for editing or inserting a contact. Note that if the contact data changes in the
- * background while this activity is running, the updates will be overwritten.
- */
-public final class EditContactActivity extends Activity implements View.OnClickListener,
- TextWatcher, View.OnFocusChangeListener {
- private static final String TAG = "EditContactActivity";
-
- private static final int STATE_UNKNOWN = 0;
- /** Editing an existing contact */
- private static final int STATE_EDIT = 1;
- /** The full insert mode */
- private static final int STATE_INSERT = 2;
-
- /** The launch code when picking a photo and the raw data is returned */
- private static final int PHOTO_PICKED_WITH_DATA = 3021;
-
- // These correspond to the string array in resources for picker "other" items
- final static int OTHER_ORGANIZATION = 0;
- final static int OTHER_NOTE = 1;
-
- // Dialog IDs
- final static int DELETE_CONFIRMATION_DIALOG = 2;
-
- // Section IDs
- final static int SECTION_PHONES = 3;
- final static int SECTION_EMAIL = 4;
- final static int SECTION_IM = 5;
- final static int SECTION_POSTAL = 6;
- final static int SECTION_ORG = 7;
- final static int SECTION_NOTE = 8;
-
- // Menu item IDs
- public static final int MENU_ITEM_SAVE = 1;
- public static final int MENU_ITEM_DONT_SAVE = 2;
- public static final int MENU_ITEM_DELETE = 3;
- public static final int MENU_ITEM_PHOTO = 6;
-
- /** Used to represent an invalid type for a contact entry */
- private static final int INVALID_TYPE = -1;
-
- /** The default type for a phone that is added via an intent */
- private static final int DEFAULT_PHONE_TYPE = Phone.TYPE_MOBILE;
-
- /** The default type for an email that is added via an intent */
- private static final int DEFAULT_EMAIL_TYPE = Email.TYPE_HOME;
-
- /** The default type for a postal address that is added via an intent */
- private static final int DEFAULT_POSTAL_TYPE = StructuredPostal.TYPE_HOME;
-
- private int mState; // saved across instances
- private boolean mInsert; // saved across instances
- private Uri mUri; // saved across instances
- private Uri mAggDataUri;
- /** In insert mode this is the photo */
- private Bitmap mPhoto; // saved across instances
- private boolean mPhotoChanged = false; // saved across instances
-
- private EditText mNameView;
- private Uri mStructuredNameUri;
- private Uri mPhotoDataUri;
- private ImageView mPhotoImageView;
- private ViewGroup mContentView;
- private LinearLayout mLayout;
- private LayoutInflater mInflater;
- private MenuItem mPhotoMenuItem;
- private boolean mPhotoPresent = false;
- private EditText mPhoneticNameView; // invisible in some locales, but always present
-
- /** Flag marking this contact as changed, meaning we should write changes back. */
- private boolean mContactChanged = false;
-
- /** List of all the group names */
- private CharSequence[] mGroups;
-
- /** Is this contact part of the group */
- private boolean[] mInTheGroup;
-
- /*
- private static final String[] GROUP_ID_PROJECTION = new String[] {
- Groups._ID,
- };
-
- private static final String[] GROUPMEMBERSHIP_ID_PROJECTION = new String[] {
- GroupMembership._ID,
- };
- */
-
- // These are accessed by inner classes. They're package scoped to make access more efficient.
- /* package */ ContentResolver mResolver;
- /* package */ ArrayList<EditEntry> mPhoneEntries = new ArrayList<EditEntry>();
- /* package */ ArrayList<EditEntry> mEmailEntries = new ArrayList<EditEntry>();
- /* package */ ArrayList<EditEntry> mImEntries = new ArrayList<EditEntry>();
- /* package */ ArrayList<EditEntry> mPostalEntries = new ArrayList<EditEntry>();
- /* package */ ArrayList<EditEntry> mOrgEntries = new ArrayList<EditEntry>();
- /* package */ ArrayList<EditEntry> mNoteEntries = new ArrayList<EditEntry>();
- /* package */ ArrayList<EditEntry> mOtherEntries = new ArrayList<EditEntry>();
- /* package */ ArrayList<ArrayList<EditEntry>> mSections = new ArrayList<ArrayList<EditEntry>>();
-
- /* package */ static final int MSG_DELETE = 1;
- /* package */ static final int MSG_CHANGE_LABEL = 2;
- /* package */ static final int MSG_ADD_PHONE = 3;
- /* package */ static final int MSG_ADD_EMAIL = 4;
- /* package */ static final int MSG_ADD_POSTAL = 5;
-
- public void onClick(View v) {
- switch (v.getId()) {
- case R.id.photoImage: {
- doPickPhotoAction();
- break;
- }
-
- case R.id.checkable: {
- CheckBox checkBox = (CheckBox) v.findViewById(R.id.checkbox);
- checkBox.toggle();
-
- EditEntry entry = findEntryForView(v);
- entry.data = checkBox.isChecked() ? "1" : "0";
-
- mContactChanged = true;
- break;
- }
-
- /*
- case R.id.entry_group: {
- EditEntry entry = findEntryForView(v);
- doPickGroup(entry);
- break;
- }
- */
-
- case R.id.separator: {
- // Someone clicked on a section header, so handle add action
- // TODO: Data addition is still being hashed out.
- /*
- int sectionType = (Integer) v.getTag();
- doAddAction(sectionType);
- */
- break;
- }
-
- case R.id.saveButton:
- doSaveAction();
- break;
-
- case R.id.discardButton:
- doRevertAction();
- break;
-
- case R.id.delete: {
- EditEntry entry = findEntryForView(v);
- if (entry != null) {
- // Clear the text and hide the view so it gets saved properly
- ((TextView) entry.view.findViewById(R.id.data)).setText(null);
- entry.view.setVisibility(View.GONE);
- entry.isDeleted = true;
- }
-
- // Force rebuild of views because section headers might need to change
- buildViews();
- break;
- }
-
- case R.id.label: {
- EditEntry entry = findEntryForView(v);
- if (entry != null) {
- String[] labels = getLabelsForMimetype(this, entry.mimetype);
- LabelPickedListener listener = new LabelPickedListener(entry, labels);
- new AlertDialog.Builder(EditContactActivity.this)
- .setItems(labels, listener)
- .setTitle(R.string.selectLabel)
- .show();
- }
- break;
- }
- }
- }
-
- private void setPhotoPresent(boolean present) {
- mPhotoPresent = present;
-
- // Correctly scale the contact photo if present, otherwise just center
- // the photo placeholder icon.
- if (mPhotoPresent) {
- mPhotoImageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
- } else {
- mPhotoImageView.setImageResource(R.drawable.ic_menu_add_picture);
- mPhotoImageView.setScaleType(ImageView.ScaleType.CENTER);
- }
-
- if (mPhotoMenuItem != null) {
- if (present) {
- mPhotoMenuItem.setTitle(R.string.removePicture);
- mPhotoMenuItem.setIcon(android.R.drawable.ic_menu_delete);
- } else {
- mPhotoMenuItem.setTitle(R.string.addPicture);
- mPhotoMenuItem.setIcon(R.drawable.ic_menu_add_picture);
- }
- }
- }
-
- private EditEntry findEntryForView(View v) {
- // Try to find the entry for this view
- EditEntry entry = null;
- do {
- Object tag = v.getTag();
- if (tag != null && tag instanceof EditEntry) {
- entry = (EditEntry) tag;
- break;
- } else {
- ViewParent parent = v.getParent();
- if (parent != null && parent instanceof View) {
- v = (View) parent;
- } else {
- v = null;
- }
- }
- } while (v != null);
- return entry;
- }
-
- private DialogInterface.OnClickListener mDeleteContactDialogListener =
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int button) {
- mResolver.delete(mUri, null, null);
- finish();
- }
- };
-
- private boolean mMobilePhoneAdded = false;
- private boolean mPrimaryEmailAdded = false;
-
- @Override
- protected void onCreate(Bundle icicle) {
- super.onCreate(icicle);
-
- mResolver = getContentResolver();
-
- // Build the list of sections
- setupSections();
-
- // Load the UI
- mInflater = getLayoutInflater();
- mContentView = (ViewGroup)mInflater.inflate(R.layout.edit_contact, null);
- setContentView(mContentView);
-
- mLayout = (LinearLayout) findViewById(R.id.list);
- mNameView = (EditText) findViewById(R.id.name);
- mPhotoImageView = (ImageView) findViewById(R.id.photoImage);
- mPhotoImageView.setOnClickListener(this);
- mPhoneticNameView = (EditText) findViewById(R.id.phonetic_name);
-
- // Setup the bottom buttons
- View view = findViewById(R.id.saveButton);
- view.setOnClickListener(this);
- view = findViewById(R.id.discardButton);
- view.setOnClickListener(this);
-
- // Resolve the intent
- mState = STATE_UNKNOWN;
- Intent intent = getIntent();
- String action = intent.getAction();
- mUri = intent.getData();
- mAggDataUri = Uri.withAppendedPath(mUri, "data");
- if (mUri != null) {
- if (action.equals(Intent.ACTION_EDIT)) {
- if (icicle == null) {
- // Build the entries & views
- buildEntriesForEdit(getIntent().getExtras());
- buildViews();
- }
- setTitle(R.string.editContact_title_edit);
- mState = STATE_EDIT;
- } else if (action.equals(Intent.ACTION_INSERT)) {
- if (icicle == null) {
- // Build the entries & views
- /*
- buildEntriesForInsert(getIntent().getExtras());
- buildViews();
- */
- }
- setTitle(R.string.editContact_title_insert);
- mState = STATE_INSERT;
- mInsert = true;
- }
- }
-
- if (mState == STATE_UNKNOWN) {
- Log.e(TAG, "Cannot resolve intent: " + intent);
- finish();
- return;
- }
-
- if (mState == STATE_EDIT) {
- setTitle(getResources().getText(R.string.editContact_title_edit));
- } else {
- setTitle(getResources().getText(R.string.editContact_title_insert));
- }
- }
-
- private void setupSections() {
- mSections.add(mPhoneEntries);
- mSections.add(mEmailEntries);
- mSections.add(mImEntries);
- mSections.add(mPostalEntries);
- mSections.add(mOrgEntries);
- mSections.add(mNoteEntries);
- mSections.add(mOtherEntries);
- }
-
- @Override
- protected void onSaveInstanceState(Bundle outState) {
-
- // To store current focus between config changes, follow focus down the
- // view tree, keeping track of any parents with EditEntry tags
- View focusedChild = mContentView.getFocusedChild();
- EditEntry focusedEntry = null;
- while (focusedChild != null) {
- Object tag = focusedChild.getTag();
- if (tag instanceof EditEntry) {
- focusedEntry = (EditEntry) tag;
- }
-
- // Keep going deeper until child isn't a group
- if (focusedChild instanceof ViewGroup) {
- View deeperFocus = ((ViewGroup) focusedChild).getFocusedChild();
- if (deeperFocus != null) {
- focusedChild = deeperFocus;
- } else {
- break;
- }
- } else {
- break;
- }
- }
-
- if (focusedChild != null) {
- int requestFocusId = focusedChild.getId();
- int requestCursor = 0;
- if (focusedChild instanceof EditText) {
- requestCursor = ((EditText) focusedChild).getSelectionStart();
- }
-
- // Store focus values in EditEntry if found, otherwise store as
- // generic values
- if (focusedEntry != null) {
- focusedEntry.requestFocusId = requestFocusId;
- focusedEntry.requestCursor = requestCursor;
- } else {
- outState.putInt("requestFocusId", requestFocusId);
- outState.putInt("requestCursor", requestCursor);
- }
- }
-
- outState.putParcelableArrayList("phoneEntries", mPhoneEntries);
- outState.putParcelableArrayList("emailEntries", mEmailEntries);
- outState.putParcelableArrayList("imEntries", mImEntries);
- outState.putParcelableArrayList("postalEntries", mPostalEntries);
- outState.putParcelableArrayList("orgEntries", mOrgEntries);
- outState.putParcelableArrayList("noteEntries", mNoteEntries);
- outState.putParcelableArrayList("otherEntries", mOtherEntries);
- outState.putInt("state", mState);
- outState.putBoolean("insert", mInsert);
- outState.putParcelable("uri", mUri);
- outState.putString("name", mNameView.getText().toString());
- outState.putParcelable("photo", mPhoto);
- outState.putBoolean("photoChanged", mPhotoChanged);
- outState.putString("phoneticName", mPhoneticNameView.getText().toString());
- outState.putBoolean("contactChanged", mContactChanged);
- }
-
- @Override
- protected void onRestoreInstanceState(Bundle inState) {
- mPhoneEntries = inState.getParcelableArrayList("phoneEntries");
- mEmailEntries = inState.getParcelableArrayList("emailEntries");
- mImEntries = inState.getParcelableArrayList("imEntries");
- mPostalEntries = inState.getParcelableArrayList("postalEntries");
- mOrgEntries = inState.getParcelableArrayList("orgEntries");
- mNoteEntries = inState.getParcelableArrayList("noteEntries");
- mOtherEntries = inState.getParcelableArrayList("otherEntries");
- setupSections();
-
- mState = inState.getInt("state");
- mInsert = inState.getBoolean("insert");
- mUri = inState.getParcelable("uri");
- mNameView.setText(inState.getString("name"));
- mPhoto = inState.getParcelable("photo");
- if (mPhoto != null) {
- mPhotoImageView.setImageBitmap(mPhoto);
- setPhotoPresent(true);
- } else {
- mPhotoImageView.setImageResource(R.drawable.ic_contact_picture);
- setPhotoPresent(false);
- }
- mPhotoChanged = inState.getBoolean("photoChanged");
- mPhoneticNameView.setText(inState.getString("phoneticName"));
- mContactChanged = inState.getBoolean("contactChanged");
-
- // Now that everything is restored, build the view
- buildViews();
-
- // Try restoring any generally requested focus
- int requestFocusId = inState.getInt("requestFocusId", View.NO_ID);
- View focusedChild = mContentView.findViewById(requestFocusId);
- if (focusedChild != null) {
- focusedChild.requestFocus();
- if (focusedChild instanceof EditText) {
- int requestCursor = inState.getInt("requestCursor", 0);
- ((EditText) focusedChild).setSelection(requestCursor);
- }
- }
- }
-
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (resultCode != RESULT_OK) {
- return;
- }
-
- switch (requestCode) {
- case PHOTO_PICKED_WITH_DATA: {
- final Bundle extras = data.getExtras();
- if (extras != null) {
- Bitmap photo = extras.getParcelable("data");
- mPhoto = photo;
- mPhotoChanged = true;
- mPhotoImageView.setImageBitmap(photo);
- setPhotoPresent(true);
- }
- break;
- }
- }
- }
-
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_BACK: {
- doSaveAction();
- return true;
- }
- }
- return super.onKeyDown(keyCode, event);
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- super.onCreateOptionsMenu(menu);
- menu.add(0, MENU_ITEM_SAVE, 0, R.string.menu_done)
- .setIcon(android.R.drawable.ic_menu_save)
- .setAlphabeticShortcut('\n');
- menu.add(0, MENU_ITEM_DONT_SAVE, 0, R.string.menu_doNotSave)
- .setIcon(android.R.drawable.ic_menu_close_clear_cancel)
- .setAlphabeticShortcut('q');
- if (!mInsert) {
- menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_deleteContact)
- .setIcon(android.R.drawable.ic_menu_delete);
- }
-
- mPhotoMenuItem = menu.add(0, MENU_ITEM_PHOTO, 0, null);
- // Updates the state of the menu item
- setPhotoPresent(mPhotoPresent);
-
- return true;
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case MENU_ITEM_SAVE:
- doSaveAction();
- return true;
-
- case MENU_ITEM_DONT_SAVE:
- doRevertAction();
- return true;
-
- case MENU_ITEM_DELETE:
- // Get confirmation
- showDialog(DELETE_CONFIRMATION_DIALOG);
- return true;
-
- case MENU_ITEM_PHOTO:
- if (!mPhotoPresent) {
- doPickPhotoAction();
- } else {
- doRemovePhotoAction();
- }
- return true;
- }
-
- return false;
- }
-
- /**
- * Try guessing the next-best type of {@link EditEntry} to insert into the
- * given list. We walk down the precedence list until we find a type that
- * doesn't exist yet, or default to the lowest ranking type.
- */
- /*
- private int guessNextType(ArrayList<EditEntry> entries, int[] precedenceList) {
- // Keep track of the types we've seen already
- SparseBooleanArray existAlready = new SparseBooleanArray(entries.size());
- for (int i = entries.size() - 1; i >= 0; i--) {
- EditEntry entry = entries.get(i);
- if (!entry.isDeleted) {
- existAlready.put(entry.type, true);
- }
- }
-
- // Pick the first item we haven't seen
- for (int type : precedenceList) {
- if (!existAlready.get(type, false)) {
- return type;
- }
- }
-
- // Otherwise default to last item
- return precedenceList[precedenceList.length - 1];
- }
-
- // TODO When this gets brought back we'll need to use the new TypePrecedence class instead of
- // the older local TYPE_PRECEDENCE* contstants.
- private void doAddAction(int sectionType) {
- EditEntry entry = null;
- switch (sectionType) {
- case SECTION_PHONES: {
- // Try figuring out which type to insert next
- int nextType = guessNextType(mPhoneEntries, TYPE_PRECEDENCE_PHONES);
- entry = EditEntry.newPhoneEntry(EditContactActivity.this, Data.CONTENT_URI,
- nextType);
- mPhoneEntries.add(entry);
- break;
- }
- case SECTION_EMAIL: {
- // Try figuring out which type to insert next
- int nextType = guessNextType(mEmailEntries, TYPE_PRECEDENCE_EMAIL);
- entry = EditEntry.newEmailEntry(EditContactActivity.this, Data.CONTENT_URI,
- nextType);
- mEmailEntries.add(entry);
- break;
- }
- case SECTION_IM: {
- // Try figuring out which type to insert next
- int nextType = guessNextType(mImEntries, TYPE_PRECEDENCE_IM);
- entry = EditEntry.newImEntry(EditContactActivity.this, Data.CONTENT_URI, nextType);
- mImEntries.add(entry);
- break;
- }
- case SECTION_POSTAL: {
- int nextType = guessNextType(mPostalEntries, TYPE_PRECEDENCE_POSTAL);
- entry = EditEntry.newPostalEntry(EditContactActivity.this, Data.CONTENT_URI,
- nextType);
- mPostalEntries.add(entry);
- break;
- }
- case SECTION_ORG: {
- int nextType = guessNextType(mOrgEntries, TYPE_PRECEDENCE_ORG);
- entry = EditEntry.newOrganizationEntry(EditContactActivity.this, Data.CONTENT_URI,
- nextType);
- mOrgEntries.add(entry);
- break;
- }
- case SECTION_NOTE: {
- entry = EditEntry.newNotesEntry(EditContactActivity.this, Data.CONTENT_URI);
- mNoteEntries.add(entry);
- break;
- }
- }
-
- // Rebuild the views if needed
- if (entry != null) {
- buildViews();
- mContactChanged = true;
-
- View dataView = entry.view.findViewById(R.id.data);
- if (dataView == null) {
- entry.view.requestFocus();
- } else {
- dataView.requestFocus();
- }
- }
- }
- */
-
- private void doRevertAction() {
- finish();
- }
-
- private void doPickPhotoAction() {
- Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
- // TODO: get these values from constants somewhere
- intent.setType("image/*");
- intent.putExtra("crop", "true");
- intent.putExtra("aspectX", 1);
- intent.putExtra("aspectY", 1);
- intent.putExtra("outputX", 96);
- intent.putExtra("outputY", 96);
- try {
- intent.putExtra("return-data", true);
- 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();
- }
- }
-
- private void doRemovePhotoAction() {
- mPhoto = null;
- mPhotoChanged = true;
- setPhotoPresent(false);
- }
-
- /*
- private void populateGroups() {
- // Create a list of all the groups
- Cursor cursor = mResolver.query(Groups.CONTENT_URI, ContactsListActivity.GROUPS_PROJECTION,
- null, null, Groups.DEFAULT_SORT_ORDER);
- try {
- ArrayList<Long> ids = new ArrayList<Long>();
- ArrayList<String> items = new ArrayList<String>();
-
- while (cursor.moveToNext()) {
- String systemId = cursor.getString(ContactsListActivity.GROUPS_COLUMN_INDEX_SYSTEM_ID);
- String name = cursor.getString(ContactsListActivity.GROUPS_COLUMN_INDEX_NAME);
-
- if (systemId != null || Groups.GROUP_MY_CONTACTS.equals(systemId)) {
- continue;
- }
-
- if (!TextUtils.isEmpty(name)) {
- ids.add(new Long(cursor.getLong(ContactsListActivity.GROUPS_COLUMN_INDEX_SYSTEM_ID)));
- items.add(name);
- }
- }
-
- mGroups = items.toArray(new CharSequence[items.size()]);
- mInTheGroup = new boolean[items.size()];
- } finally {
- cursor.close();
- }
-
- if (mGroups != null) {
-
- // Go through the groups for this member and update the list
- final Uri groupsUri = Uri.withAppendedPath(mUri, GroupMembership.CONTENT_DIRECTORY);
- Cursor groupCursor = null;
- try {
- groupCursor = mResolver.query(groupsUri, ContactsListActivity.GROUPS_PROJECTION,
- null, null, Groups.DEFAULT_SORT_ORDER);
- } catch (IllegalArgumentException e) {
- // Contact is new, so we don't need to do any work.
- }
-
- if (groupCursor != null) {
- try {
- while (groupCursor.moveToNext()) {
- String systemId = groupCursor.getString(ContactsListActivity.GROUPS_COLUMN_INDEX_SYSTEM_ID);
- String name = groupCursor.getString(ContactsListActivity.GROUPS_COLUMN_INDEX_NAME);
-
- if (systemId != null || Groups.GROUP_MY_CONTACTS.equals(systemId)) {
- continue;
- }
-
- if (!TextUtils.isEmpty(name)) {
- for (int i = 0; i < mGroups.length; i++) {
- if (name.equals(mGroups[i])) {
- mInTheGroup[i] = true;
- break;
- }
- }
- }
- }
- } finally {
- groupCursor.close();
- }
- }
- }
- }
-
- private String generateGroupList() {
- StringBuilder groupList = new StringBuilder();
- for (int i = 0; mGroups != null && i < mGroups.length; i++) {
- if (mInTheGroup[i]) {
- if (groupList.length() == 0) {
- groupList.append(mGroups[i]);
- } else {
- groupList.append(getString(R.string.group_list, mGroups[i]));
- }
- }
- }
- return groupList.length() > 0 ? groupList.toString() : null;
- }
-
- private void doPickGroup(EditEntry entry) {
- if (mGroups != null) {
- GroupDialogListener listener = new GroupDialogListener(this, entry);
-
- new AlertDialog.Builder(EditContactActivity.this)
- .setTitle(R.string.label_groups)
- .setMultiChoiceItems(mGroups, mInTheGroup, listener)
- .setPositiveButton(android.R.string.ok, listener)
- .setNegativeButton(android.R.string.cancel, null)
- .show();
- }
- }
- */
-
- /** Handles the clicks in the groups dialog */
- /*
- private static final class GroupDialogListener implements DialogInterface.OnClickListener,
- DialogInterface.OnMultiChoiceClickListener {
-
- private EditContactActivity mEditContactActivity;
- private EditEntry mEntry;
- private boolean[] mInTheGroup;
-
- public GroupDialogListener(EditContactActivity editContactActivity, EditEntry entry) {
- mEditContactActivity = editContactActivity;
- mEntry = entry;
- mInTheGroup = editContactActivity.mInTheGroup.clone();
- }
-
- // Called when the dialog's ok button is clicked
- public void onClick(DialogInterface dialog, int which) {
- mEditContactActivity.mInTheGroup = mInTheGroup;
- mEntry.data = mEditContactActivity.generateGroupList();
- mEditContactActivity.updateDataView(mEntry, mEntry.data);
- }
-
- // Called when each group is clicked
- public void onClick(DialogInterface dialog, int which, boolean isChecked) {
- mInTheGroup[which] = isChecked;
- }
- }
- */
-
- private void updateDataView(EditEntry entry, String text) {
- TextView dataView = (TextView) entry.view.findViewById(R.id.data);
- dataView.setText(text);
- }
-
- @Override
- protected Dialog onCreateDialog(int id) {
- switch (id) {
- case DELETE_CONFIRMATION_DIALOG:
- return new AlertDialog.Builder(EditContactActivity.this)
- .setTitle(R.string.deleteConfirmation_title)
- .setIcon(android.R.drawable.ic_dialog_alert)
- .setMessage(R.string.deleteConfirmation)
- .setNegativeButton(android.R.string.cancel, null)
- .setPositiveButton(android.R.string.ok, mDeleteContactDialogListener)
- .setCancelable(false)
- .create();
- }
- return super.onCreateDialog(id);
- }
-
- static String[] getLabelsForMimetype(Context context, String mimetype) {
- final Resources resources = context.getResources();
- if (mimetype.equals(Phone.CONTENT_ITEM_TYPE)) {
- return resources.getStringArray(android.R.array.phoneTypes);
- } else if (mimetype.equals(Email.CONTENT_ITEM_TYPE)) {
- return resources.getStringArray(android.R.array.emailAddressTypes);
- } else if (mimetype.equals(StructuredPostal.CONTENT_ITEM_TYPE)) {
- return resources.getStringArray(android.R.array.postalAddressTypes);
- } else if (mimetype.equals(Im.CONTENT_ITEM_TYPE)) {
- return resources.getStringArray(android.R.array.imProtocols);
- } else if (mimetype.equals(Organization.CONTENT_ITEM_TYPE)) {
- return resources.getStringArray(android.R.array.organizationTypes);
- } else {
- return resources.getStringArray(R.array.otherLabels);
- }
- }
-
- int getTypeFromLabelPosition(CharSequence[] labels, int labelPosition) {
- // In the UI Custom... comes last, but it is uses the constant 0
- // so it is in the same location across the various kinds. Fix up the
- // position to a valid type here.
- if (labelPosition == labels.length - 1) {
- return BaseTypes.TYPE_CUSTOM;
- } else {
- return labelPosition + 1;
- }
- }
-
- private EditEntry getOtherEntry(String column) {
- for (int i = mOtherEntries.size() - 1; i >= 0; i--) {
- EditEntry entry = mOtherEntries.get(i);
- if (isOtherEntry(entry, column)) {
- return entry;
- }
- }
- return null;
- }
-
- private static boolean isOtherEntry(EditEntry entry, String column) {
- return entry != null && entry.column != null && entry.column.equals(column);
- }
-
- private void createCustomPicker(final EditEntry entry, final ArrayList<EditEntry> addTo) {
- final EditText label = new EditText(this);
- label.setKeyListener(TextKeyListener.getInstance(false, Capitalize.WORDS));
- label.requestFocus();
- new AlertDialog.Builder(this)
- .setView(label)
- .setTitle(R.string.customLabelPickerTitle)
- .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int which) {
- entry.setLabel(EditContactActivity.this, BaseTypes.TYPE_CUSTOM,
- label.getText().toString());
- mContactChanged = true;
-
- if (addTo != null) {
- addTo.add(entry);
- buildViews();
- entry.view.requestFocus(View.FOCUS_DOWN);
- }
- }
- })
- .setNegativeButton(android.R.string.cancel, null)
- .show();
- }
-
- /**
- * Saves or creates the contact based on the mode, and if sucessful finishes the activity.
- */
- private void doSaveAction() {
- // Save or create the contact if needed
- switch (mState) {
- case STATE_EDIT:
- save();
- break;
-
- /*
- case STATE_INSERT:
- create();
- break;
- */
-
- default:
- Log.e(TAG, "Unknown state in doSaveOrCreate: " + mState);
- break;
- }
- finish();
- }
-
- /**
- * Gets the group id based on group name.
- *
- * @param resolver the resolver to use
- * @param groupName the name of the group to add the contact to
- * @return the id of the group
- * @throws IllegalStateException if the group can't be found
- */
- /*
- private long getGroupId(ContentResolver resolver, String groupName) {
- long groupId = 0;
- Cursor groupsCursor = resolver.query(Groups.CONTENT_URI, GROUP_ID_PROJECTION,
- Groups.NAME + "=?", new String[] { groupName }, null);
- if (groupsCursor != null) {
- try {
- if (groupsCursor.moveToFirst()) {
- groupId = groupsCursor.getLong(0);
- }
- } finally {
- groupsCursor.close();
- }
- }
-
- if (groupId == 0) {
- throw new IllegalStateException("Failed to find the " + groupName + "group");
- }
-
- return groupId;
- }
- */
-
- /**
- * Deletes group membership based on person and group ids.
- *
- * @param personId the person id
- * @param groupId the group id
- * @return the id of the group membership
- */
- /*
- private void deleteGroupMembership(long personId, long groupId) {
- long groupMembershipId = 0;
- Cursor groupsCursor = mResolver.query(GroupMembership.CONTENT_URI, GROUPMEMBERSHIP_ID_PROJECTION,
- GroupMembership.PERSON_ID + "=? AND " + GroupMembership.GROUP_ID + "=?",
- new String[] {String.valueOf(personId), String.valueOf(groupId)}, null);
- if (groupsCursor != null) {
- try {
- if (groupsCursor.moveToFirst()) {
- groupMembershipId = groupsCursor.getLong(0);
- }
- } finally {
- groupsCursor.close();
- }
- }
-
- if (groupMembershipId != 0) {
- final Uri groupsUri = ContentUris.withAppendedId(
- GroupMembership.CONTENT_URI,groupMembershipId);
- mResolver.delete(groupsUri, null, null);
- }
- }
- */
-
- /**
- * Save the various fields to the existing contact.
- */
- private void save() {
- ContentValues values = new ContentValues();
- String data;
- int numValues = 0;
-
- // Handle the name and send to voicemail specially
- final String name = mNameView.getText().toString();
- if (name != null && TextUtils.isGraphic(name)) {
- numValues++;
- }
-
- values.put(StructuredName.DISPLAY_NAME, name);
- /*
- values.put(People.PHONETIC_NAME, mPhoneticNameView.getText().toString());
- */
- mResolver.update(mStructuredNameUri, values, null, null);
-
- // This will go down in for loop somewhere
- if (mPhotoChanged) {
- // Only write the photo if it's changed, since we don't initially load mPhoto
- values.clear();
- if (mPhoto != null) {
- ByteArrayOutputStream stream = new ByteArrayOutputStream();
- mPhoto.compress(Bitmap.CompressFormat.JPEG, 75, stream);
- values.put(Photo.PHOTO, stream.toByteArray());
- mResolver.update(mPhotoDataUri, values, null, null);
- } else {
- values.putNull(Photo.PHOTO);
- mResolver.update(mPhotoDataUri, values, null, null);
- }
- }
-
- int entryCount = ContactEntryAdapter.countEntries(mSections, false);
- for (int i = 0; i < entryCount; i++) {
- EditEntry entry = ContactEntryAdapter.getEntry(mSections, i, false);
- data = entry.getData();
- boolean empty = data == null || !TextUtils.isGraphic(data);
- /*
- if (kind == EditEntry.KIND_GROUP) {
- if (entry.id != 0) {
- for (int g = 0; g < mGroups.length; g++) {
- long groupId = getGroupId(mResolver, mGroups[g].toString());
- if (mInTheGroup[g]) {
- Contacts.People.addToGroup(mResolver, entry.id, groupId);
- numValues++;
- } else {
- deleteGroupMembership(entry.id, groupId);
- }
- }
- }
- }
- */
- if (!empty) {
- values.clear();
- entry.toValues(values);
- if (entry.id != 0) {
- mResolver.update(entry.uri, values, null, null);
- } else {
- /* mResolver.insert(entry.uri, values); */
- }
- } else if (entry.id != 0) {
- mResolver.delete(entry.uri, null, null);
- }
- }
-
- /*
- if (numValues == 0) {
- // The contact is completely empty, delete it
- mResolver.delete(mUri, null, null);
- mUri = null;
- setResult(RESULT_CANCELED);
- } else {
- // Add the entry to the my contacts group if it isn't there already
- People.addToMyContactsGroup(mResolver, ContentUris.parseId(mUri));
- setResult(RESULT_OK, new Intent().setData(mUri));
-
- // Only notify user if we actually changed contact
- if (mContactChanged || mPhotoChanged) {
- Toast.makeText(this, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
- }
- }
- */
- }
-
- /**
- * Takes the entered data and saves it to a new contact.
- */
- /*
- private void create() {
- ContentValues values = new ContentValues();
- String data;
- int numValues = 0;
-
- // Create the contact itself
- final String name = mNameView.getText().toString();
- if (name != null && TextUtils.isGraphic(name)) {
- numValues++;
- }
- values.put(People.NAME, name);
- values.put(People.PHONETIC_NAME, mPhoneticNameView.getText().toString());
-
- // Add the contact to the My Contacts group
- Uri contactUri = People.createPersonInMyContactsGroup(mResolver, values);
-
- // Add the contact to the group that is being displayed in the contact list
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
- int displayType = prefs.getInt(ContactsListActivity.PREF_DISPLAY_TYPE,
- ContactsListActivity.DISPLAY_TYPE_UNKNOWN);
- if (displayType == ContactsListActivity.DISPLAY_TYPE_USER_GROUP) {
- String displayGroup = prefs.getString(ContactsListActivity.PREF_DISPLAY_INFO,
- null);
- if (!TextUtils.isEmpty(displayGroup)) {
- People.addToGroup(mResolver, ContentUris.parseId(contactUri), displayGroup);
- }
- } else {
- // Check to see if we're not syncing everything and if so if My Contacts is synced.
- // If it isn't then the created contact can end up not in any groups that are
- // currently synced and end up getting removed from the phone, which is really bad.
- boolean syncingEverything = !"0".equals(Contacts.Settings.getSetting(mResolver, null,
- Contacts.Settings.SYNC_EVERYTHING));
- if (!syncingEverything) {
- boolean syncingMyContacts = false;
- Cursor c = mResolver.query(Groups.CONTENT_URI, new String[] { Groups.SHOULD_SYNC },
- Groups.SYSTEM_ID + "=?", new String[] { Groups.GROUP_MY_CONTACTS }, null);
- if (c != null) {
- try {
- if (c.moveToFirst()) {
- syncingMyContacts = !"0".equals(c.getString(0));
- }
- } finally {
- c.close();
- }
- }
-
- if (!syncingMyContacts) {
- // Not syncing My Contacts, so find a group that is being synced and stick
- // the contact in there. We sort the list so at least all contacts
- // will appear in the same group.
- c = mResolver.query(Groups.CONTENT_URI, new String[] { Groups._ID },
- Groups.SHOULD_SYNC + "!=0", null, Groups.DEFAULT_SORT_ORDER);
- if (c != null) {
- try {
- if (c.moveToFirst()) {
- People.addToGroup(mResolver, ContentUris.parseId(contactUri),
- c.getLong(0));
- }
- } finally {
- c.close();
- }
- }
- }
- }
- }
-
- // Handle the photo
- if (mPhoto != null) {
- ByteArrayOutputStream stream = new ByteArrayOutputStream();
- mPhoto.compress(Bitmap.CompressFormat.JPEG, 75, stream);
- Contacts.People.setPhotoData(getContentResolver(), contactUri, stream.toByteArray());
- }
-
- // Create the contact methods
- int entryCount = ContactEntryAdapter.countEntries(mSections, false);
- for (int i = 0; i < entryCount; i++) {
- EditEntry entry = ContactEntryAdapter.getEntry(mSections, i, false);
- if (entry.kind == EditEntry.KIND_GROUP) {
- long contactId = ContentUris.parseId(contactUri);
- for (int g = 0; g < mGroups.length; g++) {
- if (mInTheGroup[g]) {
- long groupId = getGroupId(mResolver, mGroups[g].toString());
- People.addToGroup(mResolver, contactId, groupId);
- numValues++;
- }
- }
- } else if (entry.kind != EditEntry.KIND_CONTACT) {
- values.clear();
- if (entry.toValues(values)) {
- // Only create the entry if there is data
- entry.uri = mResolver.insert(
- Uri.withAppendedPath(contactUri, entry.contentDirectory), values);
- entry.id = ContentUris.parseId(entry.uri);
- }
- } else {
- // Update the contact with any straggling data, like notes
- data = entry.getData();
- values.clear();
- if (data != null && TextUtils.isGraphic(data)) {
- values.put(entry.column, data);
- mResolver.update(contactUri, values, null, null);
- }
- }
- }
-
- if (numValues == 0) {
- mResolver.delete(contactUri, null, null);
- setResult(RESULT_CANCELED);
- } else {
- mUri = contactUri;
- Intent resultIntent = new Intent()
- .setData(mUri)
- .putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
- setResult(RESULT_OK, resultIntent);
- Toast.makeText(this, R.string.contactCreatedToast, Toast.LENGTH_SHORT).show();
- }
- }
- */
-
- /**
- * Build up the entries to display on the screen.
- *
- * @param extras the extras used to start this activity, may be null
- */
- private void buildEntriesForEdit(Bundle extras) {
- Cursor aggCursor = mResolver.query(mAggDataUri, CONTACT_PROJECTION, null, null, null);
- if (aggCursor == null) {
- Log.e(TAG, "invalid contact uri: " + mUri);
- finish();
- return;
- } else if (!aggCursor.moveToFirst()) {
- Log.e(TAG, "invalid contact uri: " + mUri);
- finish();
- aggCursor.close();
- return;
- }
-
- // Clear out the old entries
- int numSections = mSections.size();
- for (int i = 0; i < numSections; i++) {
- mSections.get(i).clear();
- }
-
- EditEntry entry;
-
- while (aggCursor.moveToNext()) {
- final String mimetype = aggCursor.getString(DATA_MIMETYPE_COLUMN);
- boolean isSuperPrimary = aggCursor.getLong(DATA_IS_SUPER_PRIMARY_COLUMN) != 0;
-
- final long id = aggCursor.getLong(DATA_ID_COLUMN);
- final Uri uri = ContentUris.withAppendedId(Data.CONTENT_URI, id);
-
- if (mimetype.equals(CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)) {
- mNameView.setText(aggCursor.getString(DATA_9_COLUMN));
- mNameView.addTextChangedListener(this);
- mStructuredNameUri = uri;
- } else if (mimetype.equals(CommonDataKinds.Photo.CONTENT_ITEM_TYPE)) {
- mPhoto = ContactsUtils.loadContactPhoto(aggCursor, DATA_1_COLUMN, null);
- if (mPhoto == null) {
- setPhotoPresent(false);
- } else {
- setPhotoPresent(true);
- mPhotoImageView.setImageBitmap(mPhoto);
- }
- mPhotoDataUri = uri;
- } else if (mimetype.equals(CommonDataKinds.Organization.CONTENT_ITEM_TYPE)) {
- int type = aggCursor.getInt(DATA_1_COLUMN);
- String label = aggCursor.getString(DATA_2_COLUMN);
- String company = aggCursor.getString(DATA_3_COLUMN);
- String title = aggCursor.getString(DATA_4_COLUMN);
-
- entry = EditEntry.newOrganizationEntry(this, label, type, company, title, uri, id);
- entry.isPrimary = aggCursor.getLong(DATA_IS_SUPER_PRIMARY_COLUMN) != 0;
- mOrgEntries.add(entry);
- } else if (mimetype.equals(CommonDataKinds.Note.CONTENT_ITEM_TYPE)) {
- entry = EditEntry.newNotesEntry(this, aggCursor.getString(DATA_1_COLUMN),
- uri, id);
- mNoteEntries.add(entry);
- } else if (mimetype.equals(CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
- || mimetype.equals(CommonDataKinds.Email.CONTENT_ITEM_TYPE)
- || mimetype.equals(CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE)
- || mimetype.equals(CommonDataKinds.Im.CONTENT_ITEM_TYPE)) {
- int type = aggCursor.getInt(DATA_1_COLUMN);
- String data = aggCursor.getString(DATA_2_COLUMN);
- String label = aggCursor.getString(DATA_3_COLUMN);
-
- if (mimetype.equals(CommonDataKinds.Phone.CONTENT_ITEM_TYPE)) {
- // Add a phone number entry
- entry = EditEntry.newPhoneEntry(this, label, type, data, uri, id);
- entry.isPrimary = isSuperPrimary;
- mPhoneEntries.add(entry);
-
- // Keep track of which primary types have been added
- if (type == Phone.TYPE_MOBILE) {
- mMobilePhoneAdded = true;
- }
- } else if (mimetype.equals(CommonDataKinds.Email.CONTENT_ITEM_TYPE)) {
- entry = EditEntry.newEmailEntry(this, label, type, data, uri, id);
- entry.isPrimary = isSuperPrimary;
- mEmailEntries.add(entry);
-
- if (isSuperPrimary) {
- mPrimaryEmailAdded = true;
- }
- } else if (mimetype.equals(CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE)) {
- entry = EditEntry.newPostalEntry(this, label, type, data, uri, id);
- entry.isPrimary = isSuperPrimary;
- mPostalEntries.add(entry);
- } else if (mimetype.equals(CommonDataKinds.Im.CONTENT_ITEM_TYPE)) {
- String protocolStr = aggCursor.getString(DATA_5_COLUMN);
- Object protocolObj = ContactsUtils.decodeImProtocol(protocolStr);
- if (protocolObj == null) {
- // Invalid IM protocol, log it then ignore.
- Log.e(TAG, "Couldn't decode IM protocol: " + protocolStr);
- continue;
- } else {
- if (protocolObj instanceof Number) {
- int protocol = ((Number) protocolObj).intValue();
- entry = EditEntry.newImEntry(this,
- getLabelsForMimetype(this, mimetype)[protocol], protocol,
- data, uri, id);
- } else {
- entry = EditEntry.newImEntry(this, protocolObj.toString(), -1, data,
- uri, id);
- }
- mImEntries.add(entry);
- }
- }
- }
- }
-
- /*
- // Groups
- populateGroups();
- if (mGroups != null) {
- entry = EditEntry.newGroupEntry(this, generateGroupList(), mUri,
- personCursor.getLong(0));
- mOtherEntries.add(entry);
- }
-
- // Phonetic name
- mPhoneticNameView.setText(personCursor.getString(CONTACT_PHONETIC_NAME_COLUMN));
- mPhoneticNameView.addTextChangedListener(this);
-
-
- // Add values from the extras, if there are any
- if (extras != null) {
- addFromExtras(extras, phonesUri, methodsUri);
- }
-
- // Add the base types if needed
- if (!mMobilePhoneAdded) {
- entry = EditEntry.newPhoneEntry(this,
- Uri.withAppendedPath(mUri, People.Phones.CONTENT_DIRECTORY),
- DEFAULT_PHONE_TYPE);
- mPhoneEntries.add(entry);
- }
-
- if (!mPrimaryEmailAdded) {
- entry = EditEntry.newEmailEntry(this,
- Uri.withAppendedPath(mUri, People.ContactMethods.CONTENT_DIRECTORY),
- DEFAULT_EMAIL_TYPE);
- entry.isPrimary = true;
- mEmailEntries.add(entry);
- }
- */
-
- mContactChanged = false;
- }
-
- /**
- * Build the list of EditEntries for full mode insertions.
- *
- * @param extras the extras used to start this activity, may be null
- */
- /*
- private void buildEntriesForInsert(Bundle extras) {
- // Clear out the old entries
- int numSections = mSections.size();
- for (int i = 0; i < numSections; i++) {
- mSections.get(i).clear();
- }
-
- EditEntry entry;
-
- // Check the intent extras
- if (extras != null) {
- addFromExtras(extras, null, null);
- }
-
- // Photo
- mPhotoImageView.setImageResource(R.drawable.ic_contact_picture);
-
- // Add the base entries if they're not already present
- if (!mMobilePhoneAdded) {
- entry = EditEntry.newPhoneEntry(this, null, Phone.TYPE_MOBILE);
- entry.isPrimary = true;
- mPhoneEntries.add(entry);
- }
-
- if (!mPrimaryEmailAdded) {
- entry = EditEntry.newEmailEntry(this, null, DEFAULT_EMAIL_TYPE);
- entry.isPrimary = true;
- mEmailEntries.add(entry);
- }
-
- // Group
- populateGroups();
- if (mGroups != null) {
- entry = EditEntry.newGroupEntry(this, null, mUri, 0);
- mOtherEntries.add(entry);
- }
- }
-
- private void addFromExtras(Bundle extras, Uri phonesUri, Uri methodsUri) {
- EditEntry entry;
-
- // Read the name from the bundle
- CharSequence name = extras.getCharSequence(Insert.NAME);
- if (name != null && TextUtils.isGraphic(name)) {
- mNameView.setText(name);
- }
-
- // Read the phonetic name from the bundle
- CharSequence phoneticName = extras.getCharSequence(Insert.PHONETIC_NAME);
- if (!TextUtils.isEmpty(phoneticName)) {
- mPhoneticNameView.setText(phoneticName);
- }
-
- // Postal entries from extras
- CharSequence postal = extras.getCharSequence(Insert.POSTAL);
- int postalType = extras.getInt(Insert.POSTAL_TYPE, INVALID_TYPE);
- if (!TextUtils.isEmpty(postal) && postalType == INVALID_TYPE) {
- postalType = DEFAULT_POSTAL_TYPE;
- }
-
- if (postalType != INVALID_TYPE) {
- entry = EditEntry.newPostalEntry(this, null, postalType, postal.toString(),
- methodsUri, 0);
- entry.isPrimary = extras.getBoolean(Insert.POSTAL_ISPRIMARY);
- mPostalEntries.add(entry);
- }
-
- // Email entries from extras
- addEmailFromExtras(extras, methodsUri, Insert.EMAIL, Insert.EMAIL_TYPE,
- Insert.EMAIL_ISPRIMARY);
- addEmailFromExtras(extras, methodsUri, Insert.SECONDARY_EMAIL, Insert.SECONDARY_EMAIL_TYPE,
- null);
- addEmailFromExtras(extras, methodsUri, Insert.TERTIARY_EMAIL, Insert.TERTIARY_EMAIL_TYPE,
- null);
-
- // Phone entries from extras
- addPhoneFromExtras(extras, phonesUri, Insert.PHONE, Insert.PHONE_TYPE,
- Insert.PHONE_ISPRIMARY);
- addPhoneFromExtras(extras, phonesUri, Insert.SECONDARY_PHONE, Insert.SECONDARY_PHONE_TYPE,
- null);
- addPhoneFromExtras(extras, phonesUri, Insert.TERTIARY_PHONE, Insert.TERTIARY_PHONE_TYPE,
- null);
-
- // IM entries from extras
- CharSequence imHandle = extras.getCharSequence(Insert.IM_HANDLE);
- CharSequence imProtocol = extras.getCharSequence(Insert.IM_PROTOCOL);
-
- if (imHandle != null && imProtocol != null) {
- Object protocolObj = ContactMethods.decodeImProtocol(imProtocol.toString());
- if (protocolObj instanceof Number) {
- int protocol = ((Number) protocolObj).intValue();
- entry = EditEntry.newImEntry(this,
- getLabelsForKind(this, Contacts.KIND_IM)[protocol], protocol,
- imHandle.toString(), methodsUri, 0);
- } else {
- entry = EditEntry.newImEntry(this, protocolObj.toString(), -1, imHandle.toString(),
- methodsUri, 0);
- }
- entry.isPrimary = extras.getBoolean(Insert.IM_ISPRIMARY);
- mImEntries.add(entry);
- }
- }
-
- private void addEmailFromExtras(Bundle extras, Uri methodsUri, String emailField,
- String typeField, String primaryField) {
- CharSequence email = extras.getCharSequence(emailField);
-
- // Correctly handle String in typeField as TYPE_CUSTOM
- int emailType = INVALID_TYPE;
- String customLabel = null;
- if(extras.get(typeField) instanceof String) {
- emailType = ContactsContract.TYPE_CUSTOM;
- customLabel = extras.getString(typeField);
- } else {
- emailType = extras.getInt(typeField, INVALID_TYPE);
- }
-
- if (!TextUtils.isEmpty(email) && emailType == INVALID_TYPE) {
- emailType = DEFAULT_EMAIL_TYPE;
- mPrimaryEmailAdded = true;
- }
-
- if (emailType != INVALID_TYPE) {
- EditEntry entry = EditEntry.newEmailEntry(this, customLabel, emailType, email.toString(),
- methodsUri, 0);
- entry.isPrimary = (primaryField == null) ? false : extras.getBoolean(primaryField);
- mEmailEntries.add(entry);
-
- // Keep track of which primary types have been added
- if (entry.isPrimary) {
- mPrimaryEmailAdded = true;
- }
- }
- }
-
- private void addPhoneFromExtras(Bundle extras, Uri phonesUri, String phoneField,
- String typeField, String primaryField) {
- CharSequence phoneNumber = extras.getCharSequence(phoneField);
-
- // Correctly handle String in typeField as TYPE_CUSTOM
- int phoneType = INVALID_TYPE;
- String customLabel = null;
- if(extras.get(typeField) instanceof String) {
- phoneType = Phone.TYPE_CUSTOM;
- customLabel = extras.getString(typeField);
- } else {
- phoneType = extras.getInt(typeField, INVALID_TYPE);
- }
-
- if (!TextUtils.isEmpty(phoneNumber) && phoneType == INVALID_TYPE) {
- phoneType = DEFAULT_PHONE_TYPE;
- }
-
- if (phoneType != INVALID_TYPE) {
- EditEntry entry = EditEntry.newPhoneEntry(this, customLabel, phoneType,
- phoneNumber.toString(), phonesUri, 0);
- entry.isPrimary = (primaryField == null) ? false : extras.getBoolean(primaryField);
- mPhoneEntries.add(entry);
-
- // Keep track of which primary types have been added
- if (phoneType == Phone.TYPE_MOBILE) {
- mMobilePhoneAdded = true;
- }
- }
- }
- */
-
- /**
- * Removes all existing views, builds new ones for all the entries, and adds them.
- */
- private void buildViews() {
- // Remove existing views
- final LinearLayout layout = mLayout;
- layout.removeAllViews();
-
- buildViewsForSection(layout, mPhoneEntries,
- R.string.listSeparatorCallNumber_edit, SECTION_PHONES);
- buildViewsForSection(layout, mEmailEntries,
- R.string.listSeparatorSendEmail_edit, SECTION_EMAIL);
- buildViewsForSection(layout, mImEntries,
- R.string.listSeparatorSendIm_edit, SECTION_IM);
- buildViewsForSection(layout, mPostalEntries,
- R.string.listSeparatorMapAddress_edit, SECTION_POSTAL);
- buildViewsForSection(layout, mOrgEntries,
- R.string.listSeparatorOrganizations, SECTION_ORG);
- buildViewsForSection(layout, mNoteEntries,
- R.string.label_notes, SECTION_NOTE);
-
- buildOtherViews(layout, mOtherEntries);
- }
-
-
- /**
- * Builds the views for a specific section.
- *
- * @param layout the container
- * @param section the section to build the views for
- */
- private void buildViewsForSection(final LinearLayout layout, ArrayList<EditEntry> section,
- int separatorResource, int sectionType) {
-
- View divider = mInflater.inflate(R.layout.edit_divider, layout, false);
- layout.addView(divider);
-
- // Count up undeleted children
- int activeChildren = 0;
- for (int i = section.size() - 1; i >= 0; i--) {
- EditEntry entry = section.get(i);
- if (!entry.isDeleted) {
- activeChildren++;
- }
- }
-
- // Build the correct group header based on undeleted children
- ViewGroup header;
- if (activeChildren == 0) {
- header = (ViewGroup) mInflater.inflate(R.layout.edit_separator_alone, layout, false);
- } else {
- header = (ViewGroup) mInflater.inflate(R.layout.edit_separator, layout, false);
- }
-
- // Because we're emulating a ListView, we need to handle focus changes
- // with some additional logic.
- header.setOnFocusChangeListener(this);
-
- TextView text = (TextView) header.findViewById(R.id.text);
- text.setText(getText(separatorResource));
-
- // Force TextView to always default color if we have children. This makes sure
- // we don't change color when parent is pressed.
- if (activeChildren > 0) {
- ColorStateList stateList = text.getTextColors();
- text.setTextColor(stateList.getDefaultColor());
- }
-
- View addView = header.findViewById(R.id.separator);
- addView.setTag(Integer.valueOf(sectionType));
- addView.setOnClickListener(this);
-
- // Build views for the current section
- for (EditEntry entry : section) {
- entry.activity = this; // this could be null from when the state is restored
- if (!entry.isDeleted) {
- View view = buildViewForEntry(entry);
- header.addView(view);
- }
- }
-
- layout.addView(header);
- }
-
- private void buildOtherViews(final LinearLayout layout, ArrayList<EditEntry> section) {
- // Build views for the current section, putting a divider between each one
- for (EditEntry entry : section) {
- View divider = mInflater.inflate(R.layout.edit_divider, layout, false);
- layout.addView(divider);
-
- entry.activity = this; // this could be null from when the state is restored
- View view = buildViewForEntry(entry);
- view.setOnClickListener(this);
- layout.addView(view);
- }
-
- View divider = mInflater.inflate(R.layout.edit_divider, layout, false);
- layout.addView(divider);
- }
-
- /**
- * Builds a view to display an EditEntry.
- *
- * @param entry the entry to display
- * @return a view that will display the given entry
- */
- /* package */ View buildViewForEntry(final EditEntry entry) {
- // Look for any existing entered text, and save it if found
- if (entry.view != null && entry.syncDataWithView) {
- String enteredText = ((TextView) entry.view.findViewById(R.id.data))
- .getText().toString();
- if (!TextUtils.isEmpty(enteredText)) {
- entry.data = enteredText;
- }
- }
-
- // Build a new view
- final ViewGroup parent = mLayout;
- View view;
-
- // Because we're emulating a ListView, we might need to handle focus changes
- // with some additional logic.
- if (entry.mimetype.equals(Organization.CONTENT_ITEM_TYPE)) {
- view = mInflater.inflate(R.layout.edit_contact_entry_org, parent, false);
- /*
- else if (entry.mimetype.equals(Group.CONTENT_ITEM_TYPE)) {
- view = mInflater.inflate(R.layout.edit_contact_entry_group, parent, false);
- view.setOnFocusChangeListener(this);
- }
- */
- } else if (!entry.isStaticLabel) {
- view = mInflater.inflate(R.layout.edit_contact_entry, parent, false);
- } else {
- view = mInflater.inflate(R.layout.edit_contact_entry_static_label, parent, false);
- }
- entry.view = view;
-
- // Set the entry as the tag so we can find it again later given just the view
- view.setTag(entry);
-
- // Bind the label
- entry.bindLabel(this);
-
- // Bind data
- TextView data = (TextView) view.findViewById(R.id.data);
- TextView data2 = (TextView) view.findViewById(R.id.data2);
-
- if (data instanceof Button) {
- data.setOnClickListener(this);
- }
- if (data.length() == 0) {
- if (entry.syncDataWithView) {
- // If there is already data entered don't overwrite it
- data.setText(entry.data);
- } else {
- fillViewData(entry);
- }
- }
- if (data2 != null && data2.length() == 0) {
- // If there is already data entered don't overwrite it
- data2.setText(entry.data2);
- }
- data.setHint(entry.hint);
- if (data2 != null) {
- data2.setHint(entry.hint2);
- }
- if (entry.lines > 1) {
- data.setLines(entry.lines);
- data.setMaxLines(entry.maxLines);
- if (data2 != null) {
- data2.setLines(entry.lines);
- data2.setMaxLines(entry.maxLines);
- }
- }
- int contentType = entry.contentType;
- if (contentType != EditorInfo.TYPE_NULL) {
- data.setInputType(contentType);
- if (data2 != null) {
- data2.setInputType(contentType);
- }
- if ((contentType&EditorInfo.TYPE_MASK_CLASS)
- == EditorInfo.TYPE_CLASS_PHONE) {
- data.addTextChangedListener(new PhoneNumberFormattingTextWatcher());
- if (data2 != null) {
- data2.addTextChangedListener(new PhoneNumberFormattingTextWatcher());
- }
- }
- }
-
- // Give focus to children as requested, possibly after a configuration change
- View focusChild = view.findViewById(entry.requestFocusId);
- if (focusChild != null) {
- focusChild.requestFocus();
- if (focusChild instanceof EditText) {
- ((EditText) focusChild).setSelection(entry.requestCursor);
- }
- }
-
- // Reset requested focus values
- entry.requestFocusId = View.NO_ID;
- entry.requestCursor = 0;
-
- // Connect listeners up to watch for changed values.
- if (data instanceof EditText) {
- data.addTextChangedListener(this);
- }
- if (data2 instanceof EditText) {
- data2.addTextChangedListener(this);
- }
-
- // Hook up the delete button
- View delete = view.findViewById(R.id.delete);
- if (delete != null) {
- delete.setOnClickListener(this);
- }
-
- return view;
- }
-
- private void fillViewData(final EditEntry entry) {
- /*
- else if (isOtherEntry(entry, GroupMembership.GROUP_ID)) {
- if (entry.data != null) {
- updateDataView(entry, entry.data);
- }
- }
- */
- }
-
- /**
- * Handles the results from the label change picker.
- */
- private final class LabelPickedListener implements DialogInterface.OnClickListener {
- EditEntry mEntry;
- String[] mLabels;
-
- public LabelPickedListener(EditEntry entry, String[] labels) {
- mEntry = entry;
- mLabels = labels;
- }
-
- public void onClick(DialogInterface dialog, int which) {
- // TODO: Use a managed dialog
- if (mEntry.mimetype != Im.CONTENT_ITEM_TYPE) {
- final int type = getTypeFromLabelPosition(mLabels, which);
- if (type == BaseTypes.TYPE_CUSTOM) {
- createCustomPicker(mEntry, null);
- } else {
- mEntry.setLabel(EditContactActivity.this, type, mLabels[which]);
- mContactChanged = true;
- }
- } else {
- mEntry.setLabel(EditContactActivity.this, which, mLabels[which]);
- mContactChanged = true;
- }
- }
- }
-
- /**
- * A basic structure with the data for a contact entry in the list.
- */
- private static final class EditEntry extends ContactEntryAdapter.Entry implements Parcelable {
- // These aren't stuffed into the parcel
- public EditContactActivity activity;
- public View view;
-
- // These are stuffed into the parcel
- public String hint;
- public String hint2;
- public String column;
- public String contentDirectory;
- public String data2;
- public int contentType;
- public int type;
- /**
- * If 0 or 1, setSingleLine will be called. If negative, setSingleLine
- * will not be called.
- */
- public int lines = 1;
- public boolean isPrimary;
- public boolean isDeleted = false;
- public boolean isStaticLabel = false;
- public boolean syncDataWithView = true;
-
- /**
- * Request focus on the child of this {@link EditEntry} found using
- * {@link View#findViewById(int)}. This value should be reset to
- * {@link View#NO_ID} after each use.
- */
- public int requestFocusId = View.NO_ID;
-
- /**
- * If the {@link #requestFocusId} is an {@link EditText}, this value
- * indicates the requested cursor position placement.
- */
- public int requestCursor = 0;
-
- private EditEntry() {
- // only used by CREATOR
- }
-
- public EditEntry(EditContactActivity activity) {
- this.activity = activity;
- }
-
- public EditEntry(EditContactActivity activity, String label,
- int type, String data, Uri uri, long id) {
- this.activity = activity;
- this.isPrimary = false;
- this.label = label;
- this.type = type;
- this.data = data;
- this.uri = uri;
- this.id = id;
- }
-
- public int describeContents() {
- return 0;
- }
-
- public void writeToParcel(Parcel parcel, int flags) {
- // Make sure to read data from the input field, if anything is entered
- data = getData();
-
- // Write in our own fields.
- parcel.writeString(hint);
- parcel.writeString(hint2);
- parcel.writeString(column);
- parcel.writeString(contentDirectory);
- parcel.writeString(data2);
- parcel.writeInt(contentType);
- parcel.writeInt(type);
- parcel.writeInt(lines);
- parcel.writeInt(isPrimary ? 1 : 0);
- parcel.writeInt(isDeleted ? 1 : 0);
- parcel.writeInt(isStaticLabel ? 1 : 0);
- parcel.writeInt(syncDataWithView ? 1 : 0);
-
- // Write in the fields from Entry
- super.writeToParcel(parcel);
- }
-
- public static final Parcelable.Creator<EditEntry> CREATOR =
- new Parcelable.Creator<EditEntry>() {
- public EditEntry createFromParcel(Parcel in) {
- EditEntry entry = new EditEntry();
-
- // Read out our own fields
- entry.hint = in.readString();
- entry.hint2 = in.readString();
- entry.column = in.readString();
- entry.contentDirectory = in.readString();
- entry.data2 = in.readString();
- entry.contentType = in.readInt();
- entry.type = in.readInt();
- entry.lines = in.readInt();
- entry.isPrimary = in.readInt() == 1;
- entry.isDeleted = in.readInt() == 1;
- entry.isStaticLabel = in.readInt() == 1;
- entry.syncDataWithView = in.readInt() == 1;
-
- // Read out the fields from Entry
- entry.readFromParcel(in);
-
- return entry;
- }
-
- public EditEntry[] newArray(int size) {
- return new EditEntry[size];
- }
- };
-
- public void setLabel(Context context, int typeIn, String labelIn) {
- type = typeIn;
- label = labelIn;
- if (view != null) {
- bindLabel(context);
- }
- }
-
- public void bindLabel(Context context) {
- TextView v = (TextView) view.findViewById(R.id.label);
- if (isStaticLabel) {
- v.setText(label);
- return;
- }
-
- v.setText(ContactsUtils.getDisplayLabel(context, mimetype, type, label));
- if (mimetype.equals(Im.CONTENT_ITEM_TYPE) && type >= 0) {
- v.setText(getLabelsForMimetype(activity, mimetype)[type]);
- } else if (mimetype.equals(StructuredPostal.CONTENT_ITEM_TYPE)) {
- v.setMaxLines(3);
- }
- v.setOnClickListener(activity);
- }
-
- /**
- * Returns the data for the entry
- * @return the data for the entry
- */
- public String getData() {
- if (view != null && syncDataWithView) {
- CharSequence text = ((TextView) view.findViewById(R.id.data)).getText();
- if (text != null) {
- return text.toString();
- }
- }
-
- if (data != null) {
- return data.toString();
- }
-
- return null;
- }
-
- /**
- * Dumps the entry into a HashMap suitable for passing to the database.
- *
- * @param values the HashMap to fill in.
- * @return true if the value should be saved, false otherwise
- */
- public boolean toValues(ContentValues values) {
- boolean success = false;
- String labelString = null;
- // Save the type and label
- if (view != null) {
- // Read the possibly updated label from the text field
- labelString = ((TextView) view.findViewById(R.id.label)).getText().toString();
- }
- if (mimetype.equals(Phone.CONTENT_ITEM_TYPE)) {
- if (type != Phone.TYPE_CUSTOM) {
- labelString = null;
- }
- values.put(Phone.LABEL, labelString);
- values.put(Phone.TYPE, type);
- } else if (mimetype.equals(Email.CONTENT_ITEM_TYPE)) {
- if (type != Email.TYPE_CUSTOM) {
- labelString = null;
- }
- values.put(Email.LABEL, labelString);
- values.put(Email.TYPE, type);
- } else if (mimetype.equals(CommonDataKinds.Im.CONTENT_ITEM_TYPE)) {
- values.put(CommonDataKinds.Im.TYPE, type);
- values.putNull(CommonDataKinds.Im.LABEL);
- if (type != -1) {
- values.put(CommonDataKinds.Im.PROTOCOL,
- ContactsUtils.encodePredefinedImProtocol(type));
- } else {
- values.put(CommonDataKinds.Im.PROTOCOL,
- ContactsUtils.encodeCustomImProtocol(label.toString()));
- }
- } else if (mimetype.equals(CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE)) {
- if (type != StructuredPostal.TYPE_CUSTOM) {
- labelString = null;
- }
- values.put(StructuredPostal.LABEL, labelString);
- values.put(StructuredPostal.TYPE, type);
- } else if (mimetype.equals(CommonDataKinds.Organization.CONTENT_ITEM_TYPE)) {
- if (type != Organization.TYPE_CUSTOM) {
- labelString = null;
- }
- values.put(Organization.LABEL, labelString);
- values.put(Organization.TYPE, type);
- // Save the title
- if (view != null) {
- // Read the possibly updated data from the text field
- data2 = ((TextView) view.findViewById(R.id.data2)).getText().toString();
- }
- if (!TextUtils.isGraphic(data2)) {
- values.putNull(Organization.TITLE);
- } else {
- values.put(Organization.TITLE, data2.toString());
- success = true;
- }
- } else {
- Log.i(TAG, "unknown entry mimetype: " + (mimetype == null ? "" : mimetype));
- }
-
- // Only set the ISPRIMARY flag if part of the incoming data. This is because the
- // ContentProvider will try finding a new primary when setting to false, meaning
- // it's possible to lose primary altogether as we walk down the list. If this editor
- // implements editing of primaries in the future, this will need to be revisited.
- if (isPrimary) {
- values.put(Data.IS_PRIMARY, 1);
- }
-
- // Save the data
- if (view != null && syncDataWithView) {
- // Read the possibly updated data from the text field
- data = ((TextView) view.findViewById(R.id.data)).getText().toString();
- }
- if (!TextUtils.isGraphic(data)) {
- values.putNull(column);
- return success;
- } else {
- values.put(column, data.toString());
- return true;
- }
- }
-
- /**
- * Create a new empty organization entry
- */
- public static final EditEntry newOrganizationEntry(EditContactActivity activity,
- Uri uri, int type) {
- return newOrganizationEntry(activity, null, type, null, null, uri, 0);
- }
-
- /**
- * Create a new company entry with the given data.
- */
- public static final EditEntry newOrganizationEntry(EditContactActivity activity,
- String label, int type, String company, String title, Uri uri, long id) {
- EditEntry entry = new EditEntry(activity, label, type, company, uri, id);
- entry.hint = activity.getString(R.string.ghostData_company);
- entry.hint2 = activity.getString(R.string.ghostData_title);
- entry.data2 = title;
- entry.column = Organization.COMPANY;
- entry.mimetype = Organization.CONTENT_ITEM_TYPE;
- entry.contentType = EditorInfo.TYPE_CLASS_TEXT
- | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
- return entry;
- }
-
- /**
- * Create a new empty notes entry
- */
- public static final EditEntry newNotesEntry(EditContactActivity activity,
- Uri uri) {
- return newNotesEntry(activity, null, uri, 0);
- }
-
- /**
- * Create a new notes entry with the given data.
- */
- public static final EditEntry newNotesEntry(EditContactActivity activity,
- String data, Uri uri, long id) {
- EditEntry entry = new EditEntry(activity);
- entry.label = activity.getString(R.string.label_notes);
- entry.hint = activity.getString(R.string.ghostData_notes);
- entry.data = data;
- entry.uri = uri;
- entry.column = Note.NOTE;
- entry.mimetype = Note.CONTENT_ITEM_TYPE;
- entry.maxLines = 10;
- entry.lines = 2;
- entry.id = id;
- entry.contentType = EditorInfo.TYPE_CLASS_TEXT
- | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES
- | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
- entry.isStaticLabel = true;
- return entry;
- }
-
- /**
- * Create a new group entry with the given data.
- */
- /*
- public static final EditEntry newGroupEntry(EditContactActivity activity,
- String data, Uri uri, long personId) {
- EditEntry entry = new EditEntry(activity);
- entry.label = activity.getString(R.string.label_groups);
- entry.data = data;
- entry.uri = uri;
- entry.id = personId;
- entry.column = GroupMembership.GROUP_ID;
- entry.kind = KIND_GROUP;
- entry.isStaticLabel = true;
- entry.syncDataWithView = false;
- entry.lines = -1;
- return entry;
- }
- */
-
- /**
- * Create a new empty email entry
- */
- public static final EditEntry newPhoneEntry(EditContactActivity activity,
- Uri uri, int type) {
- return newPhoneEntry(activity, null, type, null, uri, 0);
- }
-
- /**
- * Create a new phone entry with the given data.
- */
- public static final EditEntry newPhoneEntry(EditContactActivity activity,
- String label, int type, String data, Uri uri,
- long id) {
- EditEntry entry = new EditEntry(activity, label, type, data, uri, id);
- entry.hint = activity.getString(R.string.ghostData_phone);
- entry.column = Phone.NUMBER;
- entry.mimetype = Phone.CONTENT_ITEM_TYPE;
- entry.contentType = EditorInfo.TYPE_CLASS_PHONE;
- return entry;
- }
-
- /**
- * Create a new empty email entry
- */
- public static final EditEntry newEmailEntry(EditContactActivity activity,
- Uri uri, int type) {
- return newEmailEntry(activity, null, type, null, uri, 0);
- }
-
- /**
- * Create a new email entry with the given data.
- */
- public static final EditEntry newEmailEntry(EditContactActivity activity,
- String label, int type, String data, Uri uri,
- long id) {
- EditEntry entry = new EditEntry(activity, label, type, data, uri, id);
- entry.hint = activity.getString(R.string.ghostData_email);
- entry.column = Email.DATA;
- entry.mimetype = Email.CONTENT_ITEM_TYPE;
- entry.contentType = EditorInfo.TYPE_CLASS_TEXT
- | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
- return entry;
- }
-
- /**
- * Create a new empty postal address entry
- */
- public static final EditEntry newPostalEntry(EditContactActivity activity,
- Uri uri, int type) {
- return newPostalEntry(activity, null, type, null, uri, 0);
- }
-
- /**
- * Create a new postal address entry with the given data.
- *
- * @param label label for the item, from the db not the display label
- * @param type the type of postal address
- * @param data the starting data for the entry, may be null
- * @param uri the uri for the entry if it already exists, may be null
- * @param id the id for the entry if it already exists, 0 it it doesn't
- * @return the new EditEntry
- */
- public static final EditEntry newPostalEntry(EditContactActivity activity,
- String label, int type, String data, Uri uri, long id) {
- EditEntry entry = new EditEntry(activity, label, type, data, uri, id);
- entry.hint = activity.getString(R.string.ghostData_postal);
- entry.column = StructuredPostal.DATA;
- entry.mimetype = StructuredPostal.CONTENT_ITEM_TYPE;
- entry.contentType = EditorInfo.TYPE_CLASS_TEXT
- | EditorInfo.TYPE_TEXT_VARIATION_POSTAL_ADDRESS
- | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS
- | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
- entry.maxLines = 4;
- entry.lines = 2;
- return entry;
- }
-
- /**
- * Create a new IM address entry
- */
- public static final EditEntry newImEntry(EditContactActivity activity,
- Uri uri, int type) {
- return newImEntry(activity, null, type, null, uri, 0);
- }
-
- /**
- * Create a new IM address entry with the given data.
- *
- * @param label label for the item, from the db not the display label
- * @param protocol the type used
- * @param data the starting data for the entry, may be null
- * @param uri the uri for the entry if it already exists, may be null
- * @param id the id for the entry if it already exists, 0 it it doesn't
- * @return the new EditEntry
- */
- public static final EditEntry newImEntry(EditContactActivity activity,
- String label, int protocol, String data, Uri uri, long id) {
- EditEntry entry = new EditEntry(activity, label, protocol, data, uri, id);
- entry.hint = activity.getString(R.string.ghostData_im);
- entry.column = Im.DATA;
- entry.mimetype = Im.CONTENT_ITEM_TYPE;
- entry.contentType = EditorInfo.TYPE_CLASS_TEXT
- | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
- return entry;
- }
- }
-
- public void afterTextChanged(Editable s) {
- // Someone edited a text field, so assume this contact is changed
- mContactChanged = true;
- }
-
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
- // Do nothing; editing handled by afterTextChanged()
- }
-
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- // Do nothing; editing handled by afterTextChanged()
- }
-
- public void onFocusChange(View v, boolean hasFocus) {
- // Because we're emulating a ListView, we need to setSelected() for
- // views as they are focused.
- v.setSelected(hasFocus);
- }
-}
diff --git a/src/com/android/contacts/model/AugmentedEntity.java b/src/com/android/contacts/model/AugmentedEntity.java
new file mode 100644
index 0000000..5213daf
--- /dev/null
+++ b/src/com/android/contacts/model/AugmentedEntity.java
@@ -0,0 +1,312 @@
+/*
+ * 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.model;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentValues;
+import android.content.Entity;
+import android.content.Entity.NamedContentValues;
+import android.os.Parcel;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * Contains an {@link Entity} that records any modifications separately so the
+ * original {@link Entity} can be swapped out with a newer version and the
+ * changes still cleanly applied.
+ * <p>
+ * One benefit of this approach is that we can build changes entirely on an
+ * empty {@link Entity}, which then becomes an insert {@link Contacts} case.
+ * <p>
+ * When applying modifications over an {@link Entity}, we try finding the
+ * original {@link Data#_ID} rows where the modifications took place. If those
+ * rows are missing from the new {@link Entity}, we know the original data must
+ * be deleted, but to preserve the user modifications we treat as an insert.
+ */
+public class AugmentedEntity {
+ // TODO: optimize by using contentvalues pool, since we allocate so many of them
+ // TODO: write unit tests to make sure that getDiff() is performing correctly
+
+ /**
+ * Direct values from {@link Entity#getEntityValues()}.
+ */
+ private AugmentedValues mValues;
+
+ /**
+ * Internal map of children values from {@link Entity#getSubValues()}, which
+ * we store here sorted into {@link Data#MIMETYPE} bins.
+ */
+ private HashMap<String, ArrayList<AugmentedValues>> mEntries;
+
+ private AugmentedEntity() {
+ mEntries = new HashMap<String, ArrayList<AugmentedValues>>();
+ }
+
+ /**
+ * Build an {@link AugmentedEntity} using the given {@link Entity} as a
+ * starting point; the "before" snapshot.
+ */
+ public static AugmentedEntity fromBefore(Entity before) {
+ final AugmentedEntity entity = new AugmentedEntity();
+ entity.mValues = AugmentedValues.fromBefore(before.getEntityValues());
+ for (NamedContentValues namedValues : before.getSubValues()) {
+ entity.addEntry(AugmentedValues.fromBefore(namedValues.values));
+ }
+ return entity;
+ }
+
+ /**
+ * Get the {@link AugmentedValues} child marked as {@link Data#IS_PRIMARY}.
+ */
+ public AugmentedValues getPrimaryEntry(String mimeType) {
+ // TODO: handle the case where the caller must have a non-null value,
+ // for example displayname
+ final ArrayList<AugmentedValues> mimeEntries = getMimeEntries(mimeType, false);
+ for (AugmentedValues entry : mimeEntries) {
+ if (entry.isPrimary()) {
+ return entry;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Return the list of child {@link AugmentedValues} from our optimized map,
+ * creating the list if requested.
+ */
+ private ArrayList<AugmentedValues> getMimeEntries(String mimeType, boolean lazyCreate) {
+ ArrayList<AugmentedValues> mimeEntries = mEntries.get(mimeType);
+ if (mimeEntries == null && lazyCreate) {
+ mimeEntries = new ArrayList<AugmentedValues>();
+ mEntries.put(mimeType, mimeEntries);
+ }
+ return mimeEntries;
+ }
+
+ public ArrayList<AugmentedValues> getMimeEntries(String mimeType) {
+ return getMimeEntries(mimeType, false);
+ }
+
+ public boolean hasMimeEntries(String mimeType) {
+ return mEntries.containsKey(mimeType);
+ }
+
+ public void addEntry(AugmentedValues entry) {
+ final String mimeType = entry.getMimetype();
+ getMimeEntries(mimeType, true).add(entry);
+ }
+
+ /**
+ * Find the {@link AugmentedValues} that has a specific
+ * {@link BaseColumns#_ID} value, used when {@link #augmentFrom(Parcel)} is
+ * inflating a modified state.
+ */
+ public AugmentedValues getEntry(long anchorId) {
+ if (anchorId < 0) {
+ // Requesting an "insert" entry, which has no "before"
+ return null;
+ }
+
+ // Search all children for requested entry
+ for (ArrayList<AugmentedValues> mimeEntries : mEntries.values()) {
+ for (AugmentedValues entry : mimeEntries) {
+ if (entry.getId() == anchorId) {
+ return entry;
+ }
+ }
+ }
+ return null;
+ }
+
+ private static final int MODE_CONTINUE = 1;
+ private static final int MODE_DONE = 2;
+
+ /**
+ * Read a set of modifying actions from the given {@link Parcel}, which
+ * expects the format written by {@link #augmentTo(Parcel)}. This expects
+ * that we already have a base {@link Entity} that we are applying over.
+ */
+ public void augmentFrom(Parcel parcel) {
+ {
+ final ContentValues after = (ContentValues)parcel.readValue(null);
+ if (mValues == null) {
+ // Entity didn't exist before, so "insert"
+ mValues = AugmentedValues.fromAfter(after);
+ } else {
+ // Existing entity "update"
+ mValues.mAfter = after;
+ }
+ }
+
+ // Read in packaged children until finished
+ int mode = parcel.readInt();
+ while (mode == MODE_CONTINUE) {
+ final long anchorId = parcel.readLong();
+ final ContentValues after = (ContentValues)parcel.readValue(null);
+
+ AugmentedValues entry = getEntry(anchorId);
+ if (anchorId < 0 || entry == null) {
+ // Is "insert", or "before" record is missing, so now "insert"
+ entry = AugmentedValues.fromAfter(after);
+ addEntry(entry);
+ } else {
+ // Existing entry "update"
+ entry.mAfter = after;
+ }
+
+ mode = parcel.readInt();
+ }
+ }
+
+ /**
+ * Store all modifying actions into the given {@link Parcel}.
+ */
+ public void augmentTo(Parcel parcel) {
+ parcel.writeValue(mValues.mAfter);
+
+ for (ArrayList<AugmentedValues> mimeEntries : mEntries.values()) {
+ for (AugmentedValues child : mimeEntries) {
+ parcel.writeInt(MODE_CONTINUE);
+ parcel.writeLong(child.getId());
+ parcel.writeValue(child.mAfter);
+ }
+ }
+ parcel.writeInt(MODE_DONE);
+ }
+
+ /**
+ * Build a list of {@link ContentProviderOperation} that will transform the
+ * current "before" {@link Entity} state into the modified state which this
+ * {@link AugmentedEntity} represents.
+ */
+ public ArrayList<ContentProviderOperation> getDiff() {
+ // TODO: assert that existing contact exists, and provide CONTACT_ID to children inserts
+ // TODO: mostly calling through to children for diff operations
+ return null;
+ }
+
+ /**
+ * Type of {@link ContentValues} that maintains both an original state and a
+ * modified version of that state. This allows us to build insert, update,
+ * or delete operations based on a "before" {@link Entity} snapshot.
+ */
+ public static class AugmentedValues {
+ private ContentValues mBefore;
+ private ContentValues mAfter;
+
+ private AugmentedValues() {
+ }
+
+ /**
+ * Create {@link AugmentedValues}, using the given object as the
+ * "before" state, usually from an {@link Entity}.
+ */
+ public static AugmentedValues fromBefore(ContentValues before) {
+ final AugmentedValues entry = new AugmentedValues();
+ entry.mBefore = before;
+ entry.mAfter = new ContentValues();
+ return entry;
+ }
+
+ /**
+ * Create {@link AugmentedValues}, using the given object as the "after"
+ * state, usually when we are inserting a row instead of updating.
+ */
+ public static AugmentedValues fromAfter(ContentValues after) {
+ final AugmentedValues entry = new AugmentedValues();
+ entry.mBefore = null;
+ entry.mAfter = after;
+ return entry;
+ }
+
+ public String getAsString(String key) {
+ if (mAfter != null && mAfter.containsKey(key)) {
+ return mAfter.getAsString(key);
+ } else if (mBefore != null && mBefore.containsKey(key)) {
+ return mBefore.getAsString(key);
+ } else {
+ return null;
+ }
+ }
+
+ public Long getAsLong(String key) {
+ if (mAfter != null && mAfter.containsKey(key)) {
+ return mAfter.getAsLong(key);
+ } else if (mBefore != null && mBefore.containsKey(key)) {
+ return mBefore.getAsLong(key);
+ } else {
+ return null;
+ }
+ }
+
+ public String getMimetype() {
+ return getAsString(Data.MIMETYPE);
+ }
+
+ public Long getId() {
+ return getAsLong(BaseColumns._ID);
+ }
+
+ public boolean isPrimary() {
+ return (getAsLong(Data.IS_PRIMARY) != 0);
+ }
+
+ public boolean isDeleted() {
+ return (mAfter == null);
+ }
+
+ public void markDeleted() {
+ mAfter = null;
+ }
+
+ /**
+ * Ensure that our internal structure is ready for storing updates.
+ */
+ private void ensureUpdate() {
+ if (mAfter == null) {
+ mAfter = new ContentValues();
+ }
+ }
+
+ public void put(String key, String value) {
+ ensureUpdate();
+ mAfter.put(key, value);
+ }
+
+ public void put(String key, int value) {
+ ensureUpdate();
+ mAfter.put(key, value);
+ }
+
+ /**
+ * Build a {@link ContentProviderOperation} that will transform our
+ * "before" state into our "after" state, using insert, update, or
+ * delete as needed.
+ */
+ public ContentProviderOperation getDiff() {
+ // TODO: build insert/update/delete based on internal state
+ // any _id under zero are inserts
+ return null;
+ }
+
+ }
+
+}
diff --git a/src/com/android/contacts/model/ContactsSource.java b/src/com/android/contacts/model/ContactsSource.java
new file mode 100644
index 0000000..ba19270
--- /dev/null
+++ b/src/com/android/contacts/model/ContactsSource.java
@@ -0,0 +1,224 @@
+/*
+ * 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.model;
+
+import android.database.Cursor;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.widget.EditText;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/*
+
+<!-- example of what SourceConstraints would look like in XML -->
+<!-- NOTE: may not directly match the current structure version -->
+
+<DataKind
+ mimeType="vnd.android.cursor.item/email"
+ title="@string/title_postal"
+ icon="@drawable/icon_postal"
+ weight="12"
+ editable="true">
+
+ <!-- these are defined using string-builder-ish -->
+ <ActionHeader></ActionHeader>
+ <ActionBody socialSummary="true" /> <!-- can pull together various columns -->
+
+ <!-- ordering handles precedence the "insert/add" case -->
+ <!-- assume uniform type when missing "column", use title in place -->
+ <EditTypes column="data5" overallMax="-1">
+ <EditType rawValue="0" label="@string/type_home" specificMax="-1" />
+ <EditType rawValue="1" label="@string/type_work" specificMax="-1" secondary="true" />
+ <EditType rawValue="4" label="@string/type_custom" customColumn="data6" specificMax="-1" secondary="true" />
+ </EditTypes>
+
+ <!-- when single edit field, simplifies edit case -->
+ <EditField column="data1" title="@string/field_family_name" android:inputType="textCapWords|textPhonetic" />
+ <EditField column="data2" title="@string/field_given_name" android:minLines="2" />
+ <EditField column="data3" title="@string/field_suffix" />
+
+</DataKind>
+
+*/
+
+/**
+ * Internal structure that represents constraints for a specific data source,
+ * such as the various data types they support, including details on how those
+ * types should be rendered and edited.
+ * <p>
+ * In the future this may be inflated from XML defined by a data source.
+ */
+public class ContactsSource {
+ /**
+ * The {@link Contacts#ACCOUNT_TYPE} these constraints apply to.
+ */
+ public String accountType;
+
+ /**
+ * Set of {@link DataKind} supported by this source.
+ */
+ private ArrayList<DataKind> mKinds = new ArrayList<DataKind>();
+
+ /**
+ * {@link Comparator} to sort by {@link DataKind#weight}.
+ */
+ private static Comparator<DataKind> sWeightComparator = new Comparator<DataKind>() {
+ public int compare(DataKind object1, DataKind object2) {
+ return object1.weight - object2.weight;
+ }
+ };
+
+ /**
+ * Return list of {@link DataKind} supported, sorted by
+ * {@link DataKind#weight}.
+ */
+ public ArrayList<DataKind> getSortedDataKinds() {
+ // TODO: optimize by marking if already sorted
+ Collections.sort(mKinds, sWeightComparator);
+ return mKinds;
+ }
+
+ /**
+ * Find the {@link DataKind} for a specifc MIME-type, if it's handled by
+ * this data source.
+ */
+ public DataKind getKindForMimetype(String mimeType) {
+ for (DataKind kind : mKinds) {
+ if (mimeType.equals(kind.mimeType)) {
+ return kind;
+ }
+ }
+ return null;
+ }
+
+ public void add(DataKind kind) {
+ this.mKinds.add(kind);
+ }
+
+ /**
+ * Description of a specific data type, usually marked by a unique
+ * {@link Data#MIMETYPE}. Includes details about how to view and edit
+ * {@link Data} rows of this kind, including the possible {@link EditType}
+ * labels and editable {@link EditField}.
+ */
+ public static class DataKind {
+ public String mimeType;
+ public int titleRes;
+ public int iconRes;
+ public int weight;
+ public boolean secondary;
+ public boolean editable;
+
+ public StringInflater actionHeader;
+ public StringInflater actionBody;
+ public boolean actionBodySocial;
+ public boolean actionBodyCombine;
+
+ public String typeColumn;
+ public int typeOverallMax;
+
+ public List<EditType> typeList;
+ public List<EditField> fieldList;
+
+ public DataKind(String mimeType, int titleRes, int iconRes, int weight, boolean editable) {
+ this.mimeType = mimeType;
+ this.titleRes = titleRes;
+ this.iconRes = iconRes;
+ this.weight = weight;
+ this.editable = editable;
+ }
+ }
+
+ /**
+ * Description of a specific "type" or "label" of a {@link DataKind} row,
+ * such as {@link Phone#TYPE_WORK}. Includes constraints on total number of
+ * rows a {@link Contacts} may have of this type, and details on how
+ * user-defined labels are stored.
+ */
+ public static class EditType {
+ public int rawValue;
+ public int labelRes;
+ public boolean secondary;
+ public int specificMax;
+ public String customColumn;
+
+ public EditType(int rawValue, int labelRes) {
+ this.rawValue = rawValue;
+ this.labelRes = labelRes;
+ }
+
+ public EditType(int rawValue, int labelRes, boolean secondary) {
+ this(rawValue, labelRes);
+ this.secondary = secondary;
+ }
+
+ public EditType(int rawValue, int labelRes, boolean secondary, int specificMax) {
+ this(rawValue, labelRes, secondary);
+ this.specificMax = specificMax;
+ }
+
+ public EditType(int rawValue, int labelRes, boolean secondary, int specificMax, String customColumn) {
+ this(rawValue, labelRes, secondary, specificMax);
+ this.customColumn = customColumn;
+ }
+ }
+
+ /**
+ * Description of a user-editable field on a {@link DataKind} row, such as
+ * {@link Phone#NUMBER}. Includes flags to apply to an {@link EditText}, and
+ * the column where this field is stored.
+ */
+ public static class EditField {
+ public String column;
+ public int titleRes;
+ public int inputType;
+ public int minLines;
+ public boolean optional;
+
+ public EditField(String column, int titleRes) {
+ this.column = column;
+ this.titleRes = titleRes;
+ }
+
+ public EditField(String column, int titleRes, int inputType) {
+ this(column, titleRes);
+ this.inputType = inputType;
+ }
+
+ public EditField(String column, int titleRes, int inputType, int minLines) {
+ this(column, titleRes, inputType);
+ this.minLines = minLines;
+ }
+ }
+
+ /**
+ * Generic method of inflating a given {@link Cursor} into a user-readable
+ * {@link CharSequence}. For example, an inflater could combine the multiple
+ * columns of {@link StructuredPostal} together using a string resource
+ * before presenting to the user.
+ */
+ public interface StringInflater {
+ public CharSequence inflateUsing(Cursor cursor);
+ }
+
+}
diff --git a/src/com/android/contacts/model/EntityDiff.java b/src/com/android/contacts/model/EntityDiff.java
new file mode 100644
index 0000000..ce1c1e6
--- /dev/null
+++ b/src/com/android/contacts/model/EntityDiff.java
@@ -0,0 +1,147 @@
+/*
+ * 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.model;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentValues;
+import android.content.Entity;
+import android.content.ContentProviderOperation.Builder;
+import android.content.Entity.NamedContentValues;
+import android.net.Uri;
+import android.provider.BaseColumns;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+
+/**
+ * Describes a set of {@link ContentProviderOperation} that need to be
+ * executed to transform a database from one {@link Entity} to another.
+ */
+public class EntityDiff extends ArrayList<ContentProviderOperation> {
+ private EntityDiff() {
+ }
+
+ /**
+ * Build the set of {@link ContentProviderOperation} needed to translate
+ * from "before" to "after". Tries its best to keep operations to
+ * minimal number required. Assumes that all {@link ContentValues} are
+ * keyed using {@link BaseColumns#_ID} values.
+ */
+ public static EntityDiff buildDiff(Entity before, Entity after, Uri targetUri,
+ String childForeignKey) {
+ final EntityDiff diff = new EntityDiff();
+
+ Builder builder;
+ ContentValues values;
+
+ if (before == null) {
+ // Before doesn't exist, so insert "after" values
+ builder = ContentProviderOperation.newInsert(targetUri);
+ builder.withValues(after.getEntityValues());
+ diff.add(builder.build());
+
+ for (NamedContentValues child : after.getSubValues()) {
+ // Add builder with reference to original _id when needed
+ builder = ContentProviderOperation.newInsert(child.uri);
+ builder.withValues(child.values);
+ if (childForeignKey != null) {
+ builder.withValueBackReference(childForeignKey, 0);
+ }
+ diff.add(builder.build());
+ }
+
+ } else if (after == null) {
+ // After doesn't exist, so delete "before" values
+ for (NamedContentValues child : before.getSubValues()) {
+ builder = ContentProviderOperation.newDelete(child.uri);
+ builder.withSelection(getSelectIdClause(child.values), null);
+ diff.add(builder.build());
+ }
+
+ builder = ContentProviderOperation.newDelete(targetUri);
+ builder.withSelection(getSelectIdClause(before.getEntityValues()), null);
+ diff.add(builder.build());
+
+ } else {
+ // Somewhere between, so update any changed values
+ values = after.getEntityValues();
+ if (!before.getEntityValues().equals(values)) {
+ // Top-level values changed, so update
+ builder = ContentProviderOperation.newUpdate(targetUri);
+ builder.withSelection(getSelectIdClause(values), null);
+ builder.withValues(values);
+ diff.add(builder.build());
+ }
+
+ // Build lookup maps for children on both sides
+ final HashMap<String, NamedContentValues> beforeChildren = buildChildrenMap(before);
+ final HashMap<String, NamedContentValues> afterChildren = buildChildrenMap(after);
+
+ // Walk through "before" children looking for deletes and updates
+ for (NamedContentValues beforeChild : beforeChildren.values()) {
+ final String key = buildChildKey(beforeChild);
+ final NamedContentValues afterChild = afterChildren.get(key);
+
+ if (afterChild == null) {
+ // After child doesn't exist, so delete "before" child
+ builder = ContentProviderOperation.newDelete(beforeChild.uri);
+ builder.withSelection(getSelectIdClause(beforeChild.values), null);
+ diff.add(builder.build());
+ } else if (!beforeChild.values.equals(afterChild.values)) {
+ // After child still exists, and is different, so update
+ values = afterChild.values;
+ builder = ContentProviderOperation.newUpdate(afterChild.uri);
+ builder.withSelection(getSelectIdClause(values), null);
+ builder.withValues(values);
+ diff.add(builder.build());
+ }
+
+ // Remove the now-handled "after" child
+ afterChildren.remove(key);
+ }
+
+ // Walk through remaining "after" children, which are inserts
+ for (NamedContentValues afterChild : afterChildren.values()) {
+ builder = ContentProviderOperation.newInsert(afterChild.uri);
+ builder.withValues(afterChild.values);
+ diff.add(builder.build());
+ }
+ }
+
+ return diff;
+ }
+
+ private static String buildChildKey(NamedContentValues child) {
+ return child.uri.toString() + child.values.getAsString(BaseColumns._ID);
+ }
+
+ private static String getSelectIdClause(ContentValues values) {
+ return BaseColumns._ID + "=" + values.getAsLong(BaseColumns._ID);
+ }
+
+ private static HashMap<String, NamedContentValues> buildChildrenMap(Entity entity) {
+ final ArrayList<NamedContentValues> children = entity.getSubValues();
+ final HashMap<String, NamedContentValues> childrenMap = new HashMap<String, NamedContentValues>(
+ children.size());
+ for (NamedContentValues child : children) {
+ final String key = buildChildKey(child);
+ childrenMap.put(key, child);
+ }
+ return childrenMap;
+ }
+}
diff --git a/src/com/android/contacts/model/EntityModifier.java b/src/com/android/contacts/model/EntityModifier.java
new file mode 100644
index 0000000..147b28f
--- /dev/null
+++ b/src/com/android/contacts/model/EntityModifier.java
@@ -0,0 +1,95 @@
+/*
+ * 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.model;
+
+import com.android.contacts.model.AugmentedEntity.AugmentedValues;
+import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.model.ContactsSource.EditType;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract.Data;
+
+import java.util.List;
+
+/**
+ * Helper methods for modifying an {@link AugmentedEntity}, such as inserting
+ * new rows, or enforcing {@link ContactsSource}.
+ */
+public class EntityModifier {
+ // TODO: provide helper to force an augmentedentity into sourceconstraints?
+
+ /**
+ * For the given {@link AugmentedEntity}, determine if the given
+ * {@link DataKind} could be inserted under specific
+ * {@link ContactsSource}.
+ */
+ public static boolean canInsert(AugmentedEntity contact, DataKind kind) {
+ // TODO: compare against constraints to determine if insert is possible
+ return true;
+ }
+
+ /**
+ * For the given {@link AugmentedEntity} and {@link DataKind}, return the
+ * list possible {@link EditType} options available based on
+ * {@link ContactsSource}.
+ */
+ public static List<EditType> getValidTypes(AugmentedEntity entity, DataKind kind,
+ EditType forceInclude) {
+ // TODO: enforce constraints and include any extra provided
+ return kind.typeList;
+ }
+
+ /**
+ * Check if the given {@link DataKind} has multiple types that should be
+ * displayed for users to pick.
+ */
+ public static boolean hasEditTypes(DataKind kind) {
+ return kind.typeList != null && kind.typeList.size() > 0;
+ }
+
+ /**
+ * Find the {@link EditType} that describes the given
+ * {@link AugmentedValues} row, assuming the given {@link DataKind} dictates
+ * the possible types.
+ */
+ public static EditType getCurrentType(AugmentedValues entry, DataKind kind) {
+ final long rawValue = entry.getAsLong(kind.typeColumn);
+ for (EditType type : kind.typeList) {
+ if (type.rawValue == rawValue) {
+ return type;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Insert a new child of kind {@link DataKind} into the given
+ * {@link AugmentedEntity}. Assumes the caller has already checked
+ * {@link #canInsert(AugmentedEntity, DataKind)}.
+ */
+ public static void insertChild(AugmentedEntity state, DataKind kind) {
+ final ContentValues after = new ContentValues();
+
+ // TODO: add the best-kind of entry based on current state machine
+ // TODO: fill in other default values
+ after.put(Data.MIMETYPE, kind.mimeType);
+// after.put(Data.CONTACT_ID, state.values.getAsLong(Contacts._ID));
+
+ state.addEntry(AugmentedValues.fromAfter(after));
+ }
+
+}
diff --git a/src/com/android/contacts/model/Sources.java b/src/com/android/contacts/model/Sources.java
new file mode 100644
index 0000000..b642064
--- /dev/null
+++ b/src/com/android/contacts/model/Sources.java
@@ -0,0 +1,403 @@
+/*
+ * 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.model;
+
+import com.android.contacts.R;
+
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.view.inputmethod.EditorInfo;
+
+import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.model.ContactsSource.EditType;
+import com.android.contacts.model.ContactsSource.EditField;
+import com.android.contacts.model.ContactsSource.StringInflater;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * Singleton holder for all parsed {@link ContactsSource} available on the
+ * system, typically filled through {@link PackageManager} queries.
+ * <p>
+ * Some {@link ContactsSource} may be hard-coded here, as the constraint
+ * language hasn't been finalized.
+ */
+public class Sources {
+ // TODO: finish hard-coding all constraints
+
+ private static Sources sInstance;
+
+ public static synchronized Sources getInstance() {
+ if (sInstance == null) {
+ sInstance = new Sources();
+ }
+ return sInstance;
+ }
+
+ public static final String ACCOUNT_TYPE_GOOGLE = "com.google.GAIA";
+ public static final String ACCOUNT_TYPE_EXCHANGE = "vnd.exchange";
+
+ private HashMap<String, ContactsSource> mSources = new HashMap<String, ContactsSource>();
+
+ private Sources() {
+ mSources.put(ACCOUNT_TYPE_GOOGLE, buildGoogle());
+ mSources.put(ACCOUNT_TYPE_EXCHANGE, buildExchange());
+ }
+
+ /**
+ * Find the {@link ContactsSource} for the given
+ * {@link Contacts#ACCOUNT_TYPE}.
+ */
+ public ContactsSource getKindsForAccountType(String accountType) {
+ return mSources.get(accountType);
+ }
+
+ /**
+ * Hard-coded instance of {@link ContactsSource} for Google Contacts.
+ */
+ private ContactsSource buildGoogle() {
+ final ContactsSource list = new ContactsSource();
+
+ {
+ // GOOGLE: STRUCTUREDNAME
+ DataKind kind = new DataKind(StructuredName.CONTENT_ITEM_TYPE,
+ R.string.nameLabelsGroup, -1, -1, true);
+ list.add(kind);
+ }
+
+ {
+ // GOOGLE: PHOTO
+ DataKind kind = new DataKind(Photo.CONTENT_ITEM_TYPE, -1, -1, -1, true);
+ list.add(kind);
+ }
+
+ {
+ // GOOGLE: PHONE
+ DataKind kind = new DataKind(Phone.CONTENT_ITEM_TYPE,
+ R.string.phoneLabelsGroup, android.R.drawable.sym_action_call, 10, true);
+
+ kind.actionHeader = new ActionLabelInflater(R.string.actionCall, kind);
+ kind.actionBody = new ColumnInflater(Phone.NUMBER);
+
+ kind.typeColumn = Phone.TYPE;
+ kind.typeList = new ArrayList<EditType>();
+ kind.typeList.add(new EditType(Phone.TYPE_HOME, R.string.type_home));
+ kind.typeList.add(new EditType(Phone.TYPE_MOBILE, R.string.type_mobile));
+ kind.typeList.add(new EditType(Phone.TYPE_WORK, R.string.type_work));
+ kind.typeList.add(new EditType(Phone.TYPE_FAX_WORK, R.string.type_fax_work, true));
+ kind.typeList.add(new EditType(Phone.TYPE_FAX_HOME, R.string.type_fax_home, true));
+ kind.typeList.add(new EditType(Phone.TYPE_PAGER, R.string.type_pager, true));
+ kind.typeList.add(new EditType(Phone.TYPE_OTHER, R.string.type_other));
+ kind.typeList.add(new EditType(Phone.TYPE_CUSTOM, R.string.type_custom, true, -1,
+ Phone.LABEL));
+
+ kind.fieldList = new ArrayList<EditField>();
+ kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup,
+ EditorInfo.TYPE_CLASS_PHONE));
+
+ list.add(kind);
+ }
+
+ {
+ // GOOGLE: EMAIL
+ DataKind kind = new DataKind(Email.CONTENT_ITEM_TYPE,
+ R.string.emailLabelsGroup, android.R.drawable.sym_action_email, 15, true);
+
+ kind.actionHeader = new ActionLabelInflater(R.string.actionEmail, kind);
+ kind.actionBody = new ColumnInflater(Email.DATA);
+
+ kind.typeColumn = Email.TYPE;
+ kind.typeList = new ArrayList<EditType>();
+ kind.typeList.add(new EditType(Email.TYPE_HOME, R.string.type_home));
+ kind.typeList.add(new EditType(Email.TYPE_WORK, R.string.type_work));
+ kind.typeList.add(new EditType(Email.TYPE_OTHER, R.string.type_other));
+ kind.typeList.add(new EditType(Email.TYPE_CUSTOM, R.string.type_custom, true, -1,
+ Email.LABEL));
+
+ kind.fieldList = new ArrayList<EditField>();
+ kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup,
+ EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS));
+
+ list.add(kind);
+ }
+
+ {
+ // GOOGLE: POSTAL
+ DataKind kind = new DataKind(StructuredPostal.CONTENT_ITEM_TYPE,
+ R.string.postalLabelsGroup, R.drawable.sym_action_map, 20, true);
+
+ kind.actionHeader = new ActionLabelInflater(R.string.actionMap, kind);
+
+ kind.typeColumn = StructuredPostal.TYPE;
+ kind.typeList = new ArrayList<EditType>();
+ kind.typeList.add(new EditType(StructuredPostal.TYPE_HOME, R.string.type_home));
+ kind.typeList.add(new EditType(StructuredPostal.TYPE_WORK, R.string.type_work));
+ kind.typeList.add(new EditType(StructuredPostal.TYPE_OTHER, R.string.type_other));
+ kind.typeList.add(new EditType(StructuredPostal.TYPE_CUSTOM, R.string.type_custom,
+ true, -1, StructuredPostal.LABEL));
+
+ // TODO: define editors for each field
+
+// EditorInfo.TYPE_CLASS_TEXT
+// | EditorInfo.TYPE_TEXT_VARIATION_POSTAL_ADDRESS
+// | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS
+// | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
+
+// entry.maxLines = 4;
+// entry.lines = 2;
+
+ list.add(kind);
+ }
+
+ // TODO: GOOGLE: IM
+// entry.contentType = EditorInfo.TYPE_CLASS_TEXT
+// | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
+
+ {
+ // GOOGLE: ORGANIZATION
+ DataKind kind = new DataKind(Organization.CONTENT_ITEM_TYPE,
+ R.string.organizationLabelsGroup, R.drawable.sym_action_organization, 30, true);
+
+ kind.typeColumn = Organization.TYPE;
+ kind.typeList = new ArrayList<EditType>();
+ kind.typeList.add(new EditType(Organization.TYPE_WORK, R.string.type_work));
+ kind.typeList.add(new EditType(Organization.TYPE_OTHER, R.string.type_other));
+ kind.typeList.add(new EditType(Organization.TYPE_CUSTOM, R.string.type_custom, true,
+ -1, Organization.LABEL));
+
+ kind.fieldList = new ArrayList<EditField>();
+ kind.fieldList.add(new EditField(Organization.COMPANY, R.string.ghostData_company,
+ EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS));
+ kind.fieldList.add(new EditField(Organization.TITLE, R.string.ghostData_title,
+ EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS));
+
+ list.add(kind);
+ }
+
+ {
+ // GOOGLE: NOTE
+ DataKind kind = new DataKind(Note.CONTENT_ITEM_TYPE,
+ R.string.label_notes, R.drawable.sym_note, 110, true);
+ kind.secondary = true;
+
+// kind.actionHeader = new ActionLabelInflater(R.string.ac, kind);
+ kind.actionBody = new ColumnInflater(Email.DATA);
+
+ kind.fieldList = new ArrayList<EditField>();
+ kind.fieldList.add(new EditField(Email.DATA, R.string.label_notes,
+ EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES
+ | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE));
+
+ list.add(kind);
+ }
+
+ {
+ // GOOGLE: NICKNAME
+ DataKind kind = new DataKind(Nickname.CONTENT_ITEM_TYPE,
+ R.string.nicknameLabelsGroup, -1, 115, true);
+ kind.secondary = true;
+
+ kind.fieldList = new ArrayList<EditField>();
+ kind.fieldList.add(new EditField(Nickname.NAME, R.string.nicknameLabelsGroup));
+
+ list.add(kind);
+ }
+
+ return list;
+ }
+
+ /**
+ * The constants below are shared with the Exchange sync adapter, and are
+ * currently static. These values should be maintained in parallel.
+ */
+ private static final int TYPE_EMAIL1 = 20;
+ private static final int TYPE_EMAIL2 = 21;
+ private static final int TYPE_EMAIL3 = 22;
+
+ private static final int TYPE_IM1 = 23;
+ private static final int TYPE_IM2 = 24;
+ private static final int TYPE_IM3 = 25;
+
+ private static final int TYPE_WORK2 = 26;
+ private static final int TYPE_HOME2 = 27;
+ private static final int TYPE_CAR = 28;
+ private static final int TYPE_COMPANY_MAIN = 29;
+ private static final int TYPE_MMS = 30;
+ private static final int TYPE_RADIO = 31;
+
+ /**
+ * Hard-coded instance of {@link ContactsSource} for Exchange.
+ */
+ private ContactsSource buildExchange() {
+ final ContactsSource list = new ContactsSource();
+
+ {
+ // EXCHANGE: STRUCTUREDNAME
+ DataKind kind = new DataKind(StructuredName.CONTENT_ITEM_TYPE,
+ R.string.nameLabelsGroup, -1, -1, true);
+ kind.typeOverallMax = 1;
+ list.add(kind);
+ }
+
+ {
+ // EXCHANGE: PHOTO
+ DataKind kind = new DataKind(Photo.CONTENT_ITEM_TYPE, -1, -1, -1, true);
+ kind.typeOverallMax = 1;
+ list.add(kind);
+ }
+
+ {
+ // EXCHANGE: PHONE
+ DataKind kind = new DataKind(Phone.CONTENT_ITEM_TYPE,
+ R.string.phoneLabelsGroup, android.R.drawable.sym_action_call, 10, true);
+
+ kind.actionHeader = new ActionLabelInflater(R.string.actionCall, kind);
+ kind.actionBody = new ColumnInflater(Phone.NUMBER);
+
+ kind.typeColumn = Phone.TYPE;
+ kind.typeList = new ArrayList<EditType>();
+ kind.typeList.add(new EditType(Phone.TYPE_HOME, R.string.type_home, false, 1));
+ kind.typeList.add(new EditType(TYPE_HOME2, R.string.type_home_2, true, 1));
+ kind.typeList.add(new EditType(Phone.TYPE_MOBILE, R.string.type_mobile, false, 1));
+ kind.typeList.add(new EditType(Phone.TYPE_WORK, R.string.type_work, false, 1));
+ kind.typeList.add(new EditType(TYPE_WORK2, R.string.type_work_2, true, 1));
+ kind.typeList.add(new EditType(Phone.TYPE_FAX_WORK, R.string.type_fax_work, true, 1));
+ kind.typeList.add(new EditType(Phone.TYPE_FAX_HOME, R.string.type_fax_home, true, 1));
+ kind.typeList.add(new EditType(Phone.TYPE_PAGER, R.string.type_pager, true, 1));
+ kind.typeList.add(new EditType(TYPE_CAR, R.string.type_car, true, 1));
+ kind.typeList.add(new EditType(TYPE_COMPANY_MAIN, R.string.type_company_main, true, 1));
+ kind.typeList.add(new EditType(TYPE_MMS, R.string.type_mms, true, 1));
+ kind.typeList.add(new EditType(TYPE_RADIO, R.string.type_radio, true, 1));
+ kind.typeList.add(new EditType(Phone.TYPE_CUSTOM, R.string.type_assistant, true, 1,
+ Phone.LABEL));
+
+ kind.fieldList = new ArrayList<EditField>();
+ kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup));
+
+ list.add(kind);
+ }
+
+ {
+ // EXCHANGE: EMAIL
+ DataKind kind = new DataKind(Email.CONTENT_ITEM_TYPE,
+ R.string.emailLabelsGroup, android.R.drawable.sym_action_email, 15, true);
+
+ kind.actionHeader = new ActionLabelInflater(R.string.actionEmail, kind);
+ kind.actionBody = new ColumnInflater(Email.DATA);
+
+ kind.typeColumn = Email.TYPE;
+ kind.typeList = new ArrayList<EditType>();
+ kind.typeList.add(new EditType(TYPE_EMAIL1, R.string.type_email_1, false, 1));
+ kind.typeList.add(new EditType(TYPE_EMAIL2, R.string.type_email_2, false, 1));
+ kind.typeList.add(new EditType(TYPE_EMAIL3, R.string.type_email_3, false, 1));
+
+ kind.fieldList = new ArrayList<EditField>();
+ kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup));
+
+ list.add(kind);
+ }
+
+ {
+ // EXCHANGE: NICKNAME
+ DataKind kind = new DataKind(Nickname.CONTENT_ITEM_TYPE,
+ R.string.nicknameLabelsGroup, -1, 115, true);
+ kind.secondary = true;
+
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<EditField>();
+ kind.fieldList.add(new EditField(Nickname.NAME, R.string.nicknameLabelsGroup));
+
+ list.add(kind);
+ }
+
+ {
+ // EXCHANGE: WEBSITE
+ DataKind kind = new DataKind(Website.CONTENT_ITEM_TYPE,
+ R.string.websiteLabelsGroup, -1, 120, true);
+ kind.secondary = true;
+
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<EditField>();
+ kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup));
+
+ list.add(kind);
+ }
+
+ return list;
+ }
+
+ /**
+ * Simple inflater that assumes a string resource has a "%s" that will be
+ * filled from the given column.
+ */
+ public static class SimpleInflater implements StringInflater {
+ // TODO: implement this
+
+ public SimpleInflater(int stringRes, String columnName) {
+ }
+
+ public CharSequence inflateUsing(Cursor cursor) {
+ return null;
+ }
+ }
+
+ /**
+ * Simple inflater that will combine two string resources, usually to
+ * provide an action string like "Call home", where "home" is provided from
+ * {@link EditType#labelRes}.
+ */
+ public static class ActionLabelInflater implements StringInflater {
+ // TODO: implement this
+
+ public ActionLabelInflater(int actionRes, DataKind labelProvider) {
+ }
+
+ public CharSequence inflateUsing(Cursor cursor) {
+ // use the given action string along with localized label name
+ return null;
+ }
+ }
+
+ /**
+ * Simple inflater that uses the raw value from the given column.
+ */
+ public static class ColumnInflater implements StringInflater {
+ // TODO: implement this
+
+ public ColumnInflater(String columnName) {
+ }
+
+ public CharSequence inflateUsing(Cursor cursor) {
+ // return the cursor value for column name
+ return null;
+ }
+ }
+
+}
diff --git a/src/com/android/contacts/ui/EditContactActivity.java b/src/com/android/contacts/ui/EditContactActivity.java
new file mode 100644
index 0000000..abb164e
--- /dev/null
+++ b/src/com/android/contacts/ui/EditContactActivity.java
@@ -0,0 +1,850 @@
+/*
+ * 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.ui;
+
+import com.android.contacts.R;
+import com.android.contacts.model.AugmentedEntity;
+import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.Sources;
+import com.android.contacts.ui.widget.ContactEditorView;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Entity;
+import android.content.EntityIterator;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+
+/**
+ * Activity for editing or inserting a contact.
+ */
+public final class EditContactActivity extends Activity implements View.OnClickListener,
+ View.OnFocusChangeListener {
+
+ // TODO: with new augmentedentity approach, turn insert and update cases into same action
+
+ private static final String TAG = "EditContactActivity";
+
+ /** The launch code when picking a photo and the raw data is returned */
+ private static final int PHOTO_PICKED_WITH_DATA = 3021;
+
+ // Dialog IDs
+ final static int DELETE_CONFIRMATION_DIALOG = 2;
+
+ // Menu item IDs
+ public static final int MENU_ITEM_DONE = 1;
+ public static final int MENU_ITEM_REVERT = 2;
+ public static final int MENU_ITEM_PHOTO = 6;
+
+
+ private LayoutInflater mInflater;
+ private ViewGroup mContentView;
+
+// private MenuItem mPhotoMenuItem;
+// private boolean mPhotoPresent = false;
+
+ /** Flag marking this contact as changed, meaning we should write changes back. */
+// private boolean mContactChanged = false;
+
+ private Uri mUri;
+ private ArrayList<AugmentedEntity> mEntities = new ArrayList<AugmentedEntity>();
+
+ private ContentResolver mResolver;
+ private ContactEditorView mEditor;
+
+ private ViewGroup mTabContent;
+
+ // we edit an aggregate, which has several entities
+ // we query and build AugmentedEntities, which is what ContactEditorView expects
+
+
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ final Context context = this;
+
+ mInflater = getLayoutInflater();
+ mResolver = getContentResolver();
+
+ mContentView = (ViewGroup)mInflater.inflate(R.layout.act_edit, null);
+ mTabContent = (ViewGroup)mContentView.findViewById(android.R.id.tabcontent);
+
+ setContentView(mContentView);
+
+ // Setup floating buttons
+ findViewById(R.id.btn_done).setOnClickListener(this);
+ findViewById(R.id.btn_discard).setOnClickListener(this);
+
+
+
+ final Intent intent = getIntent();
+ final String action = intent.getAction();
+ final Bundle extras = intent.getExtras();
+
+ mUri = intent.getData();
+
+ // TODO: read all contacts part of this aggregate and hook into tabs
+
+ // Resolve the intent
+ if (Intent.ACTION_EDIT.equals(action) && mUri != null) {
+
+ try {
+ final long aggId = ContentUris.parseId(mUri);
+ final EntityIterator iterator = mResolver.queryEntities(
+ ContactsContract.RawContacts.CONTENT_URI,
+ ContactsContract.RawContacts.CONTACT_ID + "=" + aggId, null, null);
+ while (iterator.hasNext()) {
+ final Entity before = iterator.next();
+ final AugmentedEntity entity = AugmentedEntity.fromBefore(before);
+
+ mEntities.add(entity);
+
+ Log.d(TAG, "Loaded entity...");
+ }
+ iterator.close();
+ } catch (RemoteException e) {
+ Log.d(TAG, "Problem reading aggregate", e);
+ }
+
+// if (icicle == null) {
+// // Build the entries & views
+// buildEntriesForEdit(extras);
+// buildViews();
+// }
+ setTitle(R.string.editContact_title_edit);
+ } else if (Intent.ACTION_INSERT.equals(action)) {
+// if (icicle == null) {
+// // Build the entries & views
+// buildEntriesForInsert(extras);
+// buildViews();
+// }
+// setTitle(R.string.editContact_title_insert);
+// mState = STATE_INSERT;
+ }
+
+// if (mState == STATE_UNKNOWN) {
+// Log.e(TAG, "Cannot resolve intent: " + intent);
+// finish();
+// return;
+// }
+
+ mEditor = new ContactEditorView(context);
+
+ mTabContent.removeAllViews();
+ mTabContent.addView(mEditor.getView());
+
+ final ContactsSource source = Sources.getInstance().getKindsForAccountType(
+ Sources.ACCOUNT_TYPE_GOOGLE);
+ mEditor.setState(mEntities.get(0), source);
+
+
+ }
+
+
+ // TODO: build entity from incoming intent
+ // TODO: build entity from "new" action
+
+
+// private void addFromExtras(Bundle extras, Uri phonesUri, Uri methodsUri) {
+// EditEntry entry;
+//
+// // Read the name from the bundle
+// CharSequence name = extras.getCharSequence(Insert.NAME);
+// if (name != null && TextUtils.isGraphic(name)) {
+// mNameView.setText(name);
+// }
+//
+// // Read the phonetic name from the bundle
+// CharSequence phoneticName = extras.getCharSequence(Insert.PHONETIC_NAME);
+// if (!TextUtils.isEmpty(phoneticName)) {
+// mPhoneticNameView.setText(phoneticName);
+// }
+//
+// // StructuredPostal entries from extras
+// CharSequence postal = extras.getCharSequence(Insert.POSTAL);
+// int postalType = extras.getInt(Insert.POSTAL_TYPE, INVALID_TYPE);
+// if (!TextUtils.isEmpty(postal) && postalType == INVALID_TYPE) {
+// postalType = DEFAULT_POSTAL_TYPE;
+// }
+//
+// if (postalType != INVALID_TYPE) {
+// entry = EditEntry.newPostalEntry(this, null, postalType, postal.toString(),
+// methodsUri, 0);
+// entry.isPrimary = extras.getBoolean(Insert.POSTAL_ISPRIMARY);
+// mPostalEntries.add(entry);
+// }
+//
+// // Email entries from extras
+// addEmailFromExtras(extras, methodsUri, Insert.EMAIL, Insert.EMAIL_TYPE,
+// Insert.EMAIL_ISPRIMARY);
+// addEmailFromExtras(extras, methodsUri, Insert.SECONDARY_EMAIL, Insert.SECONDARY_EMAIL_TYPE,
+// null);
+// addEmailFromExtras(extras, methodsUri, Insert.TERTIARY_EMAIL, Insert.TERTIARY_EMAIL_TYPE,
+// null);
+//
+// // Phone entries from extras
+// addPhoneFromExtras(extras, phonesUri, Insert.PHONE, Insert.PHONE_TYPE,
+// Insert.PHONE_ISPRIMARY);
+// addPhoneFromExtras(extras, phonesUri, Insert.SECONDARY_PHONE, Insert.SECONDARY_PHONE_TYPE,
+// null);
+// addPhoneFromExtras(extras, phonesUri, Insert.TERTIARY_PHONE, Insert.TERTIARY_PHONE_TYPE,
+// null);
+//
+// // IM entries from extras
+// CharSequence imHandle = extras.getCharSequence(Insert.IM_HANDLE);
+// CharSequence imProtocol = extras.getCharSequence(Insert.IM_PROTOCOL);
+//
+// if (imHandle != null && imProtocol != null) {
+// Object protocolObj = ContactMethods.decodeImProtocol(imProtocol.toString());
+// if (protocolObj instanceof Number) {
+// int protocol = ((Number)F protocolObj).intValue();
+// entry = EditEntry.newImEntry(this,
+// getLabelsForKind(this, Contacts.KIND_IM)[protocol], protocol,
+// imHandle.toString(), methodsUri, 0);
+// } else {
+// entry = EditEntry.newImEntry(this, protocolObj.toString(), -1, imHandle.toString(),
+// methodsUri, 0);
+// }
+// entry.isPrimary = extras.getBoolean(Insert.IM_ISPRIMARY);
+// mImEntries.add(entry);
+// }
+// }
+//
+// private void addEmailFromExtras(Bundle extras, Uri methodsUri, String emailField,
+// String typeField, String primaryField) {
+// CharSequence email = extras.getCharSequence(emailField);
+//
+// // Correctly handle String in typeField as TYPE_CUSTOM
+// int emailType = INVALID_TYPE;
+// String customLabel = null;
+// if(extras.get(typeField) instanceof String) {
+// emailType = ContactsContract.TYPE_CUSTOM;
+// customLabel = extras.getString(typeField);
+// } else {
+// emailType = extras.getInt(typeField, INVALID_TYPE);
+// }
+//
+// if (!TextUtils.isEmpty(email) && emailType == INVALID_TYPE) {
+// emailType = DEFAULT_EMAIL_TYPE;
+// mPrimaryEmailAdded = true;
+// }
+//
+// if (emailType != INVALID_TYPE) {
+// EditEntry entry = EditEntry.newEmailEntry(this, customLabel, emailType, email.toString(),
+// methodsUri, 0);
+// entry.isPrimary = (primaryField == null) ? false : extras.getBoolean(primaryField);
+// mEmailEntries.add(entry);
+//
+// // Keep track of which primary types have been added
+// if (entry.isPrimary) {
+// mPrimaryEmailAdded = true;
+// }
+// }
+// }
+//
+// private void addPhoneFromExtras(Bundle extras, Uri phonesUri, String phoneField,
+// String typeField, String primaryField) {
+// CharSequence phoneNumber = extras.getCharSequence(phoneField);
+//
+// // Correctly handle String in typeField as TYPE_CUSTOM
+// int phoneType = INVALID_TYPE;
+// String customLabel = null;
+// if(extras.get(typeField) instanceof String) {
+// phoneType = Phone.TYPE_CUSTOM;
+// customLabel = extras.getString(typeField);
+// } else {
+// phoneType = extras.getInt(typeField, INVALID_TYPE);
+// }
+//
+// if (!TextUtils.isEmpty(phoneNumber) && phoneType == INVALID_TYPE) {
+// phoneType = DEFAULT_PHONE_TYPE;
+// }
+//
+// if (phoneType != INVALID_TYPE) {
+// EditEntry entry = EditEntry.newPhoneEntry(this, customLabel, phoneType,
+// phoneNumber.toString(), phonesUri, 0);
+// entry.isPrimary = (primaryField == null) ? false : extras.getBoolean(primaryField);
+// mPhoneEntries.add(entry);
+//
+// // Keep track of which primary types have been added
+// if (phoneType == Phone.TYPE_MOBILE) {
+// mMobilePhoneAdded = true;
+// }
+// }
+// }
+// */
+
+
+
+ public void onClick(View v) {
+ switch (v.getId()) {
+
+ case R.id.saveButton:
+ doSaveAction();
+ break;
+
+ case R.id.discardButton:
+ doRevertAction();
+ break;
+
+ }
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_BACK: {
+ doSaveAction();
+ return true;
+ }
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+
+ // TODO: store down uri, selected contactid, and pile of augmentedstates
+
+ // store down the focused tab and child data _id (what about storing as index?)
+
+ // tell sections to store their state, which also picks correct field and cursor location
+
+
+
+//
+// // To store current focus between config changes, follow focus down the
+// // view tree, keeping track of any parents with EditEntry tags
+// View focusedChild = mContentView.getFocusedChild();
+// EditEntry focusedEntry = null;
+// while (focusedChild != null) {
+// Object tag = focusedChild.getTag();
+// if (tag instanceof EditEntry) {
+// focusedEntry = (EditEntry) tag;
+// }
+//
+// // Keep going deeper until child isn't a group
+// if (focusedChild instanceof ViewGroup) {
+// View deeperFocus = ((ViewGroup) focusedChild).getFocusedChild();
+// if (deeperFocus != null) {
+// focusedChild = deeperFocus;
+// } else {
+// break;
+// }
+// } else {
+// break;
+// }
+// }
+//
+// if (focusedChild != null) {
+// int requestFocusId = focusedChild.getId();
+// int requestCursor = 0;
+// if (focusedChild instanceof EditText) {
+// requestCursor = ((EditText) focusedChild).getSelectionStart();
+// }
+//
+// // Store focus values in EditEntry if found, otherwise store as
+// // generic values
+// if (focusedEntry != null) {
+// focusedEntry.requestFocusId = requestFocusId;
+// focusedEntry.requestCursor = requestCursor;
+// } else {
+// outState.putInt("requestFocusId", requestFocusId);
+// outState.putInt("requestCursor", requestCursor);
+// }
+// }
+//
+// outState.putParcelableArrayList("phoneEntries", mPhoneEntries);
+// outState.putParcelableArrayList("emailEntries", mEmailEntries);
+// outState.putParcelableArrayList("imEntries", mImEntries);
+// outState.putParcelableArrayList("postalEntries", mPostalEntries);
+// outState.putParcelableArrayList("orgEntries", mOrgEntries);
+// outState.putParcelableArrayList("noteEntries", mNoteEntries);
+// outState.putParcelableArrayList("otherEntries", mOtherEntries);
+// outState.putInt("state", mState);
+// outState.putBoolean("insert", mInsert);
+// outState.putParcelable("uri", mUri);
+// outState.putString("name", mNameView.getText().toString());
+// outState.putParcelable("photo", mPhoto);
+// outState.putBoolean("photoChanged", mPhotoChanged);
+// outState.putString("phoneticName", mPhoneticNameView.getText().toString());
+// outState.putBoolean("contactChanged", mContactChanged);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle inState) {
+// mPhoneEntries = inState.getParcelableArrayList("phoneEntries");
+// mEmailEntries = inState.getParcelableArrayList("emailEntries");
+// mImEntries = inState.getParcelableArrayList("imEntries");
+// mPostalEntries = inState.getParcelableArrayList("postalEntries");
+// mOrgEntries = inState.getParcelableArrayList("orgEntries");
+// mNoteEntries = inState.getParcelableArrayList("noteEntries");
+// mOtherEntries = inState.getParcelableArrayList("otherEntries");
+// setupSections();
+//
+// mState = inState.getInt("state");
+// mInsert = inState.getBoolean("insert");
+// mUri = inState.getParcelable("uri");
+// mNameView.setText(inState.getString("name"));
+// mPhoto = inState.getParcelable("photo");
+// if (mPhoto != null) {
+// mPhotoImageView.setImageBitmap(mPhoto);
+// setPhotoPresent(true);
+// } else {
+// mPhotoImageView.setImageResource(R.drawable.ic_contact_picture);
+// setPhotoPresent(false);
+// }
+// mPhotoChanged = inState.getBoolean("photoChanged");
+// mPhoneticNameView.setText(inState.getString("phoneticName"));
+// mContactChanged = inState.getBoolean("contactChanged");
+//
+// // Now that everything is restored, build the view
+// buildViews();
+//
+// // Try restoring any generally requested focus
+// int requestFocusId = inState.getInt("requestFocusId", View.NO_ID);
+// View focusedChild = mContentView.findViewById(requestFocusId);
+// if (focusedChild != null) {
+// focusedChild.requestFocus();
+// if (focusedChild instanceof EditText) {
+// int requestCursor = inState.getInt("requestCursor", 0);
+// ((EditText) focusedChild).setSelection(requestCursor);
+// }
+// }
+ }
+
+
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (resultCode != RESULT_OK) {
+ return;
+ }
+
+ switch (requestCode) {
+ case PHOTO_PICKED_WITH_DATA: {
+// final Bundle extras = data.getExtras();
+// if (extras != null) {
+// Bitmap photo = extras.getParcelable("data");
+// mPhoto = photo;
+// mPhotoChanged = true;
+// mPhotoImageView.setImageBitmap(photo);
+// setPhotoPresent(true);
+// }
+// break;
+ }
+ }
+ }
+
+
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+// menu.add(0, MENU_ITEM_SAVE, 0, R.string.menu_done)
+// .setIcon(android.R.drawable.ic_menu_save)
+// .setAlphabeticShortcut('\n');
+// menu.add(0, MENU_ITEM_DONT_SAVE, 0, R.string.menu_doNotSave)
+// .setIcon(android.R.drawable.ic_menu_close_clear_cancel)
+// .setAlphabeticShortcut('q');
+// if (!mInsert) {
+// menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_deleteContact)
+// .setIcon(android.R.drawable.ic_menu_delete);
+// }
+//
+// mPhotoMenuItem = menu.add(0, MENU_ITEM_PHOTO, 0, null);
+// // Updates the state of the menu item
+// setPhotoPresent(mPhotoPresent);
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+// case MENU_ITEM_SAVE:
+// doSaveAction();
+// return true;
+//
+// case MENU_ITEM_DONT_SAVE:
+// doRevertAction();
+// return true;
+//
+// case MENU_ITEM_DELETE:
+// // Get confirmation
+// showDialog(DELETE_CONFIRMATION_DIALOG);
+// return true;
+//
+// case MENU_ITEM_PHOTO:
+// if (!mPhotoPresent) {
+// doPickPhotoAction();
+// } else {
+// doRemovePhotoAction();
+// }
+// return true;
+ }
+
+ return false;
+ }
+
+
+
+ private void doRevertAction() {
+ finish();
+ }
+
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case DELETE_CONFIRMATION_DIALOG:
+// return new AlertDialog.Builder(EditContactActivity.this)
+// .setTitle(R.string.deleteConfirmation_title)
+// .setIcon(android.R.drawable.ic_dialog_alert)
+// .setMessage(R.string.deleteConfirmation)
+// .setNegativeButton(android.R.string.cancel, null)
+// .setPositiveButton(android.R.string.ok, mDeleteContactDialogListener)
+// .setCancelable(false)
+// .create();
+ }
+ return super.onCreateDialog(id);
+ }
+
+
+ /**
+ * Saves or creates the contact based on the mode, and if sucessful finishes the activity.
+ */
+ private void doSaveAction() {
+ // Save or create the contact if needed
+// switch (mState) {
+// case STATE_EDIT:
+// save();
+// break;
+//
+// /*
+// case STATE_INSERT:
+// create();
+// break;
+// */
+//
+// default:
+// Log.e(TAG, "Unknown state in doSaveOrCreate: " + mState);
+// break;
+// }
+ finish();
+ }
+
+
+ /**
+ * Save the various fields to the existing contact.
+ */
+ private void save() {
+// ContentValues values = new ContentValues();
+// String data;
+// int numValues = 0;
+//
+// // Handle the name and send to voicemail specially
+// final String name = mNameView.getText().toString();
+// if (name != null && TextUtils.isGraphic(name)) {
+// numValues++;
+// }
+//
+// values.put(StructuredName.DISPLAY_NAME, name);
+// /*
+// values.put(People.PHONETIC_NAME, mPhoneticNameView.getText().toString());
+// */
+// mResolver.update(mStructuredNameUri, values, null, null);
+//
+// // This will go down in for loop somewhere
+// if (mPhotoChanged) {
+// // Only write the photo if it's changed, since we don't initially load mPhoto
+// values.clear();
+// if (mPhoto != null) {
+// ByteArrayOutputStream stream = new ByteArrayOutputStream();
+// mPhoto.compress(Bitmap.CompressFormat.JPEG, 75, stream);
+// values.put(Photo.PHOTO, stream.toByteArray());
+// mResolver.update(mPhotoDataUri, values, null, null);
+// } else {
+// values.putNull(Photo.PHOTO);
+// mResolver.update(mPhotoDataUri, values, null, null);
+// }
+// }
+//
+// int entryCount = ContactEntryAdapter.countEntries(mSections, false);
+// for (int i = 0; i < entryCount; i++) {
+// EditEntry entry = ContactEntryAdapter.getEntry(mSections, i, false);
+// data = entry.getData();
+// boolean empty = data == null || !TextUtils.isGraphic(data);
+// /*
+// if (kind == EditEntry.KIND_GROUP) {
+// if (entry.id != 0) {
+// for (int g = 0; g < mGroups.length; g++) {
+// long groupId = getGroupId(mResolver, mGroups[g].toString());
+// if (mInTheGroup[g]) {
+// Contacts.People.addToGroup(mResolver, entry.id, groupId);
+// numValues++;
+// } else {
+// deleteGroupMembership(entry.id, groupId);
+// }
+// }
+// }
+// }
+// */
+// if (!empty) {
+// values.clear();
+// entry.toValues(values);
+// if (entry.id != 0) {
+// mResolver.update(entry.uri, values, null, null);
+// } else {
+// /* mResolver.insert(entry.uri, values); */
+// }
+// } else if (entry.id != 0) {
+// mResolver.delete(entry.uri, null, null);
+// }
+// }
+//
+// /*
+// if (numValues == 0) {
+// // The contact is completely empty, delete it
+// mResolver.delete(mUri, null, null);
+// mUri = null;
+// setResult(RESULT_CANCELED);
+// } else {
+// // Add the entry to the my contacts group if it isn't there already
+// People.addToMyContactsGroup(mResolver, ContentUris.parseId(mUri));
+// setResult(RESULT_OK, new Intent().setData(mUri));
+//
+// // Only notify user if we actually changed contact
+// if (mContactChanged || mPhotoChanged) {
+// Toast.makeText(this, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
+// }
+// }
+// */
+ }
+
+ /**
+ * Takes the entered data and saves it to a new contact.
+ */
+ /*
+ private void create() {
+ ContentValues values = new ContentValues();
+ String data;
+ int numValues = 0;
+
+ // Create the contact itself
+ final String name = mNameView.getText().toString();
+ if (name != null && TextUtils.isGraphic(name)) {
+ numValues++;
+ }
+ values.put(People.NAME, name);
+ values.put(People.PHONETIC_NAME, mPhoneticNameView.getText().toString());
+
+ // Add the contact to the My Contacts group
+ Uri contactUri = People.createPersonInMyContactsGroup(mResolver, values);
+
+ // Add the contact to the group that is being displayed in the contact list
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ int displayType = prefs.getInt(ContactsListActivity.PREF_DISPLAY_TYPE,
+ ContactsListActivity.DISPLAY_TYPE_UNKNOWN);
+ if (displayType == ContactsListActivity.DISPLAY_TYPE_USER_GROUP) {
+ String displayGroup = prefs.getString(ContactsListActivity.PREF_DISPLAY_INFO,
+ null);
+ if (!TextUtils.isEmpty(displayGroup)) {
+ People.addToGroup(mResolver, ContentUris.parseId(contactUri), displayGroup);
+ }
+ } else {
+ // Check to see if we're not syncing everything and if so if My Contacts is synced.
+ // If it isn't then the created contact can end up not in any groups that are
+ // currently synced and end up getting removed from the phone, which is really bad.
+ boolean syncingEverything = !"0".equals(Contacts.Settings.getSetting(mResolver, null,
+ Contacts.Settings.SYNC_EVERYTHING));
+ if (!syncingEverything) {
+ boolean syncingMyContacts = false;
+ Cursor c = mResolver.query(Groups.CONTENT_URI, new String[] { Groups.SHOULD_SYNC },
+ Groups.SYSTEM_ID + "=?", new String[] { Groups.GROUP_MY_CONTACTS }, null);
+ if (c != null) {
+ try {
+ if (c.moveToFirst()) {
+ syncingMyContacts = !"0".equals(c.getString(0));
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ if (!syncingMyContacts) {
+ // Not syncing My Contacts, so find a group that is being synced and stick
+ // the contact in there. We sort the list so at least all contacts
+ // will appear in the same group.
+ c = mResolver.query(Groups.CONTENT_URI, new String[] { Groups._ID },
+ Groups.SHOULD_SYNC + "!=0", null, Groups.DEFAULT_SORT_ORDER);
+ if (c != null) {
+ try {
+ if (c.moveToFirst()) {
+ People.addToGroup(mResolver, ContentUris.parseId(contactUri),
+ c.getLong(0));
+ }
+ } finally {
+ c.close();
+ }
+ }
+ }
+ }
+ }
+
+ // Handle the photo
+ if (mPhoto != null) {
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ mPhoto.compress(Bitmap.CompressFormat.JPEG, 75, stream);
+ Contacts.People.setPhotoData(getContentResolver(), contactUri, stream.toByteArray());
+ }
+
+ // Create the contact methods
+ int entryCount = ContactEntryAdapter.countEntries(mSections, false);
+ for (int i = 0; i < entryCount; i++) {
+ EditEntry entry = ContactEntryAdapter.getEntry(mSections, i, false);
+ if (entry.kind == EditEntry.KIND_GROUP) {
+ long contactId = ContentUris.parseId(contactUri);
+ for (int g = 0; g < mGroups.length; g++) {
+ if (mInTheGroup[g]) {
+ long groupId = getGroupId(mResolver, mGroups[g].toString());
+ People.addToGroup(mResolver, contactId, groupId);
+ numValues++;
+ }
+ }
+ } else if (entry.kind != EditEntry.KIND_CONTACT) {
+ values.clear();
+ if (entry.toValues(values)) {
+ // Only create the entry if there is data
+ entry.uri = mResolver.insert(
+ Uri.withAppendedPath(contactUri, entry.contentDirectory), values);
+ entry.id = ContentUris.parseId(entry.uri);
+ }
+ } else {
+ // Update the contact with any straggling data, like notes
+ data = entry.getData();
+ values.clear();
+ if (data != null && TextUtils.isGraphic(data)) {
+ values.put(entry.column, data);
+ mResolver.update(contactUri, values, null, null);
+ }
+ }
+ }
+
+ if (numValues == 0) {
+ mResolver.delete(contactUri, null, null);
+ setResult(RESULT_CANCELED);
+ } else {
+ mUri = contactUri;
+ Intent resultIntent = new Intent()
+ .setData(mUri)
+ .putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
+ setResult(RESULT_OK, resultIntent);
+ Toast.makeText(this, R.string.contactCreatedToast, Toast.LENGTH_SHORT).show();
+ }
+ }
+ */
+
+
+
+// /**
+// * Builds the views for a specific section.
+// *
+// * @param layout the container
+// * @param section the section to build the views for
+// */
+// private void buildViewsForSection(final LinearLayout layout, ArrayList<EditEntry> section,
+// int separatorResource, int sectionType) {
+//
+// View divider = mInflater.inflate(R.layout.edit_divider, layout, false);
+// layout.addView(divider);
+//
+// // Count up undeleted children
+// int activeChildren = 0;
+// for (int i = section.size() - 1; i >= 0; i--) {
+// EditEntry entry = section.get(i);
+// if (!entry.isDeleted) {
+// activeChildren++;
+// }
+// }
+//
+// // Build the correct group header based on undeleted children
+// ViewGroup header;
+// if (activeChildren == 0) {
+// header = (ViewGroup) mInflater.inflate(R.layout.edit_separator_alone, layout, false);
+// } else {
+// header = (ViewGroup) mInflater.inflate(R.layout.edit_separator, layout, false);
+// }
+//
+// // Because we're emulating a ListView, we need to handle focus changes
+// // with some additional logic.
+// header.setOnFocusChangeListener(this);
+//
+// TextView text = (TextView) header.findViewById(R.id.text);
+// text.setText(getText(separatorResource));
+//
+// // Force TextView to always default color if we have children. This makes sure
+// // we don't change color when parent is pressed.
+// if (activeChildren > 0) {
+// ColorStateList stateList = text.getTextColors();
+// text.setTextColor(stateList.getDefaultColor());
+// }
+//
+// View addView = header.findViewById(R.id.separator);
+// addView.setTag(Integer.valueOf(sectionType));
+// addView.setOnClickListener(this);
+//
+// // Build views for the current section
+// for (EditEntry entry : section) {
+// entry.activity = this; // this could be null from when the state is restored
+// if (!entry.isDeleted) {
+// View view = buildViewForEntry(entry);
+// header.addView(view);
+// }
+// }
+//
+// layout.addView(header);
+// }
+
+
+
+
+ public void onFocusChange(View v, boolean hasFocus) {
+ // Because we're emulating a ListView, we need to setSelected() for
+ // views as they are focused.
+ v.setSelected(hasFocus);
+ }
+}
diff --git a/src/com/android/contacts/ui/widget/ContactEditorView.java b/src/com/android/contacts/ui/widget/ContactEditorView.java
new file mode 100644
index 0000000..c0c7fb7
--- /dev/null
+++ b/src/com/android/contacts/ui/widget/ContactEditorView.java
@@ -0,0 +1,465 @@
+/*
+ * 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.ui.widget;
+
+import com.android.contacts.R;
+import com.android.contacts.model.AugmentedEntity;
+import com.android.contacts.model.EntityModifier;
+import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.AugmentedEntity.AugmentedValues;
+import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.model.ContactsSource.EditField;
+import com.android.contacts.model.ContactsSource.EditType;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Entity;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.ContextThemeWrapper;
+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.ListAdapter;
+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 AugmentedEntity}. Callers can
+ * reuse this view and quickly rebuild its contents through
+ * {@link #setState(AugmentedEntity, ContactsSource)}.
+ * <p>
+ * Internal updates are performed against {@link AugmentedValues} so that the
+ * source {@link Entity} can be swapped out. Any state-based changes, such as
+ * adding {@link Data} rows or changing {@link EditType}, are performed through
+ * {@link EntityModifier} to ensure that {@link ContactsSource} are enforced.
+ */
+public class ContactEditorView extends ViewHolder {
+ private static final int RES_CONTENT = R.layout.act_edit_contact;
+
+ private PhotoEditor mPhoto;
+ private DisplayNameEditor mDisplayName;
+
+ private ViewGroup mGeneral;
+ private ViewGroup mSecondary;
+
+ public ContactEditorView(Context context) {
+ super(context, RES_CONTENT);
+
+ mGeneral = (ViewGroup)mContent.findViewById(R.id.sect_general);
+ mSecondary = (ViewGroup)mContent.findViewById(R.id.sect_secondary);
+
+ mPhoto = new PhotoEditor(context);
+ mDisplayName = new DisplayNameEditor(context);
+ }
+
+ /**
+ * Set the internal state for this view, given a current
+ * {@link AugmentedEntity} state and the {@link ContactsSource} that
+ * apply to that state.
+ */
+ public void setState(AugmentedEntity state, ContactsSource source) {
+ // Remove any existing sections
+ mGeneral.removeAllViews();
+ mSecondary.removeAllViews();
+
+ // Create editor sections for each possible data kind
+ for (DataKind kind : source.getSortedDataKinds()) {
+ // Skip kind of not editable
+ if (!kind.editable) continue;
+
+ final String mimeType = kind.mimeType;
+ if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ // Handle special case editor for structured name
+ final AugmentedValues primary = state.getPrimaryEntry(mimeType);
+ mDisplayName.setValues(null, primary, state);
+ } else if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ // Handle special case editor for photos
+ final AugmentedValues firstValue = state.getPrimaryEntry(mimeType);
+ mPhoto.setValues(null, firstValue, state);
+ } else {
+ // Otherwise use generic section-based editors
+ if (kind.fieldList == null) continue;
+ final KindSection section = new KindSection(mContext, kind, state);
+ if (kind.secondary) {
+ mSecondary.addView(section.getView());
+ } else {
+ mGeneral.addView(section.getView());
+ }
+ }
+ }
+ }
+
+ /**
+ * Custom view for an entire section of data as segmented by
+ * {@link DataKind} around a {@link Data#MIMETYPE}. This view shows a
+ * section header and a trigger for adding new {@link Data} rows.
+ */
+ public static class KindSection extends ViewHolder implements OnClickListener {
+ private static final int RES_SECTION = R.layout.item_edit_kind;
+
+ private ViewGroup mEditors;
+ private View mAdd;
+ private TextView mTitle;
+
+ private DataKind mKind;
+ private AugmentedEntity mState;
+
+ public KindSection(Context context, DataKind kind, AugmentedEntity state) {
+ super(context, RES_SECTION);
+
+ mKind = kind;
+ mState = state;
+
+ mEditors = (ViewGroup)mContent.findViewById(R.id.kind_editors);
+
+ mAdd = mContent.findViewById(R.id.kind_header);
+ mAdd.setOnClickListener(this);
+
+ mTitle = (TextView)mContent.findViewById(R.id.kind_title);
+ mTitle.setText(kind.titleRes);
+
+ rebuildFromState();
+ }
+
+ /**
+ * Build editors for all current {@link #mState} rows.
+ */
+ public void rebuildFromState() {
+ // TODO: build special "stub" entries to help enter first-phone or first-email
+ // TODO: set the add-enabled state based on entitymodifier
+
+ // Remove any existing editors
+ mEditors.removeAllViews();
+
+ // Build individual editors for each entry
+ if (!mState.hasMimeEntries(mKind.mimeType)) return;
+ for (AugmentedValues entry : mState.getMimeEntries(mKind.mimeType)) {
+ final GenericEditor editor = new GenericEditor(mContext);
+ editor.setValues(mKind, entry, mState);
+ mEditors.addView(editor.getView());
+ }
+ }
+
+ public void onClick(View v) {
+ // Insert a new child and rebuild
+ EntityModifier.insertChild(mState, mKind);
+ rebuildFromState();
+ }
+ }
+
+ /**
+ * Generic definition of something that edits a {@link Data} row through an
+ * {@link AugmentedValues} object.
+ */
+ public interface Editor {
+ /**
+ * Prepare this editor for the given {@link AugmentedValues}, which
+ * builds any needed views. Any changes performed by the user will be
+ * written back to that same object.
+ */
+ public void setValues(DataKind kind, AugmentedValues values, AugmentedEntity state);
+ }
+
+ /**
+ * Simple editor that handles labels and any {@link EditField} defined for
+ * the entry. Uses {@link AugmentedValues} to read any existing
+ * {@link Entity} values, and to correctly write any changes values.
+ */
+ public static class GenericEditor extends ViewHolder implements Editor, OnClickListener {
+ private static final int RES_EDITOR = R.layout.item_editor;
+ private static final int RES_FIELD = R.layout.item_editor_field;
+ private static final int RES_LABEL_ITEM = android.R.layout.simple_list_item_1;
+
+ private TextView mLabel;
+ private ViewGroup mFields;
+ private View mDelete;
+
+ private DataKind mKind;
+ private AugmentedValues mEntry;
+ private AugmentedEntity mState;
+
+ private EditType mType;
+
+ public GenericEditor(Context context) {
+ super(context, RES_EDITOR);
+
+ mLabel = (TextView)mContent.findViewById(R.id.edit_label);
+ mLabel.setOnClickListener(this);
+
+ mFields = (ViewGroup)mContent.findViewById(R.id.edit_fields);
+
+ mDelete = mContent.findViewById(R.id.edit_delete);
+ mDelete.setOnClickListener(this);
+ }
+
+ /**
+ * Build the current label state based on selected {@link EditType} and
+ * possible custom label string.
+ */
+ private void rebuildLabel() {
+ if (mType.customColumn != null) {
+ // Use custom label string when present
+ final String customText = mEntry.getAsString(mType.customColumn);
+ if (customText != null) {
+ mLabel.setText(customText);
+ return;
+ }
+ }
+
+ // Otherwise fall back to using default label
+ mLabel.setText(mType.labelRes);
+ }
+
+ public void setValues(DataKind kind, AugmentedValues entry, AugmentedEntity state) {
+ mKind = kind;
+ mEntry = entry;
+ mState = state;
+
+ if (entry.isDeleted()) {
+ // Hide ourselves entirely if deleted
+ mContent.setVisibility(View.GONE);
+ return;
+ } else {
+ mContent.setVisibility(View.VISIBLE);
+ }
+
+ // Display label selector if multiple types available
+ final boolean hasTypes = EntityModifier.hasEditTypes(kind);
+ mLabel.setVisibility(hasTypes ? View.VISIBLE : View.GONE);
+ if (hasTypes) {
+ mType = EntityModifier.getCurrentType(entry, kind);
+ rebuildLabel();
+ }
+
+ // Build out set of fields
+ mFields.removeAllViews();
+ for (EditField field : kind.fieldList) {
+ // Inflate field from definition
+ EditText fieldView = (EditText)mInflater.inflate(RES_FIELD, mFields, false);
+ fieldView.setHint(field.titleRes);
+ fieldView.setInputType(field.inputType);
+ fieldView.setMinLines(field.minLines);
+
+ // Read current value from state
+ final String column = field.column;
+ final String value = entry.getAsString(column);
+ fieldView.setText(value);
+
+ // Prepare listener for writing changes
+ fieldView.addTextChangedListener(new TextWatcher() {
+ public void afterTextChanged(Editable s) {
+ // Write the newly changed value
+ mEntry.put(column, s.toString());
+ }
+
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+ });
+
+ // Hide field when empty and optional value
+ boolean shouldHide = (value == null && field.optional);
+ fieldView.setVisibility(shouldHide ? View.GONE : View.VISIBLE);
+
+ mFields.addView(fieldView);
+ }
+ }
+
+ private static final int INPUT_TYPE_CUSTOM = EditorInfo.TYPE_CLASS_TEXT
+ | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
+
+ /**
+ * Show dialog for entering a custom label.
+ */
+ private void showCustomDialog() {
+ final EditText customType = new EditText(mContext);
+ customType.setInputType(INPUT_TYPE_CUSTOM);
+ customType.requestFocus();
+
+ final DialogInterface.OnClickListener clickPositive = new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ final String customText = customType.getText().toString();
+ mEntry.put(mType.customColumn, customText);
+ rebuildLabel();
+ }
+ };
+
+ // TODO: handle canceled case by reverting to previous type?
+
+ new AlertDialog.Builder(mContext).setView(customType).setTitle(
+ R.string.customLabelPickerTitle).setPositiveButton(android.R.string.ok,
+ clickPositive).setNegativeButton(android.R.string.cancel, null).show();
+ }
+
+ /**
+ * Show dialog for picking a new {@link EditType} or entering a custom
+ * label. This dialog is limited to the valid types as determined by
+ * {@link EntityModifier}.
+ */
+ private void showTypeDialog() {
+ // Build list of valid types, including the current value
+ final List<EditType> validTypes = EntityModifier.getValidTypes(mState, mKind, mType);
+
+ final ListAdapter typeAdapter = new ArrayAdapter<EditType>(mContext, RES_LABEL_ITEM,
+ validTypes) {
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = mInflater.inflate(RES_LABEL_ITEM, parent, false);
+ }
+
+ final EditType type = this.getItem(position);
+ final TextView textView = (TextView)convertView;
+ textView.setText(type.labelRes);
+ return textView;
+ }
+ };
+
+ final DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+
+ // User picked type, so write to entry
+ mType = validTypes.get(which);
+ mEntry.put(mKind.typeColumn, mType.rawValue);
+
+ if (mType.customColumn != null) {
+ // Show custom label dialog if requested by type
+ showCustomDialog();
+ } else {
+ rebuildLabel();
+ }
+ }
+ };
+
+ // Wrap our context to inflate list items using correct theme
+ final Context dialogContext = new ContextThemeWrapper(mContext,
+ android.R.style.Theme_Black);
+
+ new AlertDialog.Builder(mContext).setSingleChoiceItems(typeAdapter, 0, clickListener)
+ .setTitle(R.string.selectLabel).show();
+ }
+
+
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.edit_label: {
+ showTypeDialog();
+ break;
+ }
+ case R.id.edit_delete: {
+ // Mark as deleted and hide this editor
+ mEntry.markDeleted();
+ mContent.setVisibility(View.GONE);
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Simple editor for {@link Photo}.
+ */
+ public static class PhotoEditor extends ViewHolder implements Editor {
+ private static final int RES_PHOTO = R.layout.item_editor_photo;
+
+ public PhotoEditor(Context context) {
+ super(context, RES_PHOTO);
+ }
+
+// private void setPhotoPresent(boolean present) {
+// mPhotoPresent = present;
+//
+// // Correctly scale the contact photo if present, otherwise just center
+// // the photo placeholder icon.
+// if (mPhotoPresent) {
+// mPhotoImageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
+// } else {
+// mPhotoImageView.setImageResource(R.drawable.ic_menu_add_picture);
+// mPhotoImageView.setScaleType(ImageView.ScaleType.CENTER);
+// }
+//
+// if (mPhotoMenuItem != null) {
+// if (present) {
+// mPhotoMenuItem.setTitle(R.string.removePicture);
+// mPhotoMenuItem.setIcon(android.R.drawable.ic_menu_delete);
+// } else {
+// mPhotoMenuItem.setTitle(R.string.addPicture);
+// mPhotoMenuItem.setIcon(R.drawable.ic_menu_add_picture);
+// }
+// }
+// }
+
+// private void doPickPhotoAction() {
+// Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
+// // TODO: get these values from constants somewhere
+// intent.setType("image/*");
+// intent.putExtra("crop", "true");
+// intent.putExtra("aspectX", 1);
+// intent.putExtra("aspectY", 1);
+// intent.putExtra("outputX", 96);
+// intent.putExtra("outputY", 96);
+// try {
+// intent.putExtra("return-data", true);
+// 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();
+// }
+// }
+
+// private void doRemovePhotoAction() {
+// mPhoto = null;
+// mPhotoChanged = true;
+// setPhotoPresent(false);
+// }
+
+ public void setValues(DataKind kind, AugmentedValues values, AugmentedEntity state) {
+ }
+ }
+
+ /**
+ * Simple editor for {@link StructuredName}.
+ */
+ public static class DisplayNameEditor extends ViewHolder implements Editor {
+ private static final int RES_DISPLAY_NAME = R.layout.item_editor_displayname;
+
+ public DisplayNameEditor(Context context) {
+ super(context, RES_DISPLAY_NAME);
+ }
+
+ public void setValues(DataKind kind, AugmentedValues values, AugmentedEntity state) {
+ }
+ }
+
+}
diff --git a/src/com/android/contacts/ui/widget/ViewHolder.java b/src/com/android/contacts/ui/widget/ViewHolder.java
new file mode 100644
index 0000000..2dec441
--- /dev/null
+++ b/src/com/android/contacts/ui/widget/ViewHolder.java
@@ -0,0 +1,40 @@
+/*
+ * 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.ui.widget;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+
+/**
+ * Helper to inflate a given layout and produce the {@link View} when requested.
+ */
+public class ViewHolder {
+ protected Context mContext;
+ protected LayoutInflater mInflater;
+ protected View mContent;
+
+ public ViewHolder(Context context, int layoutRes) {
+ mContext = context;
+ mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mContent = mInflater.inflate(layoutRes, null);
+ }
+
+ public View getView() {
+ return mContent;
+ }
+}