Merge "Refactoring ContactEntryListFragment/Adapter"
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 307b6cc..24dfe86 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -400,7 +400,8 @@
         <!-- Edit and insert details for a contact -->
         <activity
             android:name=".activities.ContactEditorActivity"
-            android:theme="@style/TallTitleBarTheme">
+            android:theme="@style/TallTitleBarTheme"
+            android:windowSoftInputMode="adjustResize">
 
             <intent-filter android:label="@string/editContactDescription">
                 <action android:name="android.intent.action.EDIT" />
@@ -525,6 +526,7 @@
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
                 <data android:mimeType="text/directory" />
+                <data android:mimeType="text/vcard" />
                 <data android:mimeType="text/x-vcard" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
@@ -533,13 +535,13 @@
         <activity android:name=".vcard.SelectAccountActivity"
             android:theme="@style/BackgroundOnly" />
 
-        <service
-            android:name=".vcard.ImportVCardService"
-            android:exported="false" />
-
         <activity android:name=".vcard.ExportVCardActivity"
             android:theme="@style/BackgroundOnly" />
 
+        <service
+            android:name=".vcard.VCardService"
+            android:exported="false" />
+
         <!-- Pinned header list demo -->
         <activity android:name=".widget.PinnedHeaderListDemoActivity">
             <intent-filter>
diff --git a/res/layout/contact_editor_fragment.xml b/res/layout/contact_editor_fragment.xml
index 3324d35..0055956 100644
--- a/res/layout/contact_editor_fragment.xml
+++ b/res/layout/contact_editor_fragment.xml
@@ -25,11 +25,15 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"/>
 
-    <ListView android:id="@android:id/list"
-        android:layout_width="match_parent"
-        android:layout_height="0px"
-        android:layout_weight="1"
-        android:background="@drawable/title_bar_shadow"
-    />
+    <ScrollView
+            android:layout_width="match_parent"
+            android:layout_height="0px"
+            android:layout_weight="1">
+        <com.android.contacts.views.editor.MyListView android:id="@android:id/list"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="@drawable/title_bar_shadow"
+        />
+    </ScrollView>
 </LinearLayout>
 
diff --git a/res/layout/list_edit_item_field_and_type.xml b/res/layout/list_edit_item_field_and_type.xml
new file mode 100644
index 0000000..ac43ceb
--- /dev/null
+++ b/res/layout/list_edit_item_field_and_type.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright 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.
+ */
+-->
+
+<com.android.contacts.views.editor.view.FieldAndTypeView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:orientation="horizontal"
+    android:paddingLeft="9dip"
+    android:gravity="center_vertical"
+    android:background="@drawable/edit_rawcontact_bg"
+>
+
+    <TextView android:id="@+id/caption"
+        android:layout_width="60dip"
+        android:layout_height="wrap_content"
+    />
+
+    <EditText android:id="@+id/field"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+    />
+
+    <Button android:id="@+id/type"
+        android:layout_width="100dip"
+        android:layout_height="wrap_content"
+    />
+
+</com.android.contacts.views.editor.view.FieldAndTypeView>
diff --git a/res/layout/list_edit_item_footer.xml b/res/layout/list_edit_item_footer.xml
index cf53ceb..4ad3638 100644
--- a/res/layout/list_edit_item_footer.xml
+++ b/res/layout/list_edit_item_footer.xml
@@ -17,7 +17,7 @@
  */
 -->
 
-<com.android.contacts.views.editor.typeViews.FooterView
+<com.android.contacts.views.editor.view.FooterView
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
@@ -46,4 +46,4 @@
         android:text="@string/edit_delete_rawcontact"
     />
 
-</com.android.contacts.views.editor.typeViews.FooterView>
+</com.android.contacts.views.editor.view.FooterView>
diff --git a/res/layout/list_edit_item_header.xml b/res/layout/list_edit_item_header.xml
index d2f9317..aa33a78 100644
--- a/res/layout/list_edit_item_header.xml
+++ b/res/layout/list_edit_item_header.xml
@@ -17,7 +17,7 @@
  */
 -->
 
-<com.android.contacts.views.editor.typeViews.HeaderView
+<com.android.contacts.views.editor.view.HeaderView
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
@@ -42,4 +42,4 @@
         android:scaleType="center"
         android:background="@android:drawable/list_selector_background"
     />
-</com.android.contacts.views.editor.typeViews.HeaderView>
+</com.android.contacts.views.editor.view.HeaderView>
diff --git a/res/layout/list_edit_item_photo.xml b/res/layout/list_edit_item_photo.xml
index c8f0cdf..9e7951f 100644
--- a/res/layout/list_edit_item_photo.xml
+++ b/res/layout/list_edit_item_photo.xml
@@ -17,7 +17,7 @@
  */
 -->
 
-<com.android.contacts.views.editor.typeViews.PhotoView
+<com.android.contacts.views.editor.view.PhotoView
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
@@ -69,4 +69,4 @@
         android:scaleType="center"
         android:background="@android:drawable/list_selector_background"
     />
-</com.android.contacts.views.editor.typeViews.PhotoView>
+</com.android.contacts.views.editor.view.PhotoView>
diff --git a/res/layout/list_edit_item_text_icons.xml b/res/layout/list_edit_item_text_icons.xml
index ab7b149..03868b7 100644
--- a/res/layout/list_edit_item_text_icons.xml
+++ b/res/layout/list_edit_item_text_icons.xml
@@ -17,7 +17,7 @@
  */
 -->
 
-<com.android.contacts.views.editor.typeViews.DataView
+<com.android.contacts.views.editor.view.DataView
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
@@ -96,4 +96,4 @@
         android:background="@android:drawable/list_selector_background"
     />
 
-</com.android.contacts.views.editor.typeViews.DataView>
+</com.android.contacts.views.editor.view.DataView>
diff --git a/res/layout/two_pane_activity.xml b/res/layout/two_pane_activity.xml
index bcf0ad8..407360b 100644
--- a/res/layout/two_pane_activity.xml
+++ b/res/layout/two_pane_activity.xml
@@ -24,21 +24,22 @@
         layout="@layout/search_bar"/>
 
     <LinearLayout
-        android:id="@+id/two_pane_activity"
         android:orientation="horizontal"
         android:layout_width="match_parent"
         android:layout_height="match_parent">
 
         <fragment android:name="com.android.contacts.list.DefaultContactBrowseListFragment"
                 android:id="@+id/two_pane_list"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
+                android:layout_width="0px"
+                android:layout_height="match_parent"
                 android:layout_weight="1" />
 
-        <fragment android:name="com.android.contacts.views.detail.ContactDetailFragment"
-                android:id="@+id/two_pane_detail"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_weight="1" />
+        <!--  Holder for detail- or editor-fragment. -->
+        <LinearLayout
+            android:id="@+id/two_pane_right_view"
+            android:orientation="horizontal"
+            android:layout_width="0px"
+            android:layout_height="match_parent"
+            android:layout_weight="1" />
   </LinearLayout>
 </LinearLayout>
diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml
index 9da8428..791d23e 100644
--- a/res/values-it/strings.xml
+++ b/res/values-it/strings.xml
@@ -162,7 +162,7 @@
     <string name="noContactsHelpTextWithSync" msgid="3734101165712848179">"Non sono presenti contatti da visualizzare (se hai appena aggiunto un account, la sincronizzazione dei contatti potrebbe richiedere alcuni minuti)."\n\n"Per aggiungere contatti, premi "<font fgcolor="#ffffffff"><b>"Menu"</b></font>" e tocca:"\n" "\n<li><font fgcolor="#ffffffff"><b>"Account"</b></font>" per aggiungere o configurare un account con contatti sincronizzabili con il telefono"\n</li>" "\n<li><font fgcolor="#ffffffff"><b>"Opzioni di visualizzazione"</b></font>" per modificare i contatti visibili"\n</li>" "\n<li><font fgcolor="#ffffffff"><b>"Nuovo contatto"</b></font>" per creare da zero un nuovo contatto"\n</li>" "\n<li><font fgcolor="#ffffffff"><b>"Importa/esporta"</b></font>\n</li></string>
     <string name="noContactsNoSimHelpText" msgid="6553845386917463292">"Non sono presenti contatti da visualizzare."\n\n"Per aggiungere contatti, premi "<font fgcolor="#ffffffff"><b>"Menu"</b></font>" e tocca:"\n" "\n<li><font fgcolor="#ffffffff"><b>"Account"</b></font>" per aggiungere o configurare un account con contatti sincronizzabili con il telefono"\n</li>" "\n<li><font fgcolor="#ffffffff"><b>"Nuovo contatto"</b></font>" per creare da zero un nuovo contatto"\n</li>" "\n<li><font fgcolor="#ffffffff"><b>"Importa/esporta"</b></font>\n</li></string>
     <string name="noContactsNoSimHelpTextWithSync" msgid="1122296298361373488">"Non sono presenti contatti da visualizzare (se hai appena aggiunto un account, la sincronizzazione dei contatti potrebbe richiedere alcuni minuti)."\n\n"Per aggiungere contatti, premi "<font fgcolor="#ffffffff"><b>"Menu"</b></font>" e tocca:"\n" "\n<li><font fgcolor="#ffffffff"><b>"Account"</b></font>" per aggiungere o configurare un account con contatti sincronizzabili con il telefono"\n</li>" "\n<li><font fgcolor="#ffffffff"><b>"Opzioni di visualizzazione"</b></font>" per modificare i contatti visibili"\n</li>" "\n<li><font fgcolor="#ffffffff"><b>"Nuovo contatto"</b></font>" per creare da zero un nuovo contatto"\n</li>" "\n<li><font fgcolor="#ffffffff"><b>"Importa/esporta"</b></font>\n</li></string>
-    <string name="noFavoritesHelpText" msgid="3744655776704833277">"Non sono presenti preferiti."\n\n"Per aggiungere un contatto al tuo elenco di preferiti:"\n\n"        "<li>"Tocca la scheda "<b>"Contatti."</b>\n</li>" "\n<li>"Tocca il contatto da aggiungere ai preferiti."\n</li>" "\n<li>"Tocca la stella accanto al nome del contatto."\n</li></string>
+    <string name="noFavoritesHelpText" msgid="3744655776704833277">"Non sono presenti preferiti."\n\n"Per aggiungere un contatto al tuo elenco di preferiti:"\n\n<li>"Tocca la scheda "<b>"Contatti."</b>\n</li>" "\n<li>"Tocca il contatto da aggiungere ai preferiti."\n</li>" "\n<li>"Tocca la stella accanto al nome del contatto."\n</li></string>
     <string name="liveFolder_all_label" msgid="5961411940473276616">"Tutti i contatti"</string>
     <string name="liveFolder_favorites_label" msgid="2674341514070517105">"Speciali"</string>
     <string name="liveFolder_phones_label" msgid="1709786878793436245">"Telefoni"</string>
diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml
index 7cc424e..a9802b8 100644
--- a/res/values-ko/strings.xml
+++ b/res/values-ko/strings.xml
@@ -250,14 +250,14 @@
     <string name="call_disambig_title" msgid="1911302597959335178">"전화 걸기:"</string>
     <string name="sms_disambig_title" msgid="4675399294513152364">"다음을 사용하여 문자 보내기:"</string>
     <string name="make_primary" msgid="5829291915305113983">"이 선택사항 저장"</string>
-    <string name="quickcontact_missing_app" msgid="4600366393134289038">"이 작업을 처리하는 응용프로그램을 찾을 수 없습니다."</string>
+    <string name="quickcontact_missing_app" msgid="4600366393134289038">"이 작업을 처리하는 애플리케이션을 찾을 수 없습니다."</string>
     <string name="quickcontact_remember_choice" msgid="5964536411579749424">"이 선택사항 저장"</string>
     <string name="quickcontact_missing_name" msgid="5590266114306996632">"알 수 없음"</string>
     <string name="menu_accounts" msgid="8499114602017077970">"계정"</string>
     <string name="menu_import_export" msgid="3765725645491577190">"가져오기/내보내기"</string>
     <string name="dialog_import_export" msgid="4771877268244096596">"연락처 가져오기/내보내기"</string>
     <string name="menu_share" msgid="943789700636542260">"공유"</string>
-    <string name="share_via" msgid="563121028023030093">"연락처 공유에 사용할 응용프로그램:"</string>
+    <string name="share_via" msgid="563121028023030093">"연락처 공유에 사용할 애플리케이션:"</string>
     <string name="share_error" msgid="4374508848981697170">"연락처를 공유할 수 없습니다."</string>
     <string name="nameLabelsGroup" msgid="2034640839640477827">"이름"</string>
     <string name="nicknameLabelsGroup" msgid="2891682101053358010">"닉네임"</string>
@@ -374,7 +374,7 @@
     <string name="locale_change_in_progress" msgid="1124266507671178413">"변경된 언어를 반영하도록 연락처 목록을 업데이트하는 중입니다."\n\n"잠시 기다려 주세요."</string>
     <string name="upgrade_in_progress" msgid="7530893673211750223">"연락처 목록을 업데이트하고 있습니다."\n\n"잠시 기다려 주세요..."</string>
     <string name="upgrade_out_of_memory" msgid="492151063059824962">"연락처를 업그레이드하는 중입니다. "\n\n"업그레이드 프로세스에는 약 <xliff:g id="SIZE_IN_MEGABYTES">%d</xliff:g>Mb의 휴대전화 내부 저장공간이 필요합니다."\n\n"다음 옵션 중 하나를 선택하세요."</string>
-    <string name="upgrade_out_of_memory_uninstall" msgid="1721798828992091432">"일부 응용프로그램 제거"</string>
+    <string name="upgrade_out_of_memory_uninstall" msgid="1721798828992091432">"일부 애플리케이션 제거"</string>
     <string name="upgrade_out_of_memory_retry" msgid="8431289830472724609">"업그레이드 다시 시도"</string>
     <string name="search_results_for" msgid="8705490885073188513">"<xliff:g id="QUERY">%s</xliff:g>에 대한 검색결과"</string>
     <string name="search_results_searching" msgid="7755623475227227314">"검색 중..."</string>
diff --git a/res/values/donottranslate_config.xml b/res/values/donottranslate_config.xml
index f1c6951..cad2a63 100644
--- a/res/values/donottranslate_config.xml
+++ b/res/values/donottranslate_config.xml
@@ -53,7 +53,7 @@
     <string name="config_export_vcard_type" translatable="false">default</string>
 
     <!-- Directory in which exported VCard file is stored -->
-    <string name="config_export_dir" translatable="false">/sdcard</string>
+    <string name="config_export_dir" translatable="false">/mnt/sdcard</string>
 
     <!-- Prefix of exported VCard file -->
     <string name="config_export_file_prefix" translatable="false"></string>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 29a39df..1b07bff 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -748,7 +748,7 @@
          data storage. -->
     <string name="caching_vcard_message">Importer is caching vCard(s) to local temporary storage. Actual import will start soon.</string>
 
-    <!-- The message shown while reading vCard(s).
+    <!-- The message shown while importing vCard(s).
          First argument is current index of contacts to be imported.
          Second argument is the total number of contacts.
          Third argument is the Uri which is being read. -->
@@ -764,7 +764,7 @@
     <string name="reading_vcard_canceled_title">Reading vCard data was canceled</string>
 
     <!-- The title shown when reading vCard is canceled (probably by a user) -->
-    <string name="reading_vcard_finished_title">Finished Reading vCard data</string>
+    <string name="importing_vcard_finished_title">Finished importing vCard</string>
 
     <!-- The message shown when vCard importer started running. -->
     <string name="vcard_importer_start_message">vCard importer started.</string>
@@ -772,7 +772,7 @@
     <!-- The message shown when additional vCard to be imported is given during the import for others -->
     <string name="vcard_importer_will_start_message">vCard importer will import the vCard after a while.</string>
 
-    <!-- The percentage, used for expressing the progress of vCard import. -->
+    <!-- The percentage, used for expressing the progress of vCard import/export. -->
     <string name="percentage">%s%%</string>
 
     <!-- Dialog title shown when a user confirms whether he/she export Contact data -->
@@ -801,6 +801,12 @@
          mention it here. -->
     <string name="fail_reason_too_long_filename">Required filename is too long (\"<xliff:g id="filename">%s</xliff:g>\")</string>
 
+    <!-- The message shown when vCard importer started running. -->
+    <string name="vcard_exporter_start_message">vCard exporter started.</string>
+
+    <!-- The title shown when reading vCard is canceled (probably by a user) -->
+    <string name="exporting_vcard_finished_title">Finished exporting vCard</string>
+
     <!-- Dialog title shown when the application is exporting contact data outside -->
     <string name="exporting_contact_list_title">Exporting contact data</string>
 
@@ -1230,5 +1236,5 @@
     <string name="edit_delete_rawcontact">Delete</string>
 
     <!-- Shown in a Toast to indicate an error while trying to save the Data -->
-    <string name="edit_error_saving"></string>
+    <string name="edit_error_saving">Error saving</string>
 </resources>
diff --git a/src/com/android/contacts/activities/TwoPaneActivity.java b/src/com/android/contacts/activities/TwoPaneActivity.java
index ec6d4e3..63283ad 100644
--- a/src/com/android/contacts/activities/TwoPaneActivity.java
+++ b/src/com/android/contacts/activities/TwoPaneActivity.java
@@ -20,6 +20,7 @@
 import com.android.contacts.list.DefaultContactBrowseListFragment;
 import com.android.contacts.list.OnContactBrowserActionListener;
 import com.android.contacts.views.detail.ContactDetailFragment;
+import com.android.contacts.views.editor.ContactEditorFragment;
 import com.android.contacts.widget.SearchEditText;
 import com.android.contacts.widget.SearchEditText.OnFilterTextListener;
 
@@ -28,14 +29,21 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.text.TextUtils;
+import android.widget.LinearLayout;
 import android.widget.Toast;
+import android.widget.LinearLayout.LayoutParams;
 
 public class TwoPaneActivity extends Activity {
     private final static String TAG = "TwoPaneActivity";
+
     private DefaultContactBrowseListFragment mListFragment;
+    private ListFragmentListener mListFragmentListener = new ListFragmentListener();
+
     private ContactDetailFragment mDetailFragment;
     private DetailFragmentListener mDetailFragmentListener = new DetailFragmentListener();
-    private ListFragmentListener mListFragmentListener = new ListFragmentListener();
+
+    private ContactEditorFragment mEditorFragment;
+    private EditorFragmentListener mEditorFragmentListener = new EditorFragmentListener();
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -46,12 +54,51 @@
         mListFragment = (DefaultContactBrowseListFragment) findFragmentById(R.id.two_pane_list);
         mListFragment.setOnContactListActionListener(mListFragmentListener);
 
-        mDetailFragment = (ContactDetailFragment) findFragmentById(R.id.two_pane_detail);
-        mDetailFragment.setListener(mDetailFragmentListener);
+        setupContactDetailFragment();
 
         setupSearchUI();
     }
 
+    private void setupContactDetailFragment() {
+        // No editor here
+        if (mEditorFragment != null) {
+            mEditorFragment.setListener(null);
+            mEditorFragment = null;
+        }
+
+        // Already showing? Nothing to do
+        if (mDetailFragment != null) return;
+
+        mDetailFragment = new ContactDetailFragment();
+        mDetailFragment.setListener(mDetailFragmentListener);
+
+        // Nothing showing yet? Create (this happens during Activity-Startup)
+        openFragmentTransaction()
+                .replace(R.id.two_pane_right_view, mDetailFragment)
+                .commit();
+
+    }
+
+    private void setupContactEditorFragment() {
+        // No detail view here
+        if (mDetailFragment != null) {
+            mDetailFragment.setListener(null);
+            mDetailFragment = null;
+        }
+
+        // Already showing? Nothing to do
+        if (mEditorFragment != null) return;
+
+        mEditorFragment = new ContactEditorFragment();
+        mEditorFragment.setListener(mEditorFragmentListener);
+
+        // Nothing showing yet? Create (this happens during Activity-Startup)
+        openFragmentTransaction()
+                .replace(R.id.two_pane_right_view, mEditorFragment)
+                .commit();
+
+    }
+
     private void setupSearchUI() {
         SearchEditText searchEditText = (SearchEditText)findViewById(R.id.search_src_text);
         searchEditText.setOnFilterTextListener(new OnFilterTextListener() {
@@ -112,6 +159,7 @@
         }
 
         public void onViewContactAction(Uri contactLookupUri) {
+            setupContactDetailFragment();
             mDetailFragment.loadUri(contactLookupUri);
         }
     }
@@ -122,10 +170,8 @@
         }
 
         public void onEditRequested(Uri contactLookupUri) {
-//          final ContactEditFragment fragment = new ContactEditFragment();
-//          openFragmentTransaction().replace(mDetailFragment.getView().getId(), fragment).commit();
-//          fragment.loadUri(contactLookupUri);
-            Toast.makeText(TwoPaneActivity.this, "editContact", Toast.LENGTH_LONG).show();
+            setupContactEditorFragment();
+            mEditorFragment.loadUri(contactLookupUri);
         }
 
         public void onItemClicked(Intent intent) {
@@ -136,4 +182,22 @@
             showDialog(id, bundle);
         }
     }
+
+    private class EditorFragmentListener implements ContactEditorFragment.Listener {
+        public void onContactNotFound() {
+            Toast.makeText(TwoPaneActivity.this, "onContactNotFound", Toast.LENGTH_LONG).show();
+        }
+
+        public void onDialogRequested(int id, Bundle bundle) {
+            Toast.makeText(TwoPaneActivity.this, "onDialogRequested", Toast.LENGTH_LONG).show();
+        }
+
+        public void onEditorRequested(Intent intent) {
+            Toast.makeText(TwoPaneActivity.this, "onEditorRequested", Toast.LENGTH_LONG).show();
+        }
+
+        public void onError() {
+            Toast.makeText(TwoPaneActivity.this, "onError", Toast.LENGTH_LONG).show();
+        }
+    }
 }
diff --git a/src/com/android/contacts/model/EntityDelta.java b/src/com/android/contacts/model/EntityDelta.java
index 9eb7779..cdf2e41 100644
--- a/src/com/android/contacts/model/EntityDelta.java
+++ b/src/com/android/contacts/model/EntityDelta.java
@@ -59,7 +59,7 @@
     // TODO: optimize by using contentvalues pool, since we allocate so many of them
 
     private static final String TAG = "EntityDelta";
-    private static final boolean LOGV = true;
+    private static final boolean LOGV = false;
 
     /**
      * Direct values from {@link Entity#getEntityValues()}.
diff --git a/src/com/android/contacts/vcard/ExportProcessor.java b/src/com/android/contacts/vcard/ExportProcessor.java
new file mode 100644
index 0000000..7e7c7c8
--- /dev/null
+++ b/src/com/android/contacts/vcard/ExportProcessor.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2010 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.vcard;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.RemoteViews;
+
+import com.android.contacts.ContactsListActivity;
+import com.android.contacts.R;
+import com.android.vcard.VCardComposer;
+import com.android.vcard.VCardConfig;
+
+import java.io.FileNotFoundException;
+import java.io.OutputStream;
+import java.util.LinkedList;
+import java.util.Queue;
+
+public class ExportProcessor {
+    private static final String LOG_TAG = "ExportProcessor";
+
+    private final Service mService;
+
+    private ContentResolver mResolver;
+    private NotificationManager mNotificationManager;
+
+    boolean mCanceled;
+
+    boolean mReadyForRequest;
+    private final Queue<ExportRequest> mPendingRequests =
+        new LinkedList<ExportRequest>();
+
+    private RemoteViews mProgressViews;
+
+    public ExportProcessor(Service service) {
+        mService = service;
+    }
+
+    /* package */ ThreadStarter mThreadStarter = new ThreadStarter() {
+        public void start() {
+            final Thread thread = new Thread(new Runnable() {
+                public void run() {
+                    process();
+                }
+            });
+            thread.start();
+        }
+    };
+
+    public synchronized void pushRequest(ExportRequest parameter) {
+        if (mResolver == null) {
+            // Service object may not ready at the construction time
+            // (e.g. ContentResolver may be null).
+            mResolver = mService.getContentResolver();
+            mNotificationManager =
+                    (NotificationManager)mService.getSystemService(Context.NOTIFICATION_SERVICE);
+        }
+
+        final boolean needThreadStart;
+        if (!mReadyForRequest) {
+            needThreadStart = true;
+        } else {
+            needThreadStart = false;
+        }
+        mPendingRequests.add(parameter);
+        if (needThreadStart) {
+            mThreadStarter.start();
+        }
+
+        mReadyForRequest = true;
+    }
+
+    /* package */ void process() {
+        if (!mReadyForRequest) {
+            throw new RuntimeException(
+                    "process() is called before request being pushed "
+                    + "or after this object's finishing its processing.");
+        }
+
+        try {
+            while (!mCanceled) {
+                final ExportRequest parameter;
+                synchronized (this) {
+                    if (mPendingRequests.size() == 0) {
+                        mReadyForRequest = false;
+                        break;
+                    } else {
+                        parameter = mPendingRequests.poll();
+                    }
+                }  // synchronized (this)
+                handleOneRequest(parameter);
+            }
+
+            doFinishNotification(mService.getString(R.string.exporting_vcard_finished_title),
+                    "");
+        } finally {
+            // Not thread safe. Just in case.
+            // TODO: verify this works fine.
+            mReadyForRequest = false;
+        }
+    }
+
+    /* package */ void handleOneRequest(ExportRequest request) {
+        boolean shouldCallFinish = true;
+        VCardComposer composer = null;
+        try {
+            final Uri uri = request.destUri;
+            final OutputStream outputStream;
+            try {
+                outputStream = mResolver.openOutputStream(uri);
+            } catch (FileNotFoundException e) {
+                // Need concise title.
+
+                final String errorReason =
+                    mService.getString(R.string.fail_reason_could_not_open_file,
+                            uri, e.getMessage());
+                shouldCallFinish = false;
+                doFinishNotification(errorReason, "");
+                return;
+            }
+            final String exportType = request.exportType;
+            final int vcardType;
+            if (TextUtils.isEmpty(exportType)) {
+                vcardType = VCardConfig.getVCardTypeFromString(
+                        mService.getString(R.string.config_export_vcard_type));
+            } else {
+                vcardType = VCardConfig.getVCardTypeFromString(exportType);
+            }
+
+            composer = new VCardComposer(mService, vcardType, true);
+
+            // for test
+            // int vcardType = (VCardConfig.VCARD_TYPE_V21_GENERIC |
+            //     VCardConfig.FLAG_USE_QP_TO_PRIMARY_PROPERTIES);
+            // composer = new VCardComposer(ExportVCardActivity.this, vcardType, true);
+
+            composer.addHandler(composer.new HandlerForOutputStream(outputStream));
+
+            if (!composer.init()) {
+                final String errorReason = composer.getErrorReason();
+                Log.e(LOG_TAG, "initialization of vCard composer failed: " + errorReason);
+                final String translatedErrorReason =
+                        translateComposerError(errorReason);
+                final String title =
+                        mService.getString(R.string.fail_reason_could_not_initialize_exporter,
+                                translatedErrorReason);
+                doFinishNotification(title, "");
+                return;
+            }
+
+            final int total = composer.getCount();
+            if (total == 0) {
+                final String title =
+                        mService.getString(R.string.fail_reason_no_exportable_contact);
+                doFinishNotification(title, "");
+                return;
+            }
+
+            int current = 1;  // 1-origin
+            while (!composer.isAfterLast()) {
+                if (mCanceled) {
+                    return;
+                }
+                if (!composer.createOneEntry()) {
+                    final String errorReason = composer.getErrorReason();
+                    Log.e(LOG_TAG, "Failed to read a contact: " + errorReason);
+                    final String translatedErrorReason =
+                            translateComposerError(errorReason);
+                    final String title =
+                            mService.getString(R.string.fail_reason_error_occurred_during_export,
+                                    translatedErrorReason);
+                    doFinishNotification(title, "");
+                    return;
+                }
+                doProgressNotification(uri, total, current);
+                current++;
+            }
+        } finally {
+            if (composer != null) {
+                composer.terminate();
+            }
+        }
+    }
+
+    private String translateComposerError(String errorMessage) {
+        final Resources resources = mService.getResources();
+        if (VCardComposer.FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO.equals(errorMessage)) {
+            return resources.getString(R.string.composer_failed_to_get_database_infomation);
+        } else if (VCardComposer.FAILURE_REASON_NO_ENTRY.equals(errorMessage)) {
+            return resources.getString(R.string.composer_has_no_exportable_contact);
+        } else if (VCardComposer.FAILURE_REASON_NOT_INITIALIZED.equals(errorMessage)) {
+            return resources.getString(R.string.composer_not_initialized);
+        } else {
+            return errorMessage;
+        }
+    }
+
+    private void doProgressNotification(Uri uri, int total, int current) {
+        final String title = mService.getString(R.string.exporting_contact_list_title);
+        final String message =
+                mService.getString(R.string.exporting_contact_list_message, uri);
+
+        final RemoteViews remoteViews = new RemoteViews(mService.getPackageName(),
+                R.layout.status_bar_ongoing_event_progress_bar);
+        remoteViews.setTextViewText(R.id.description, message);
+        remoteViews.setProgressBar(R.id.progress_bar, total, current, (total == -1));
+
+        final String percentage = mService.getString(R.string.percentage,
+                String.valueOf((current * 100)/total));
+        remoteViews.setTextViewText(R.id.progress_text, percentage);
+        remoteViews.setImageViewResource(R.id.appIcon, android.R.drawable.stat_sys_upload);
+
+        final Notification notification = new Notification();
+        notification.icon = android.R.drawable.stat_sys_upload;
+        notification.flags |= Notification.FLAG_ONGOING_EVENT;
+        notification.tickerText = title;
+        notification.contentView = remoteViews;
+        notification.contentIntent =
+                PendingIntent.getActivity(mService, 0,
+                        new Intent(mService, ContactsListActivity.class), 0);
+        mNotificationManager.notify(VCardService.EXPORT_NOTIFICATION_ID, notification);
+    }
+
+    private void doFinishNotification(final String title, final String message) {
+        final Notification notification = new Notification();
+        notification.icon = android.R.drawable.stat_sys_upload_done;
+        notification.flags |= Notification.FLAG_AUTO_CANCEL;
+        notification.setLatestEventInfo(mService, title, message, null);
+        final Intent intent = new Intent(mService, ContactsListActivity.class);
+        notification.contentIntent =
+                PendingIntent.getActivity(mService, 0, intent, 0);
+        mNotificationManager.notify(VCardService.EXPORT_NOTIFICATION_ID, notification);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/vcard/ExportRequest.java b/src/com/android/contacts/vcard/ExportRequest.java
new file mode 100644
index 0000000..fae2d07
--- /dev/null
+++ b/src/com/android/contacts/vcard/ExportRequest.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2010 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.vcard;
+
+import android.net.Uri;
+
+public class ExportRequest {
+    public final Uri destUri;
+    /**
+     * Can be null.
+     */
+    public final String exportType;
+
+    public ExportRequest(Uri destUri) {
+        this(destUri, null);
+    }
+
+    public ExportRequest(Uri destUri, String exportType) {
+        this.destUri = destUri;
+        this.exportType = exportType;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/vcard/ExportVCardActivity.java b/src/com/android/contacts/vcard/ExportVCardActivity.java
index 509702b..43ae858 100644
--- a/src/com/android/contacts/vcard/ExportVCardActivity.java
+++ b/src/com/android/contacts/vcard/ExportVCardActivity.java
@@ -19,12 +19,21 @@
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.app.ProgressDialog;
+import android.content.ComponentName;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.ServiceConnection;
 import android.content.res.Resources;
+import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
 import android.os.PowerManager;
+import android.os.RemoteException;
 import android.text.TextUtils;
 import android.util.Log;
 
@@ -37,6 +46,8 @@
 import java.io.FileOutputStream;
 import java.io.OutputStream;
 import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Queue;
 import java.util.Set;
 
 /**
@@ -58,21 +69,76 @@
     private int mFileIndexMinimum;
     private int mFileIndexMaximum;
     private String mFileNameExtension;
-    private String mVCardTypeStr;
     private Set<String> mExtensionsToConsider;
 
-    private ProgressDialog mProgressDialog;
-    private String mExportingFileName;
-
-    private Handler mHandler = new Handler();
-
-    // Used temporaly when asking users to confirm the file name
+    // Used temporarily when asking users to confirm the file name
     private String mTargetFileName;
 
-    // String for storing error reason temporaly.
+    // String for storing error reason temporarily.
     private String mErrorReason;
 
-    private ActualExportThread mActualExportThread;
+
+    private class CustomConnection implements ServiceConnection {
+        private Messenger mMessenger;
+        private Queue<ExportRequest> mPendingRequests = new LinkedList<ExportRequest>();
+
+        public void doBindService() {
+            bindService(new Intent(ExportVCardActivity.this,
+                    VCardService.class), this, Context.BIND_AUTO_CREATE);
+        }
+
+        public synchronized void requestSend(final ExportRequest parameter) {
+            if (mMessenger != null) {
+                sendMessage(parameter);
+            } else {
+                mPendingRequests.add(parameter);
+            }
+        }
+
+        private void sendMessage(final ExportRequest request) {
+            try {
+                mMessenger.send(Message.obtain(null,
+                        VCardService.MSG_EXPORT_REQUEST,
+                        request));
+            } catch (RemoteException e) {
+                Log.e(LOG_TAG, "RemoteException is thrown when trying to send request");
+                runOnUiThread(new ErrorReasonDisplayer(
+                        getString(R.string.fail_reason_unknown)));
+            }
+        }
+
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            synchronized (this) {
+                mMessenger = new Messenger(service);
+                // Send pending requests thrown from this Activity before an actual connection
+                // is established.
+                while (!mPendingRequests.isEmpty()) {
+                    final ExportRequest parameter = mPendingRequests.poll();
+                    if (parameter == null) {
+                        throw new NullPointerException();
+                    }
+                    sendMessage(parameter);
+                }
+
+                unbindService(this);
+                finish();
+            }
+        }
+
+        public void onServiceDisconnected(ComponentName name) {
+            synchronized (this) {
+                if (!mPendingRequests.isEmpty()) {
+                    Log.w(LOG_TAG, "Some request(s) are dropped.");
+                }
+                // Set to null so that we can detect inappropriate re-connection toward
+                // the Service via NullPointerException;
+                mPendingRequests = null;
+                mMessenger = null;
+            }
+        }
+    }
+
+    private final CustomConnection mConnection = new CustomConnection();
 
     private class CancelListener
             implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
@@ -104,130 +170,28 @@
     }
 
     private class ExportConfirmationListener implements DialogInterface.OnClickListener {
-        private final String mFileName;
+        private final Uri mDestUri;
 
         public ExportConfirmationListener(String fileName) {
-            mFileName = fileName;
+            this(Uri.parse("file://" + fileName));
+        }
+
+        public ExportConfirmationListener(Uri uri) {
+            mDestUri = uri;
         }
 
         public void onClick(DialogInterface dialog, int which) {
             if (which == DialogInterface.BUTTON_POSITIVE) {
-                mActualExportThread = new ActualExportThread(mFileName);
-                showDialog(R.id.dialog_exporting_vcard);
+                mConnection.doBindService();
+
+                final ExportRequest request = new ExportRequest(mDestUri);
+
+                // The connection object will call finish().
+                mConnection.requestSend(request);
             }
         }
     }
 
-    private class ActualExportThread extends Thread
-            implements DialogInterface.OnCancelListener {
-        private PowerManager.WakeLock mWakeLock;
-        private boolean mCanceled = false;
-
-        public ActualExportThread(String fileName) {
-            mExportingFileName = fileName;
-            PowerManager powerManager = (PowerManager)getSystemService(Context.POWER_SERVICE);
-            mWakeLock = powerManager.newWakeLock(
-                    PowerManager.SCREEN_DIM_WAKE_LOCK |
-                    PowerManager.ON_AFTER_RELEASE, LOG_TAG);
-        }
-
-        @Override
-        public void run() {
-            boolean shouldCallFinish = true;
-            mWakeLock.acquire();
-            VCardComposer composer = null;
-            try {
-                OutputStream outputStream = null;
-                try {
-                    outputStream = new FileOutputStream(mExportingFileName);
-                } catch (FileNotFoundException e) {
-                    final String errorReason =
-                        getString(R.string.fail_reason_could_not_open_file,
-                                mExportingFileName, e.getMessage());
-                    shouldCallFinish = false;
-                    mHandler.post(new ErrorReasonDisplayer(errorReason));
-                    return;
-                }
-
-                final int vcardType = VCardConfig.getVCardTypeFromString(mVCardTypeStr);
-                composer = new VCardComposer(ExportVCardActivity.this, vcardType, true);
-                /*int vcardType = (VCardConfig.VCARD_TYPE_V21_GENERIC |
-                        VCardConfig.FLAG_USE_QP_TO_PRIMARY_PROPERTIES);
-                composer = new VCardComposer(ExportVCardActivity.this, vcardType, true);*/
-
-                composer.addHandler(composer.new HandlerForOutputStream(outputStream));
-
-                if (!composer.init()) {
-                    final String errorReason = composer.getErrorReason();
-                    Log.e(LOG_TAG, "initialization of vCard composer failed: " + errorReason);
-                    final String translatedErrorReason =
-                            translateComposerError(errorReason);
-                    mHandler.post(new ErrorReasonDisplayer(
-                            getString(R.string.fail_reason_could_not_initialize_exporter,
-                                    translatedErrorReason)));
-                    shouldCallFinish = false;
-                    return;
-                }
-
-                int size = composer.getCount();
-
-                if (size == 0) {
-                    mHandler.post(new ErrorReasonDisplayer(
-                            getString(R.string.fail_reason_no_exportable_contact)));
-                    shouldCallFinish = false;
-                    return;
-                }
-
-                mProgressDialog.setProgressNumberFormat(
-                        getString(R.string.exporting_contact_list_progress));
-                mProgressDialog.setMax(size);
-                mProgressDialog.setProgress(0);
-
-                while (!composer.isAfterLast()) {
-                    if (mCanceled) {
-                        return;
-                    }
-                    if (!composer.createOneEntry()) {
-                        final String errorReason = composer.getErrorReason();
-                        Log.e(LOG_TAG, "Failed to read a contact: " + errorReason);
-                        final String translatedErrorReason =
-                            translateComposerError(errorReason);
-                        mHandler.post(new ErrorReasonDisplayer(
-                                getString(R.string.fail_reason_error_occurred_during_export,
-                                        translatedErrorReason)));
-                        shouldCallFinish = false;
-                        return;
-                    }
-                    mProgressDialog.incrementProgressBy(1);
-                }
-            } finally {
-                if (composer != null) {
-                    composer.terminate();
-                }
-                mWakeLock.release();
-                mProgressDialog.dismiss();
-                if (shouldCallFinish && !isFinishing()) {
-                    finish();
-                }
-            }
-        }
-
-        @Override
-        public void finalize() {
-            if (mWakeLock != null && mWakeLock.isHeld()) {
-                mWakeLock.release();
-            }
-        }
-
-        public void cancel() {
-            mCanceled = true;
-        }
-
-        public void onCancel(DialogInterface dialog) {
-            cancel();
-        }
-    }
-
     private String translateComposerError(String errorMessage) {
         Resources resources = getResources();
         if (VCardComposer.FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO.equals(errorMessage)) {
@@ -249,7 +213,6 @@
         mFileNamePrefix = getString(R.string.config_export_file_prefix);
         mFileNameSuffix = getString(R.string.config_export_file_suffix);
         mFileNameExtension = getString(R.string.config_export_file_extension);
-        mVCardTypeStr = getString(R.string.config_export_vcard_type);
 
         mExtensionsToConsider = new HashSet<String>();
         mExtensionsToConsider.add(mFileNameExtension);
@@ -273,7 +236,7 @@
     }
 
     @Override
-    protected Dialog onCreateDialog(int id) {
+    protected Dialog onCreateDialog(int id, Bundle bundle) {
         switch (id) {
             case R.id.dialog_export_confirmation: {
                 return getExportConfirmationDialog();
@@ -297,44 +260,25 @@
                 .setPositiveButton(android.R.string.ok, mCancelListener);
                 return builder.create();
             }
-            case R.id.dialog_exporting_vcard: {
-                if (mProgressDialog == null) {
-                    String title = getString(R.string.exporting_contact_list_title);
-                    String message = getString(R.string.exporting_contact_list_message,
-                            mExportingFileName);
-                    mProgressDialog = new ProgressDialog(ExportVCardActivity.this);
-                    mProgressDialog.setTitle(title);
-                    mProgressDialog.setMessage(message);
-                    mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
-                    mProgressDialog.setOnCancelListener(mActualExportThread);
-                    mActualExportThread.start();
-                }
-                return mProgressDialog;
-            }
         }
-        return super.onCreateDialog(id);
+        return super.onCreateDialog(id, bundle);
     }
 
     @Override
-    protected void onPrepareDialog(int id, Dialog dialog) {
+    protected void onPrepareDialog(int id, Dialog dialog, Bundle args) {
         if (id == R.id.dialog_fail_to_export_with_reason) {
             ((AlertDialog)dialog).setMessage(getErrorReason());
         } else if (id == R.id.dialog_export_confirmation) {
             ((AlertDialog)dialog).setMessage(
                     getString(R.string.confirm_export_message, mTargetFileName));
         } else {
-            super.onPrepareDialog(id, dialog);
+            super.onPrepareDialog(id, dialog, args);
         }
     }
 
     @Override
     protected void onStop() {
         super.onStop();
-        if (mActualExportThread != null) {
-            // The Activity is no longer visible. Stop the thread.
-            mActualExportThread.cancel();
-            mActualExportThread = null;
-        }
 
         if (!isFinishing()) {
             finish();
@@ -449,13 +393,6 @@
             .create();
     }
 
-    public void cancelExport() {
-        if (mActualExportThread != null) {
-            mActualExportThread.cancel();
-            mActualExportThread = null;
-        }
-    }
-
     public String getErrorReason() {
         return mErrorReason;
     }
diff --git a/src/com/android/contacts/vcard/ImportRequestProcessor.java b/src/com/android/contacts/vcard/ImportProcessor.java
similarity index 92%
rename from src/com/android/contacts/vcard/ImportRequestProcessor.java
rename to src/com/android/contacts/vcard/ImportProcessor.java
index b4158b6..f4b5f5a 100644
--- a/src/com/android/contacts/vcard/ImportRequestProcessor.java
+++ b/src/com/android/contacts/vcard/ImportProcessor.java
@@ -53,7 +53,7 @@
  * This class is designed so that a user ({@link Service}) does not need to (and should not)
  * recreate multiple instances, as this holds total count of vCard entries to be imported.
  */
-public class ImportRequestProcessor {
+public class ImportProcessor {
     private static final String LOG_TAG = "ImportRequestProcessor";
 
     private final Service mService;
@@ -67,19 +67,6 @@
 
     private VCardParser mVCardParser;
 
-    // TODO(dmiyakawa): better design for testing?
-    /* package */ interface CommitterGenerator {
-        public VCardEntryCommitter generate(ContentResolver resolver);
-    }
-
-    private static class DefaultCommitterGenerator implements CommitterGenerator {
-        public VCardEntryCommitter generate(ContentResolver resolver) {
-            return new VCardEntryCommitter(resolver);
-        }
-    }
-
-    /* package */ CommitterGenerator mCommitterGenerator = new DefaultCommitterGenerator();
-
     /**
      * Meaning a controller of this object requests the operation should be canceled
      * or not, which implies {@link #mReadyForRequest} should be set to false soon, but
@@ -95,9 +82,7 @@
     private final Queue<ImportRequest> mPendingRequests =
             new LinkedList<ImportRequest>();
 
-    /* package */ interface ThreadStarter {
-        public void start();
-    }
+    // For testability.
     /* package */ ThreadStarter mThreadStarter = new ThreadStarter() {
         public void start() {
             final Thread thread = new Thread(new Runnable() {
@@ -108,9 +93,17 @@
             thread.start();
         }
     };
+    /* package */ interface CommitterGenerator {
+        public VCardEntryCommitter generate(ContentResolver resolver);
+    }
+    /* package */ class DefaultCommitterGenerator implements CommitterGenerator {
+        public VCardEntryCommitter generate(ContentResolver resolver) {
+            return new VCardEntryCommitter(mResolver);
+        }
+    }
+    /* package */ CommitterGenerator mCommitterGenerator = new DefaultCommitterGenerator();
 
-
-    public ImportRequestProcessor(final Service service) {
+    public ImportProcessor(final Service service) {
         mService = service;
     }
 
@@ -153,7 +146,8 @@
     /* package */ void process() {
         if (!mReadyForRequest) {
             throw new RuntimeException(
-                    "process() is called after this object finishing its process.");
+                    "process() is called before request being pushed "
+                    + "or after this object's finishing its processing.");
         }
         try {
             while (!mCanceled) {
@@ -172,7 +166,7 @@
             // Currenty we don't have an appropriate way to let users see all entries
             // imported in this procedure. Instead, we show them entries only when
             // there's just one created uri.
-            doFinishNotification(mCreatedUris.size() == 1 ? mCreatedUris.get(0) : null);
+            doFinishNotification(mCreatedUris.size() > 0 ? mCreatedUris.get(0) : null);
         } finally {
             // TODO: verify this works fine.
             mReadyForRequest = false;  // Just in case.
@@ -211,7 +205,7 @@
         final String estimatedCharset = parameter.estimatedCharset;
 
         final VCardEntryConstructor constructor =
-            new VCardEntryConstructor(estimatedVCardType, account, estimatedCharset);
+                new VCardEntryConstructor(estimatedVCardType, account, estimatedCharset);
         final VCardEntryCommitter committer = mCommitterGenerator.generate(mResolver);
         constructor.addEntryHandler(committer);
         constructor.addEntryHandler(mNotifier);
@@ -239,6 +233,7 @@
     private void doErrorNotification(int id) {
         final Notification notification = new Notification();
         notification.icon = android.R.drawable.stat_sys_download_done;
+        notification.flags |= Notification.FLAG_AUTO_CANCEL;
         final String title = mService.getString(R.string.reading_vcard_failed_title);
         final PendingIntent intent =
                 PendingIntent.getActivity(mService, 0, new Intent(), 0);
@@ -250,7 +245,9 @@
     private void doFinishNotification(Uri createdUri) {
         final Notification notification = new Notification();
         notification.icon = android.R.drawable.stat_sys_download_done;
-        final String title = mService.getString(R.string.reading_vcard_finished_title);
+        notification.flags |= Notification.FLAG_AUTO_CANCEL;
+
+        final String title = mService.getString(R.string.importing_vcard_finished_title);
 
         final Intent intent;
         if (createdUri != null) {
@@ -263,10 +260,9 @@
             intent = null;
         }
 
-        final PendingIntent pendingIntent =
-                PendingIntent.getActivity(mService, 0, intent, 0);
-        notification.setLatestEventInfo(mService, title, "", pendingIntent);
-        mNotificationManager.notify(ImportVCardService.NOTIFICATION_ID, notification);
+        notification.setLatestEventInfo(mService, title, "",
+                PendingIntent.getActivity(mService, 0, intent, 0));
+        mNotificationManager.notify(VCardService.IMPORT_NOTIFICATION_ID, notification);
     }
 
     private boolean readOneVCard(Uri uri, int vcardType, String charset,
diff --git a/src/com/android/contacts/vcard/ImportProgressNotifier.java b/src/com/android/contacts/vcard/ImportProgressNotifier.java
index 42d101b..e6f1037 100644
--- a/src/com/android/contacts/vcard/ImportProgressNotifier.java
+++ b/src/com/android/contacts/vcard/ImportProgressNotifier.java
@@ -56,38 +56,42 @@
         // - We cannot know name there but here.
         // - There's high probability where name comes soon after the beginning of entry, so
         //   we don't need to hurry to show something.
-        final String packageName = "com.android.contacts";
-        final RemoteViews remoteViews = new RemoteViews(packageName,
+
+        // TODO: should not create this every time?
+        final RemoteViews remoteViews = new RemoteViews(mContext.getPackageName(),
                 R.layout.status_bar_ongoing_event_progress_bar);
+
         final String title = mContext.getString(R.string.reading_vcard_title);
+
         String totalCountString;
         synchronized (this) {
             totalCountString = String.valueOf(mTotalCount);
         }
-        final String text = mContext.getString(R.string.progress_notifier_message,
+        final String description = mContext.getString(R.string.progress_notifier_message,
                 String.valueOf(mCurrentCount),
                 totalCountString,
                 contactStruct.getDisplayName());
 
-        // TODO: uploading image does not work correctly. (looks like a static image).
-        remoteViews.setTextViewText(R.id.description, text);
+        remoteViews.setTextViewText(R.id.title, title);
+        remoteViews.setTextViewText(R.id.description, description);
         remoteViews.setProgressBar(R.id.progress_bar, mTotalCount, mCurrentCount,
                 mTotalCount == -1);
         final String percentage =
                 mContext.getString(R.string.percentage,
                         String.valueOf(mCurrentCount * 100/mTotalCount));
+
         remoteViews.setTextViewText(R.id.progress_text, percentage);
         remoteViews.setImageViewResource(R.id.appIcon, android.R.drawable.stat_sys_download);
 
         final Notification notification = new Notification();
         notification.icon = android.R.drawable.stat_sys_download;
         notification.flags |= Notification.FLAG_ONGOING_EVENT;
+        notification.tickerText = description;
         notification.contentView = remoteViews;
-
         notification.contentIntent =
                 PendingIntent.getActivity(mContext, 0,
                         new Intent(mContext, ContactsListActivity.class), 0);
-        mNotificationManager.notify(ImportVCardService.NOTIFICATION_ID, notification);
+        mNotificationManager.notify(VCardService.IMPORT_NOTIFICATION_ID, notification);
     }
 
     public synchronized void addTotalCount(int additionalCount) {
diff --git a/src/com/android/contacts/vcard/ImportRequest.java b/src/com/android/contacts/vcard/ImportRequest.java
index 841a09c..5d46166 100644
--- a/src/com/android/contacts/vcard/ImportRequest.java
+++ b/src/com/android/contacts/vcard/ImportRequest.java
@@ -21,9 +21,9 @@
 import com.android.vcard.VCardSourceDetector;
 
 /**
- * Class representing one request for reading vCard (as a Uri representation).
+ * Class representing one request for importing vCard (given as a Uri).
  *
- * Mainly used when {@link ImportVCardActivity} requests {@link ImportVCardService}
+ * Mainly used when {@link ImportVCardActivity} requests {@link VCardService}
  * to import some specific Uri.
  *
  * Note: This object's accepting only One Uri does NOT mean that
diff --git a/src/com/android/contacts/vcard/ImportVCardActivity.java b/src/com/android/contacts/vcard/ImportVCardActivity.java
index c7606eb..ec63a96 100644
--- a/src/com/android/contacts/vcard/ImportVCardActivity.java
+++ b/src/com/android/contacts/vcard/ImportVCardActivity.java
@@ -32,7 +32,6 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Environment;
-import android.os.Handler;
 import android.os.IBinder;
 import android.os.Message;
 import android.os.Messenger;
@@ -100,9 +99,6 @@
     /* package */ final static int VCARD_VERSION_V21 = 1;
     /* package */ final static int VCARD_VERSION_V30 = 2;
 
-    // Run on the UI thread. Must not be null except after onDestroy().
-    private Handler mHandler = new Handler();
-
     private AccountSelectionUtil.AccountSelectedListener mAccountSelectionListener;
 
     private Account mAccount;
@@ -133,7 +129,7 @@
 
         public void doBindService() {
             bindService(new Intent(ImportVCardActivity.this,
-                    ImportVCardService.class), this, Context.BIND_AUTO_CREATE);
+                    VCardService.class), this, Context.BIND_AUTO_CREATE);
         }
 
         public void setNeedFinish() {
@@ -154,14 +150,14 @@
             }
         }
 
-        private void sendMessage(final ImportRequest parameter) {
+        private void sendMessage(final ImportRequest request) {
             try {
                 mMessenger.send(Message.obtain(null,
-                        ImportVCardService.MSG_IMPORT_REQUEST,
-                        parameter));
+                        VCardService.MSG_IMPORT_REQUEST,
+                        request));
             } catch (RemoteException e) {
-                Log.e(LOG_TAG, "RemoteException is thrown when trying to import vCard");
-                runOnUIThread(new DialogDisplayer(
+                Log.e(LOG_TAG, "RemoteException is thrown when trying to send request");
+                runOnUiThread(new DialogDisplayer(
                         getString(R.string.fail_reason_unknown)));
             }
         }
@@ -255,10 +251,10 @@
 
     /**
      * Caches all vCard data into local data directory so that we allow
-     * {@link ImportVCardService} to access all the contents in given Uris, some of
+     * {@link VCardService} to access all the contents in given Uris, some of
      * which may not be accessible from other components due to permission problem.
      * (Activity which gives the Uri may allow only this Activity to access that content,
-     * not the ohter components like {@link ImportVCardService}.
+     * not the other components like {@link VCardService}.
      *
      * We also allow the Service to happen to exit during the vCard import procedure.
      */
@@ -333,14 +329,16 @@
                 // smaller memory than we usually expect.
                 System.gc();
                 needFinish = false;
+
+                // TODO: call this from connection object.
                 unbindService(mConnection);
-                runOnUIThread(new DialogDisplayer(
+                runOnUiThread(new DialogDisplayer(
                         getString(R.string.fail_reason_low_memory_during_import)));
             } catch (IOException e) {
                 Log.e(LOG_TAG, e.getMessage());
                 needFinish = false;
                 unbindService(mConnection);
-                runOnUIThread(new DialogDisplayer(
+                runOnUiThread(new DialogDisplayer(
                         getString(R.string.fail_reason_io_error)));
             } finally {
                 mWakeLock.release();
@@ -631,14 +629,14 @@
             mProgressDialogForScanVCard = null;
 
             if (mGotIOException) {
-                runOnUIThread(new DialogDisplayer(R.id.dialog_io_exception));
+                runOnUiThread(new DialogDisplayer(R.id.dialog_io_exception));
             } else if (mCanceled) {
                 finish();
             } else {
                 int size = mAllVCardFileList.size();
                 final Context context = ImportVCardActivity.this;
                 if (size == 0) {
-                    runOnUIThread(new DialogDisplayer(R.id.dialog_vcard_not_found));
+                    runOnUiThread(new DialogDisplayer(R.id.dialog_vcard_not_found));
                 } else {
                     startVCardSelectAndImport();
                 }
@@ -697,9 +695,9 @@
                 size == 1) {
             importVCardFromSDCard(mAllVCardFileList);
         } else if (getResources().getBoolean(R.bool.config_allow_users_select_all_vcard_import)) {
-            runOnUIThread(new DialogDisplayer(R.id.dialog_select_import_type));
+            runOnUiThread(new DialogDisplayer(R.id.dialog_select_import_type));
         } else {
-            runOnUIThread(new DialogDisplayer(R.id.dialog_select_one_vcard));
+            runOnUiThread(new DialogDisplayer(R.id.dialog_select_one_vcard));
         }
     }
 
@@ -732,7 +730,7 @@
     }
 
     private void importVCard(final Uri[] uris) {
-        runOnUIThread(new Runnable() {
+        runOnUiThread(new Runnable() {
             public void run() {
                 mVCardCacheThread = new VCardCacheThread(uris);
                 showDialog(R.id.dialog_cache_vcard);
@@ -962,33 +960,6 @@
         }
     }
 
-    @Override
-    protected void onDestroy() {
-        // The code assumes the handler runs on the UI thread. If not,
-        // clearing the message queue is not enough, one would have to
-        // make sure that the handler does not run any callback when
-        // this activity isFinishing().
-
-        // Callbacks messages have what == 0.
-        if (mHandler.hasMessages(0)) {
-            mHandler.removeMessages(0);
-        }
-
-        mHandler = null;  // Prevents memory leaks by breaking any circular dependency.
-        super.onDestroy();
-    }
-
-    /**
-     * Tries to run a given Runnable object when the UI thread can. Ignore it otherwise
-     */
-    private void runOnUIThread(Runnable runnable) {
-        if (mHandler == null) {
-            Log.w(LOG_TAG, "Handler object is null. No dialog is shown.");
-        } else {
-            mHandler.post(runnable);
-        }
-    }
-
     /**
      * Scans vCard in external storage (typically SDCard) and tries to import it.
      * - When there's no SDCard available, an error dialog is shown.
diff --git a/src/com/android/contacts/vcard/ThreadStarter.java b/src/com/android/contacts/vcard/ThreadStarter.java
new file mode 100644
index 0000000..d7adad6
--- /dev/null
+++ b/src/com/android/contacts/vcard/ThreadStarter.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2010 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.vcard;
+
+public interface ThreadStarter {
+    public void start();
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/vcard/ImportVCardService.java b/src/com/android/contacts/vcard/VCardService.java
similarity index 69%
rename from src/com/android/contacts/vcard/ImportVCardService.java
rename to src/com/android/contacts/vcard/VCardService.java
index 76f597c..58e1333 100644
--- a/src/com/android/contacts/vcard/ImportVCardService.java
+++ b/src/com/android/contacts/vcard/VCardService.java
@@ -29,38 +29,51 @@
 /**
  * The class responsible for importing vCard from one ore multiple Uris.
  */
-public class ImportVCardService extends Service {
+public class VCardService extends Service {
     private final static String LOG_TAG = "ImportVCardService";
 
     /* package */ static final int MSG_IMPORT_REQUEST = 1;
+    /* package */ static final int MSG_EXPORT_REQUEST = 2;
 
-    /* package */ static final int NOTIFICATION_ID = 1000;
+    /* package */ static final int IMPORT_NOTIFICATION_ID = 1000;
+    /* package */ static final int EXPORT_NOTIFICATION_ID = 1001;
 
     /**
      * Small vCard file is imported soon, so any meassage saying "vCard import started" is
      * not needed. We show the message when the size of vCard is larger than this constant. 
      */
-    private static final int IMPORT_NOTIFICATION_THRESHOLD = 10; 
+    private static final int IMPORT_NOTIFICATION_THRESHOLD = 10;
 
     public class ImportRequestHandler extends Handler {
-        private final ImportRequestProcessor mRequestProcessor =
-                new ImportRequestProcessor(ImportVCardService.this);
+        private final ImportProcessor mImportProcessor =
+                new ImportProcessor(VCardService.this);
+        private final ExportProcessor mExportProcessor =
+                new ExportProcessor(VCardService.this);
         @Override
         public void handleMessage(Message msg) {
             switch (msg.what) {
                 case MSG_IMPORT_REQUEST: {
                     final ImportRequest parameter = (ImportRequest)msg.obj;
+                    mImportProcessor.pushRequest(parameter);
                     if (parameter.entryCount > IMPORT_NOTIFICATION_THRESHOLD) {
-                        Toast.makeText(ImportVCardService.this,
+                        Toast.makeText(VCardService.this,
                                 getString(R.string.vcard_importer_start_message),
                                 Toast.LENGTH_LONG).show();
                     }
-                    mRequestProcessor.pushRequest(parameter);
                     break;
                 }
-                default:
+                case MSG_EXPORT_REQUEST: {
+                    final ExportRequest parameter = (ExportRequest)msg.obj;
+                    mExportProcessor.pushRequest(parameter);
+                    Toast.makeText(VCardService.this,
+                            getString(R.string.vcard_exporter_start_message),
+                            Toast.LENGTH_LONG).show();
+                    break;
+                }
+                default: {
                     Log.e(LOG_TAG, "Unknown request type: " + msg.what);
                     super.hasMessages(msg.what);
+                }
             }
         }
     }
diff --git a/src/com/android/contacts/views/ContactLoader.java b/src/com/android/contacts/views/ContactLoader.java
index f03d269..87c3a34 100644
--- a/src/com/android/contacts/views/ContactLoader.java
+++ b/src/com/android/contacts/views/ContactLoader.java
@@ -458,6 +458,7 @@
             mContact = result;
             mLookupUri = result.getLookupUri();
             if (result != null) {
+                unregisterObserver();
                 if (mObserver == null) {
                     mObserver = new ForceLoadContentObserver();
                 }
@@ -472,6 +473,13 @@
         }
     }
 
+    private void unregisterObserver() {
+        if (mObserver != null) {
+            getContext().getContentResolver().unregisterContentObserver(mObserver);
+            mObserver = null;
+        }
+    }
+
     public ContactLoader(Context context, Uri lookupUri) {
         super(context);
         mLookupUri = lookupUri;
@@ -495,9 +503,6 @@
     @Override
     public void stopLoading() {
         mContact = null;
-        if (mObserver != null) {
-            getContext().getContentResolver().unregisterContentObserver(mObserver);
-        }
     }
 
     @Override
diff --git a/src/com/android/contacts/views/detail/ContactDetailFragment.java b/src/com/android/contacts/views/detail/ContactDetailFragment.java
index bfbd4d7..49471cc 100644
--- a/src/com/android/contacts/views/detail/ContactDetailFragment.java
+++ b/src/com/android/contacts/views/detail/ContactDetailFragment.java
@@ -103,6 +103,8 @@
     private Uri mLookupUri;
     private Listener mListener;
 
+    private boolean mIsInitialized;
+
     private ContactLoader.Result mContactData;
     private ContactDetailHeaderView mHeaderView;
     private ListView mListView;
@@ -194,11 +196,13 @@
 
     public void loadUri(Uri lookupUri) {
         mLookupUri = lookupUri;
-        startLoading(LOADER_DETAILS, null);
+        if (mIsInitialized) startLoading(LOADER_DETAILS, null);
     }
 
     @Override
     protected void onInitializeLoaders() {
+        mIsInitialized = true;
+        if (mLookupUri != null) startLoading(LOADER_DETAILS, null);
     }
 
     @Override
diff --git a/src/com/android/contacts/views/editor/ContactEditorFragment.java b/src/com/android/contacts/views/editor/ContactEditorFragment.java
index 4f57fb7..58f56ff 100644
--- a/src/com/android/contacts/views/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/views/editor/ContactEditorFragment.java
@@ -23,13 +23,14 @@
 import com.android.contacts.model.Sources;
 import com.android.contacts.model.ContactsSource.DataKind;
 import com.android.contacts.ui.EditContactActivity;
-import com.android.contacts.util.Constants;
 import com.android.contacts.util.DataStatus;
 import com.android.contacts.views.ContactLoader;
-import com.android.contacts.views.editor.typeViews.DataView;
-import com.android.contacts.views.editor.typeViews.FooterView;
-import com.android.contacts.views.editor.typeViews.HeaderView;
-import com.android.contacts.views.editor.typeViews.PhotoView;
+import com.android.contacts.views.editor.view.ViewTypes;
+import com.android.contacts.views.editor.viewModel.BaseViewModel;
+import com.android.contacts.views.editor.viewModel.DataViewModel;
+import com.android.contacts.views.editor.viewModel.EmailViewModel;
+import com.android.contacts.views.editor.viewModel.FooterViewModel;
+import com.android.contacts.views.editor.viewModel.PhoneViewModel;
 
 import android.app.Activity;
 import android.app.AlertDialog;
@@ -44,8 +45,6 @@
 import android.content.Intent;
 import android.content.Loader;
 import android.content.Entity.NamedContentValues;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
 import android.net.Uri;
 import android.os.Bundle;
 import android.provider.ContactsContract.CommonDataKinds;
@@ -62,7 +61,6 @@
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.provider.ContactsContract.CommonDataKinds.Website;
-import android.telephony.PhoneNumberUtils;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.ContextMenu;
@@ -78,12 +76,11 @@
 import android.widget.BaseAdapter;
 import android.widget.ListView;
 import android.widget.Toast;
-import android.widget.AdapterView.OnItemClickListener;
 
 import java.util.ArrayList;
 
 public class ContactEditorFragment extends LoaderManagingFragment<ContactLoader.Result>
-        implements OnCreateContextMenuListener, OnItemClickListener {
+        implements OnCreateContextMenuListener {
     private static final String TAG = "ContactEditorFragment";
 
     private static final String BUNDLE_RAW_CONTACT_ID = "rawContactId";
@@ -96,9 +93,11 @@
     private Uri mLookupUri;
     private Listener mListener;
 
+    private boolean mIsInitialized;
+
     private ContactLoader.Result mContactData;
     private ContactEditorHeaderView mHeaderView;
-    private ListView mListView;
+    private MyListView mListView;
     private ViewAdapter mAdapter;
 
     private int mReadOnlySourcesCnt;
@@ -108,7 +107,7 @@
     /**
      * A list of RawContacts included in this Contact.
      */
-    private ArrayList<RawContact> mRawContacts = new ArrayList<RawContact>();
+    private ArrayList<DisplayRawContact> mRawContacts = new ArrayList<DisplayRawContact>();
 
     private LayoutInflater mInflater;
 
@@ -130,13 +129,13 @@
 
         mInflater = inflater;
 
-        mHeaderView =
-                (ContactEditorHeaderView) view.findViewById(R.id.contact_header_widget);
+        mHeaderView = (ContactEditorHeaderView) view.findViewById(R.id.contact_header_widget);
 
-        mListView = (ListView) view.findViewById(android.R.id.list);
+        mListView = (MyListView) view.findViewById(android.R.id.list);
         mListView.setOnCreateContextMenuListener(this);
         mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
         mListView.setOnItemClickListener(this);
+        mListView.setItemsCanFocus(true);
 
         return view;
     }
@@ -147,11 +146,13 @@
 
     public void loadUri(Uri lookupUri) {
         mLookupUri = lookupUri;
-        startLoading(LOADER_DETAILS, null);
+        if (mIsInitialized) startLoading(LOADER_DETAILS, null);
     }
 
     @Override
     protected void onInitializeLoaders() {
+        mIsInitialized = true;
+        if (mLookupUri != null) startLoading(LOADER_DETAILS, null);
     }
 
     @Override
@@ -248,8 +249,8 @@
                 mReadOnlySourcesCnt += 1;
             }
 
-            final RawContact rawContact =
-                    new RawContact(contactsSource, accountName, rawContactId, writable);
+            final DisplayRawContact rawContact = new DisplayRawContact(mContext, contactsSource,
+                    accountName, rawContactId, writable, mRawContactFooterListener);
             mRawContacts.add(rawContact);
 
             for (NamedContentValues subValue : entity.getSubValues()) {
@@ -264,7 +265,22 @@
                         ContactsSource.LEVEL_MIMETYPES);
                 if (kind == null) continue;
 
-                final DataViewEntry entry = new DataViewEntry(mContext, mimeType, kind,
+                if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                    final PhoneViewModel itemEditor = PhoneViewModel.createForExisting(mContext,
+                            rawContact, dataId, entryValues, kind.titleRes);
+                    rawContact.getFields().add(itemEditor);
+                    continue;
+                }
+
+                if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
+                    final EmailViewModel itemEditor = EmailViewModel.createForExisting(mContext,
+                            rawContact, dataId, entryValues, kind.titleRes);
+                    rawContact.getFields().add(itemEditor);
+                    continue;
+                }
+
+
+                final DataViewModel entry = new DataViewModel(mContext, mimeType, kind,
                         rawContact, dataId, entryValues);
 
                 final boolean hasData = !TextUtils.isEmpty(entry.data);
@@ -332,223 +348,12 @@
         }
     }
 
-    private static String buildActionString(DataKind kind, ContentValues values,
-            boolean lowerCase, Context context) {
-        if (kind.actionHeader == null) {
-            return null;
-        }
-        CharSequence actionHeader = kind.actionHeader.inflateUsing(context, values);
-        if (actionHeader == null) {
-            return null;
-        }
-        return lowerCase ? actionHeader.toString().toLowerCase() : actionHeader.toString();
-    }
-
-    private static String buildDataString(DataKind kind, ContentValues values,
-            Context context) {
-        if (kind.actionBody == null) {
-            return null;
-        }
-        CharSequence actionBody = kind.actionBody.inflateUsing(context, values);
-        return actionBody == null ? null : actionBody.toString();
-    }
-
-    private abstract static class BaseViewEntry {
-        private final RawContact mRawContact;
-
-        public BaseViewEntry(RawContact rawContact) {
-            mRawContact = rawContact;
-        }
-
-        public RawContact getRawContact() {
-            return mRawContact;
-        }
-
-        public abstract int getEntryType();
-        public abstract View getView(View convertView, ViewGroup parent);
-    }
-
-    private class HeaderViewEntry extends BaseViewEntry {
-        private boolean mCollapsed;
-
-        public HeaderViewEntry(RawContact rawContact) {
-            super(rawContact);
-        }
-
-        public boolean isCollapsed() {
-            return mCollapsed;
-        }
-
-        public void setCollapsed(boolean collapsed) {
-            mCollapsed = collapsed;
-        }
-
-        @Override
-        public int getEntryType() {
-            return ItemTypes.RAW_CONTACT_HEADER;
-        }
-
-        @Override
-        public View getView(View convertView, ViewGroup parent) {
-            final HeaderView result = convertView != null
-                    ? (HeaderView) convertView
-                    : HeaderView.inflate(mInflater, parent, false);
-
-            CharSequence accountType = getRawContact().getSource().getDisplayLabel(mContext);
-            if (TextUtils.isEmpty(accountType)) {
-                accountType = mContext.getString(R.string.account_phone);
-            }
-            final String accountName = getRawContact().getAccountName();
-
-            final String accountTypeDisplay;
-            if (TextUtils.isEmpty(accountName)) {
-                accountTypeDisplay = mContext.getString(R.string.account_type_format,
-                        accountType);
-            } else {
-                accountTypeDisplay = mContext.getString(R.string.account_type_and_name,
-                        accountType, accountName);
-            }
-
-            result.setCaptionText(accountTypeDisplay);
-            result.setLogo(getRawContact().getSource().getDisplayIcon(mContext));
-
-            return result;
-        }
-    }
-
-    public class FooterViewEntry extends BaseViewEntry {
-        public FooterViewEntry(RawContact rawContact) {
-            super(rawContact);
-        }
-
-        @Override
-        public int getEntryType() {
-            return ItemTypes.RAW_CONTACT_FOOTER;
-        }
-
-        @Override
-        public View getView(View convertView, ViewGroup parent) {
-            final FooterView result = convertView != null
-                    ? (FooterView) convertView
-                    : FooterView.inflate(mInflater, parent, false);
-
-            result.setListener(mViewListener);
-            return result;
-        }
-
-        private FooterView.Listener mViewListener = new FooterView.Listener() {
-            public void onAddClicked() {
-              // Create a bundle to show the Dialog
-              final Bundle bundle = new Bundle();
-              bundle.putLong(BUNDLE_RAW_CONTACT_ID, getRawContact().getId());
-              if (mListener != null) {
-                  mListener.onDialogRequested(R.id.edit_dialog_add_information, bundle);
-              }
-            }
-            public void onSeparateClicked() {
-            }
-            public void onDeleteClicked() {
-            }
-        };
-    }
-
-    private class DataViewEntry extends BaseViewEntry {
-        public String label;
-        public String data;
-        public Uri uri;
-        public long id = 0;
-        public int maxLines = 1;
-        public String mimetype;
-
-        public int actionIcon = -1;
-        public boolean isPrimary = false;
-        public Intent intent;
-        public Intent secondaryIntent = null;
-        public int maxLabelLines = 1;
-        public byte[] binaryData = null;
-
-        /**
-         * Build new {@link DataViewEntry} and populate from the given values.
-         */
-        public DataViewEntry(Context context, String mimeType, DataKind kind,
-                RawContact rawContact, long dataId, ContentValues values) {
-            super(rawContact);
-            id = dataId;
-            uri = ContentUris.withAppendedId(Data.CONTENT_URI, id);
-            mimetype = mimeType;
-            label = buildActionString(kind, values, false, context);
-            data = buildDataString(kind, values, context);
-            binaryData = values.getAsByteArray(Data.DATA15);
-        }
-
-        @Override
-        public int getEntryType() {
-            return Photo.CONTENT_ITEM_TYPE.equals(mimetype) ? ItemTypes.PHOTO : ItemTypes.DATA;
-        }
-
-        @Override
-        public View getView(View convertView, ViewGroup parent) {
-            // Special Case: Photo
-            if (Photo.CONTENT_ITEM_TYPE.equals(mimetype)) {
-                final PhotoView result = convertView != null
-                        ? (PhotoView) convertView
-                        : PhotoView.inflate(mInflater, parent, false);
-
-                final Bitmap bitmap = binaryData != null
-                        ? BitmapFactory.decodeByteArray(binaryData, 0, binaryData.length)
-                        : null;
-                result.setPhoto(bitmap);
-                return result;
-            }
-
-            // All other cases
-            final DataView result = convertView != null
-                    ? (DataView) convertView
-                    : DataView.inflate(mInflater, parent, false);
-
-            // Set the label
-            result.setLabelText(label, maxLabelLines);
-
-            // Set data
-            if (data != null) {
-                if (Phone.CONTENT_ITEM_TYPE.equals(mimetype)
-                        || Constants.MIME_SMS_ADDRESS.equals(mimetype)) {
-                    result.setDataText(PhoneNumberUtils.formatNumber(data), maxLines);
-                } else {
-                    result.setDataText(data, maxLines);
-                }
-            } else {
-                result.setDataText("", maxLines);
-            }
-
-            // Set the primary icon
-            result.setPrimary(isPrimary);
-
-            // Set the action icon
-            result.setPrimaryIntent(intent, mContext.getResources(), actionIcon);
-
-            // Set the secondary action button
-            // TODO: Change this to our new form
-            result.setSecondaryIntent(null, null, 0);
-            return result;
-        }
-    }
-
-    /** Possible Item Types */
-    private interface ItemTypes {
-        public static final int DATA = 0;
-        public static final int PHOTO = 1;
-        public static final int RAW_CONTACT_HEADER = 2;
-        public static final int RAW_CONTACT_FOOTER = 3;
-        public static final int _COUNT = 4;
-    }
-
     private final class ViewAdapter extends BaseAdapter {
         public View getView(int position, View convertView, ViewGroup parent) {
             final View result;
 
-            final BaseViewEntry viewEntry = getEntry(position);
-            return viewEntry.getView(convertView, parent);
+            final BaseViewModel viewEntry = getEntry(position);
+            return viewEntry.getView(mInflater, convertView, parent);
         }
 
         public Object getItem(int position) {
@@ -560,9 +365,9 @@
             return position;
         }
 
-        private BaseViewEntry getEntry(int position) {
+        private BaseViewModel getEntry(int position) {
             for (int i = 0; i < mRawContacts.size(); i++) {
-                final RawContact rawContact = mRawContacts.get(i);
+                final DisplayRawContact rawContact = mRawContacts.get(i);
                 if (position == 0) return rawContact.getHeader();
 
                 // Collapsed header? Count one item and continue
@@ -571,7 +376,7 @@
                     continue;
                 }
 
-                final ArrayList<DataViewEntry> fields = rawContact.getFields();
+                final ArrayList<BaseViewModel> fields = rawContact.getFields();
                 // +1 for header, +1 for footer
                 final int fieldCount = fields.size() + 2;
                 if (position == fieldCount - 1) {
@@ -588,7 +393,7 @@
 
         @Override
         public int getViewTypeCount() {
-            return ItemTypes._COUNT;
+            return ViewTypes._COUNT;
         }
 
         @Override
@@ -599,7 +404,7 @@
         public int getCount() {
             int result = 0;
             for (int i = 0; i < mRawContacts.size(); i++) {
-                final RawContact rawContact = mRawContacts.get(i);
+                final DisplayRawContact rawContact = mRawContacts.get(i);
                 if (rawContact.getHeader().isCollapsed()) {
                     // just one header item
                     result++;
@@ -612,14 +417,17 @@
         }
     }
 
+    @Override
     public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
         inflater.inflate(R.menu.view, menu);
     }
 
+    @Override
     public void onPrepareOptionsMenu(Menu menu) {
         // TODO: Prepare options
     }
 
+    @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         switch (item.getItemId()) {
             case R.id.menu_edit: {
@@ -627,7 +435,7 @@
                 // later
                 final Intent intent = new Intent();
                 intent.setClass(mContext, EditContactActivity.class);
-                final long rawContactId = mRawContacts.get(0).mId;
+                final long rawContactId = mRawContacts.get(0).getId();
                 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI,
                         rawContactId);
                 intent.setAction(Intent.ACTION_EDIT);
@@ -684,6 +492,7 @@
         if (mListener != null) mListener.onDialogRequested(dialogId, null);
     }
 
+    @Override
     public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
         AdapterView.AdapterContextMenuInfo info;
         try {
@@ -699,9 +508,9 @@
             return;
         }
 
-        final BaseViewEntry baseEntry = mAdapter.getEntry(info.position);
-        if (baseEntry instanceof DataViewEntry) {
-            final DataViewEntry entry = (DataViewEntry) baseEntry;
+        final BaseViewModel baseEntry = mAdapter.getEntry(info.position);
+        if (baseEntry instanceof DataViewModel) {
+            final DataViewModel entry = (DataViewModel) baseEntry;
             menu.setHeaderTitle(R.string.contactOptionsTitle);
             if (entry.mimetype.equals(CommonDataKinds.Phone.CONTENT_ITEM_TYPE)) {
                 menu.add(0, 0, 0, R.string.menu_call).setIntent(entry.intent);
@@ -720,23 +529,19 @@
         }
     }
 
-    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
-        if (mListener == null) return;
-        final BaseViewEntry baseEntry = mAdapter.getEntry(position);
-        if (baseEntry == null) return;
-
-        if (baseEntry instanceof HeaderViewEntry) {
-            // Toggle rawcontact visibility
-            final HeaderViewEntry entry = (HeaderViewEntry) baseEntry;
-            entry.setCollapsed(!entry.isCollapsed());
-            mAdapter.notifyDataSetChanged();
-        } else if (baseEntry instanceof DataViewEntry) {
-            final DataViewEntry entry = (DataViewEntry) baseEntry;
-            final Intent intent = entry.intent;
-            if (intent == null) return;
-            mListener.onEditorRequested(intent);
-        }
-    }
+    // This was the ListView based code to expand/collapse sections.
+//    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+//        if (mListener == null) return;
+//        final BaseViewModel baseEntry = mAdapter.getEntry(position);
+//        if (baseEntry == null) return;
+//
+//        if (baseEntry instanceof HeaderViewModel) {
+//            // Toggle rawcontact visibility
+//            final HeaderViewModel entry = (HeaderViewModel) baseEntry;
+//            entry.setCollapsed(!entry.isCollapsed());
+//            mAdapter.notifyDataSetChanged();
+//        }
+//    }
 
     private final DialogInterface.OnClickListener mDeleteListener =
             new DialogInterface.OnClickListener() {
@@ -787,7 +592,7 @@
             }
             case R.id.edit_dialog_add_information: {
                 final long rawContactId = bundle.getLong(BUNDLE_RAW_CONTACT_ID);
-                final RawContact rawContact = findRawContactById(rawContactId);
+                final DisplayRawContact rawContact = findRawContactById(rawContactId);
                 if (rawContact == null) return null;
                 final ContactsSource source = rawContact.getSource();
 
@@ -831,13 +636,14 @@
         }
     }
 
-    private RawContact findRawContactById(long rawContactId) {
-        for (RawContact rawContact : mRawContacts) {
+    private DisplayRawContact findRawContactById(long rawContactId) {
+        for (DisplayRawContact rawContact : mRawContacts) {
             if (rawContact.getId() == rawContactId) return rawContact;
         }
         return null;
     }
 
+    @Override
     public boolean onContextItemSelected(MenuItem item) {
         switch (item.getItemId()) {
             case MENU_ITEM_MAKE_DEFAULT: {
@@ -852,11 +658,11 @@
     }
 
     private boolean makeItemDefault(MenuItem item) {
-        final BaseViewEntry baseEntry = getViewEntryForMenuItem(item);
-        if (baseEntry == null || !(baseEntry instanceof DataViewEntry)) {
+        final BaseViewModel baseEntry = getViewEntryForMenuItem(item);
+        if (baseEntry == null || !(baseEntry instanceof DataViewModel)) {
             return false;
         }
-        final DataViewEntry entry = (DataViewEntry) baseEntry;
+        final DataViewModel entry = (DataViewModel) baseEntry;
 
         // Update the primary values in the data record.
         ContentValues values = new ContentValues(1);
@@ -867,7 +673,7 @@
         return true;
     }
 
-    private BaseViewEntry getViewEntryForMenuItem(MenuItem item) {
+    private BaseViewModel getViewEntryForMenuItem(MenuItem item) {
         final AdapterView.AdapterContextMenuInfo info;
         try {
              info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
@@ -879,50 +685,21 @@
         return mAdapter.getEntry(info.position);
     }
 
-    private class RawContact {
-        private final ContactsSource mSource;
-        private String mAccountName;
-        private final long mId;
-        private boolean mWritable;
-        private final HeaderViewEntry mHeader = new HeaderViewEntry(this);
-        private final FooterViewEntry mFooter = new FooterViewEntry(this);
-        private final ArrayList<DataViewEntry> mFields = new ArrayList<DataViewEntry>();
-
-        public RawContact(ContactsSource source, String accountName, long id, boolean writable) {
-            mSource = source;
-            mAccountName = accountName;
-            mId = id;
-            mWritable = writable;
+    private FooterViewModel.Listener mRawContactFooterListener =
+            new FooterViewModel.Listener() {
+        public void onAddClicked(DisplayRawContact rawContact) {
+            // Create a bundle to show the Dialog
+            final Bundle bundle = new Bundle();
+            bundle.putLong(BUNDLE_RAW_CONTACT_ID, rawContact.getId());
+            if (mListener != null) {
+                mListener.onDialogRequested(R.id.edit_dialog_add_information, bundle);
+            }
         }
-
-        public ContactsSource getSource() {
-            return mSource;
+        public void onSeparateClicked(DisplayRawContact rawContact) {
         }
-
-        public String getAccountName() {
-            return mAccountName;
+        public void onDeleteClicked(DisplayRawContact rawContact) {
         }
-
-        public long getId() {
-            return mId;
-        }
-
-        public boolean isWritable() {
-            return mWritable;
-        }
-
-        public ArrayList<DataViewEntry> getFields() {
-            return mFields;
-        }
-
-        public HeaderViewEntry getHeader() {
-            return mHeader;
-        }
-
-        public FooterViewEntry getFooter() {
-            return mFooter;
-        }
-    }
+    };
 
     public static interface Listener {
         /**
diff --git a/src/com/android/contacts/views/editor/DisplayRawContact.java b/src/com/android/contacts/views/editor/DisplayRawContact.java
new file mode 100644
index 0000000..0e6564b
--- /dev/null
+++ b/src/com/android/contacts/views/editor/DisplayRawContact.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.views.editor;
+
+import com.android.contacts.model.ContactsSource;
+import com.android.contacts.views.editor.viewModel.BaseViewModel;
+import com.android.contacts.views.editor.viewModel.FooterViewModel;
+import com.android.contacts.views.editor.viewModel.HeaderViewModel;
+
+import android.content.Context;
+
+import java.util.ArrayList;
+
+public class DisplayRawContact {
+    private final ContactsSource mSource;
+    private String mAccountName;
+    private final long mId;
+    private boolean mWritable;
+    private final HeaderViewModel mHeader;
+    private final FooterViewModel mFooter;
+    private final ArrayList<BaseViewModel> mFields = new ArrayList<BaseViewModel>();
+
+    public DisplayRawContact(Context context, ContactsSource source, String accountName, long id,
+            boolean writable, FooterViewModel.Listener footerListener) {
+        mSource = source;
+        mAccountName = accountName;
+        mId = id;
+        mWritable = writable;
+        mHeader = new HeaderViewModel(context, this);
+        mFooter = new FooterViewModel(context, this, footerListener);
+    }
+
+    public ContactsSource getSource() {
+        return mSource;
+    }
+
+    public String getAccountName() {
+        return mAccountName;
+    }
+
+    public long getId() {
+        return mId;
+    }
+
+    public boolean isWritable() {
+        return mWritable;
+    }
+
+    public ArrayList<BaseViewModel> getFields() {
+        return mFields;
+    }
+
+    public HeaderViewModel getHeader() {
+        return mHeader;
+    }
+
+    public FooterViewModel getFooter() {
+        return mFooter;
+    }
+}
diff --git a/src/com/android/contacts/views/editor/MyListView.java b/src/com/android/contacts/views/editor/MyListView.java
new file mode 100644
index 0000000..12651c1
--- /dev/null
+++ b/src/com/android/contacts/views/editor/MyListView.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.views.editor;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.BaseAdapter;
+import android.widget.LinearLayout;
+
+/**
+ * Compatibility pseudo-ListView. This just renders everything into a LinearLayout using a ListView
+ * adapter. If this turns out to be fast enough, we can keep using this. This view will be removed
+ * once the decision has been made
+ */
+public class MyListView extends LinearLayout {
+    public MyListView(Context context) {
+        super(context);
+    }
+
+    public MyListView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public MyListView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        setOrientation(VERTICAL);
+    }
+
+    public void setOnItemClickListener(ContactEditorFragment contactEditorFragment) {
+        // Keep compatibility with ListView
+    }
+
+    public void setItemsCanFocus(boolean value) {
+        // Keep compatibility with ListView
+    }
+
+    public void setAdapter(BaseAdapter adapter) {
+        removeAllViews();
+        for (int i = 0; i < adapter.getCount(); i++) {
+            final View childView = adapter.getView(i, null, this);
+            addView(childView);
+        }
+    }
+}
diff --git a/src/com/android/contacts/views/editor/typeViews/DataView.java b/src/com/android/contacts/views/editor/view/DataView.java
similarity index 97%
rename from src/com/android/contacts/views/editor/typeViews/DataView.java
rename to src/com/android/contacts/views/editor/view/DataView.java
index f146e36..e4cd1de 100644
--- a/src/com/android/contacts/views/editor/typeViews/DataView.java
+++ b/src/com/android/contacts/views/editor/view/DataView.java
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.contacts.views.editor.typeViews;
+package com.android.contacts.views.editor.view;
 
 import com.android.contacts.R;
 
@@ -52,7 +52,8 @@
 
     public static DataView inflate(LayoutInflater inflater, ViewGroup parent,
             boolean attachToRoot) {
-        return (DataView) inflater.inflate(R.layout.list_edit_item_text_icons, parent, attachToRoot);
+        return (DataView) inflater.inflate(R.layout.list_edit_item_text_icons, parent,
+                attachToRoot);
     }
 
     @Override
diff --git a/src/com/android/contacts/views/editor/view/FieldAndTypeView.java b/src/com/android/contacts/views/editor/view/FieldAndTypeView.java
new file mode 100644
index 0000000..69e35b3
--- /dev/null
+++ b/src/com/android/contacts/views/editor/view/FieldAndTypeView.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.views.editor.view;
+
+import com.android.contacts.R;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class FieldAndTypeView extends LinearLayout {
+    private TextView mCaptionTextView;
+    private EditText mFieldEditText;
+    private Button mTypeButton;
+    private Listener mListener;
+    private boolean mHasFocus;
+
+    public FieldAndTypeView(Context context) {
+        super(context);
+    }
+
+    public FieldAndTypeView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public FieldAndTypeView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    public static FieldAndTypeView inflate(LayoutInflater inflater, ViewGroup parent,
+            boolean attachToRoot) {
+        return (FieldAndTypeView) inflater.inflate(R.layout.list_edit_item_field_and_type,
+                parent, attachToRoot);
+    }
+
+    public void setListener(Listener value) {
+        mListener = value;
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        mCaptionTextView = (TextView) findViewById(R.id.caption);
+        mFieldEditText = (EditText) findViewById(R.id.field);
+        mFieldEditText.setOnFocusChangeListener(mFieldEditTextFocusChangeListener);
+        mTypeButton = (Button) findViewById(R.id.type);
+    }
+
+    public void setLabelText(int resId) {
+        mCaptionTextView.setText(resId);
+    }
+
+    public void setFieldValue(CharSequence value) {
+        mFieldEditText.setText(value);
+    }
+
+    public CharSequence getFieldValue() {
+        return mFieldEditText.getText();
+    }
+
+    public void setTypeDisplayLabel(CharSequence type) {
+        mTypeButton.setText(type);
+    }
+
+    private OnFocusChangeListener mFieldEditTextFocusChangeListener = new OnFocusChangeListener() {
+        public void onFocusChange(View v, boolean hasFocus) {
+            if (mHasFocus && !hasFocus && mListener != null) {
+                mListener.onFocusLost(FieldAndTypeView.this);
+            }
+            mHasFocus = hasFocus;
+        }
+    };
+
+    public interface Listener {
+        void onFocusLost(FieldAndTypeView view);
+    }
+}
diff --git a/src/com/android/contacts/views/editor/typeViews/FooterView.java b/src/com/android/contacts/views/editor/view/FooterView.java
similarity index 97%
rename from src/com/android/contacts/views/editor/typeViews/FooterView.java
rename to src/com/android/contacts/views/editor/view/FooterView.java
index d8ae66d..adc47cd 100644
--- a/src/com/android/contacts/views/editor/typeViews/FooterView.java
+++ b/src/com/android/contacts/views/editor/view/FooterView.java
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.contacts.views.editor.typeViews;
+package com.android.contacts.views.editor.view;
 
 import com.android.contacts.R;
 
diff --git a/src/com/android/contacts/views/editor/typeViews/HeaderView.java b/src/com/android/contacts/views/editor/view/HeaderView.java
similarity index 97%
rename from src/com/android/contacts/views/editor/typeViews/HeaderView.java
rename to src/com/android/contacts/views/editor/view/HeaderView.java
index bd69c50..6822520 100644
--- a/src/com/android/contacts/views/editor/typeViews/HeaderView.java
+++ b/src/com/android/contacts/views/editor/view/HeaderView.java
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.contacts.views.editor.typeViews;
+package com.android.contacts.views.editor.view;
 
 import com.android.contacts.R;
 
diff --git a/src/com/android/contacts/views/editor/typeViews/PhotoView.java b/src/com/android/contacts/views/editor/view/PhotoView.java
similarity index 97%
rename from src/com/android/contacts/views/editor/typeViews/PhotoView.java
rename to src/com/android/contacts/views/editor/view/PhotoView.java
index 9e63a79..d43e90e 100644
--- a/src/com/android/contacts/views/editor/typeViews/PhotoView.java
+++ b/src/com/android/contacts/views/editor/view/PhotoView.java
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.contacts.views.editor.typeViews;
+package com.android.contacts.views.editor.view;
 
 import com.android.contacts.R;
 
diff --git a/src/com/android/contacts/views/editor/view/ViewTypes.java b/src/com/android/contacts/views/editor/view/ViewTypes.java
new file mode 100644
index 0000000..7cd2702
--- /dev/null
+++ b/src/com/android/contacts/views/editor/view/ViewTypes.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.views.editor.view;
+
+public interface ViewTypes {
+    public static final int DATA = 0;
+    public static final int FIELD_AND_TYPE = 1;
+    public static final int PHOTO = 2;
+    public static final int RAW_CONTACT_HEADER = 3;
+    public static final int RAW_CONTACT_FOOTER = 4;
+    public static final int _COUNT = 5;
+}
diff --git a/src/com/android/contacts/views/editor/viewModel/BaseViewModel.java b/src/com/android/contacts/views/editor/viewModel/BaseViewModel.java
new file mode 100644
index 0000000..0a324ca
--- /dev/null
+++ b/src/com/android/contacts/views/editor/viewModel/BaseViewModel.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.views.editor.viewModel;
+
+import com.android.contacts.views.editor.DisplayRawContact;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+public abstract class BaseViewModel {
+    // TODO: Consider just storing the Id of the RawContact. Would prevent some cyclic references
+    private final DisplayRawContact mRawContact;
+    private final Context mContext;
+
+    public BaseViewModel(Context context, DisplayRawContact rawContact) {
+        if (context == null) throw new IllegalArgumentException("context must not be null");
+        if (rawContact == null) throw new IllegalArgumentException("rawContact must not be null");
+        mContext = context;
+        mRawContact = rawContact;
+    }
+
+    public DisplayRawContact getRawContact() {
+        return mRawContact;
+    }
+
+    public Context getContext() {
+        return mContext;
+    }
+
+    public abstract int getEntryType();
+    public abstract View getView(LayoutInflater inflater, View convertView, ViewGroup parent);
+}
diff --git a/src/com/android/contacts/views/editor/viewModel/DataViewModel.java b/src/com/android/contacts/views/editor/viewModel/DataViewModel.java
new file mode 100644
index 0000000..fb1c4c5
--- /dev/null
+++ b/src/com/android/contacts/views/editor/viewModel/DataViewModel.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.views.editor.viewModel;
+
+import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.util.Constants;
+import com.android.contacts.views.editor.DisplayRawContact;
+import com.android.contacts.views.editor.view.DataView;
+import com.android.contacts.views.editor.view.PhotoView;
+import com.android.contacts.views.editor.view.ViewTypes;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.telephony.PhoneNumberUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+public class DataViewModel extends BaseViewModel {
+    public String label;
+    public String data;
+    public Uri uri;
+    public long id = 0;
+    public int maxLines = 1;
+    public String mimetype;
+
+    public int actionIcon = -1;
+    public boolean isPrimary = false;
+    public Intent intent;
+    public Intent secondaryIntent = null;
+    public int maxLabelLines = 1;
+    public byte[] binaryData = null;
+
+    /**
+     * Build new {@link DataViewModel} and populate from the given values.
+     */
+    public DataViewModel(Context context, String mimeType, DataKind kind,
+            DisplayRawContact rawContact, long dataId, ContentValues values) {
+        super(context, rawContact);
+        id = dataId;
+        uri = ContentUris.withAppendedId(Data.CONTENT_URI, id);
+        mimetype = mimeType;
+        label = buildActionString(kind, values, false, context);
+        data = buildDataString(kind, values, context);
+        binaryData = values.getAsByteArray(Data.DATA15);
+    }
+
+    @Override
+    public int getEntryType() {
+        return Photo.CONTENT_ITEM_TYPE.equals(mimetype) ? ViewTypes.PHOTO : ViewTypes.DATA;
+    }
+
+    @Override
+    public View getView(LayoutInflater inflater, View convertView, ViewGroup parent) {
+        // Special Case: Photo
+        if (Photo.CONTENT_ITEM_TYPE.equals(mimetype)) {
+            final PhotoView result = convertView != null
+                    ? (PhotoView) convertView
+                    : PhotoView.inflate(inflater, parent, false);
+
+            final Bitmap bitmap = binaryData != null
+                    ? BitmapFactory.decodeByteArray(binaryData, 0, binaryData.length)
+                    : null;
+            result.setPhoto(bitmap);
+            return result;
+        }
+
+        // All other cases
+        final DataView result = convertView != null
+                ? (DataView) convertView
+                : DataView.inflate(inflater, parent, false);
+
+        // Set the label
+        result.setLabelText(label, maxLabelLines);
+
+        // Set data
+        if (data != null) {
+            if (Phone.CONTENT_ITEM_TYPE.equals(mimetype)
+                    || Constants.MIME_SMS_ADDRESS.equals(mimetype)) {
+                result.setDataText(PhoneNumberUtils.formatNumber(data), maxLines);
+            } else {
+                result.setDataText(data, maxLines);
+            }
+        } else {
+            result.setDataText("", maxLines);
+        }
+
+        // Set the primary icon
+        result.setPrimary(isPrimary);
+
+        // Set the action icon
+        result.setPrimaryIntent(intent, getContext().getResources(), actionIcon);
+
+        // Set the secondary action button
+        // TODO: Change this to our new form
+        result.setSecondaryIntent(null, null, 0);
+        return result;
+    }
+
+    private static String buildActionString(DataKind kind, ContentValues values,
+            boolean lowerCase, Context context) {
+        if (kind.actionHeader == null) {
+            return null;
+        }
+        CharSequence actionHeader = kind.actionHeader.inflateUsing(context, values);
+        if (actionHeader == null) {
+            return null;
+        }
+        return lowerCase ? actionHeader.toString().toLowerCase() : actionHeader.toString();
+    }
+
+    private static String buildDataString(DataKind kind, ContentValues values,
+            Context context) {
+        if (kind.actionBody == null) {
+            return null;
+        }
+        CharSequence actionBody = kind.actionBody.inflateUsing(context, values);
+        return actionBody == null ? null : actionBody.toString();
+    }
+
+}
diff --git a/src/com/android/contacts/views/editor/viewModel/EmailViewModel.java b/src/com/android/contacts/views/editor/viewModel/EmailViewModel.java
new file mode 100644
index 0000000..70bb8ea
--- /dev/null
+++ b/src/com/android/contacts/views/editor/viewModel/EmailViewModel.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.views.editor.viewModel;
+
+import com.android.contacts.views.editor.DisplayRawContact;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+
+public class EmailViewModel extends FieldAndTypeViewModel {
+    private EmailViewModel(Context context, DisplayRawContact rawContact, long dataId,
+            ContentValues contentValues, int titleResId) {
+        super(context, rawContact, dataId, contentValues, titleResId, Email.ADDRESS, Email.TYPE,
+                Email.LABEL);
+    }
+
+    public static EmailViewModel createForExisting(Context context, DisplayRawContact rawContact,
+            long dataId, ContentValues contentValues, int titleResId) {
+        return new EmailViewModel(context, rawContact, dataId, contentValues, titleResId);
+    }
+
+    @Override
+    protected CharSequence getTypeDisplayLabel() {
+        return Email.getTypeLabel(getContext().getResources(), getType(), getLabel());
+    }
+}
diff --git a/src/com/android/contacts/views/editor/viewModel/FieldAndTypeViewModel.java b/src/com/android/contacts/views/editor/viewModel/FieldAndTypeViewModel.java
new file mode 100644
index 0000000..e6468c6
--- /dev/null
+++ b/src/com/android/contacts/views/editor/viewModel/FieldAndTypeViewModel.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.views.editor.viewModel;
+
+import com.android.contacts.views.ContactSaveService;
+import com.android.contacts.views.editor.DisplayRawContact;
+import com.android.contacts.views.editor.view.FieldAndTypeView;
+import com.android.contacts.views.editor.view.ViewTypes;
+import com.android.internal.util.ArrayUtils;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ContentProviderOperation.Builder;
+import android.net.Uri;
+import android.provider.ContactsContract.Data;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+
+public abstract class FieldAndTypeViewModel extends BaseViewModel {
+    private static final String TAG = "FieldAndTypeViewModel";
+
+    private final long mDataId;
+    private final ContentValues mContentValues;
+    private final int mLabelResId;
+    private final Uri mDataUri;
+    private final String mFieldColumn;
+    private final String mLabelColumn;
+    private final String mTypeColumn;
+
+    protected FieldAndTypeViewModel(Context context, DisplayRawContact rawContact,
+            long dataId, ContentValues contentValues, int labelResId, String fieldColumn,
+            String typeColumn, String labelColumn) {
+        super(context, rawContact);
+        mDataId = dataId;
+        mDataUri = ContentUris.withAppendedId(Data.CONTENT_URI, mDataId);
+        mContentValues = contentValues;
+        mLabelResId = labelResId;
+
+        mFieldColumn = fieldColumn;
+        mTypeColumn = typeColumn;
+        mLabelColumn = labelColumn;
+    }
+
+    @Override
+    public int getEntryType() {
+        return ViewTypes.FIELD_AND_TYPE;
+    }
+
+    @Override
+    public FieldAndTypeView getView(LayoutInflater inflater, View convertView, ViewGroup parent) {
+        final FieldAndTypeView result = convertView != null
+                ? (FieldAndTypeView) convertView
+                : FieldAndTypeView.inflate(inflater, parent, false);
+
+        result.setListener(mViewListener);
+        result.setLabelText(mLabelResId);
+        result.setFieldValue(getFieldValue());
+        result.setTypeDisplayLabel(getTypeDisplayLabel());
+
+        return result;
+    }
+
+    public long getDataId() {
+        return mDataId;
+    }
+
+    public ContentValues getContentValues() {
+        return mContentValues;
+    }
+
+    private void saveData() {
+        final ContentResolver resolver = getContext().getContentResolver();
+
+        final ArrayList<ContentProviderOperation> operations =
+            new ArrayList<ContentProviderOperation>();
+
+        final Builder builder;
+//        if (getDataUri() == null) {
+//            // INSERT
+//            builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
+//            builder.withValue(Data.MIMETYPE, mMimeType);
+//            builder.withValue(Data.RAW_CONTACT_ID, getRawContactId());
+//            writeToBuilder(builder);
+//        } else {
+            // UPDATE
+            builder = ContentProviderOperation.newUpdate(mDataUri);
+            writeToBuilder(builder);
+//        }
+        operations.add(builder.build());
+
+        // Tell the Service to save
+        final Intent serviceIntent = new Intent();
+        final ContentProviderOperation[] operationsArray =
+                operations.toArray(ArrayUtils.emptyArray(ContentProviderOperation.class));
+        serviceIntent.putExtra(ContactSaveService.EXTRA_OPERATIONS, operationsArray);
+        serviceIntent.setClass(getContext().getApplicationContext(), ContactSaveService.class);
+
+        getContext().startService(serviceIntent);
+    }
+
+    private void writeToBuilder(final Builder builder) {
+        builder.withValue(mFieldColumn, getFieldValue());
+        builder.withValue(mTypeColumn, getType());
+        builder.withValue(mLabelColumn, getLabel());
+    }
+
+    protected String getFieldValue() {
+        return getContentValues().getAsString(mFieldColumn);
+    }
+
+    private void putFieldValue(String value) {
+        getContentValues().put(mFieldColumn, value);
+    }
+
+    public int getType() {
+        return getContentValues().getAsInteger(mTypeColumn).intValue();
+    }
+
+    private void putType(int value) {
+        getContentValues().put(mTypeColumn, value);
+    }
+
+    public String getLabel() {
+        return getContentValues().getAsString(mLabelColumn);
+    }
+
+    private void putLabel(String value) {
+        getContentValues().put(mLabelColumn, value);
+    }
+
+    protected abstract CharSequence getTypeDisplayLabel();
+
+    private FieldAndTypeView.Listener mViewListener = new FieldAndTypeView.Listener() {
+        public void onFocusLost(FieldAndTypeView view) {
+            Log.v(TAG, "Received FocusLost. Checking for changes");
+            boolean hasChanged = false;
+
+            final String oldValue = getFieldValue();
+            final String newValue = view.getFieldValue().toString();
+            if (!oldValue.equals(newValue)) {
+                putFieldValue(newValue);
+                hasChanged = true;
+            }
+            if (hasChanged) {
+                Log.v(TAG, "Found changes. Updating DB");
+                saveData();
+            }
+        }
+    };
+}
diff --git a/src/com/android/contacts/views/editor/viewModel/FooterViewModel.java b/src/com/android/contacts/views/editor/viewModel/FooterViewModel.java
new file mode 100644
index 0000000..9277a8b
--- /dev/null
+++ b/src/com/android/contacts/views/editor/viewModel/FooterViewModel.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.views.editor.viewModel;
+
+import com.android.contacts.views.editor.DisplayRawContact;
+import com.android.contacts.views.editor.view.FooterView;
+import com.android.contacts.views.editor.view.ViewTypes;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+public class FooterViewModel extends BaseViewModel {
+    private final Listener mListener;
+
+    public FooterViewModel(Context context, DisplayRawContact rawContact, Listener listener) {
+        super(context, rawContact);
+        if (listener == null) throw new IllegalArgumentException("listener must not be null");
+        mListener = listener;
+    }
+
+    @Override
+    public int getEntryType() {
+        return ViewTypes.RAW_CONTACT_FOOTER;
+    }
+
+    @Override
+    public View getView(LayoutInflater inflater, View convertView, ViewGroup parent) {
+        final FooterView result = convertView != null
+                ? (FooterView) convertView
+                : FooterView.inflate(inflater, parent, false);
+
+        result.setListener(mViewListener);
+        return result;
+    }
+
+    private FooterView.Listener mViewListener = new FooterView.Listener() {
+        public void onAddClicked() {
+            if (mListener != null) mListener.onAddClicked(getRawContact());
+        }
+
+        public void onSeparateClicked() {
+            if (mListener != null) mListener.onAddClicked(getRawContact());
+        }
+
+        public void onDeleteClicked() {
+            if (mListener != null) mListener.onAddClicked(getRawContact());
+        }
+    };
+
+    public interface Listener {
+        public void onAddClicked(DisplayRawContact rawContact);
+        public void onSeparateClicked(DisplayRawContact rawContact);
+        public void onDeleteClicked(DisplayRawContact rawContact);
+    }
+}
diff --git a/src/com/android/contacts/views/editor/viewModel/HeaderViewModel.java b/src/com/android/contacts/views/editor/viewModel/HeaderViewModel.java
new file mode 100644
index 0000000..dca3a06
--- /dev/null
+++ b/src/com/android/contacts/views/editor/viewModel/HeaderViewModel.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.views.editor.viewModel;
+
+import com.android.contacts.R;
+import com.android.contacts.views.editor.DisplayRawContact;
+import com.android.contacts.views.editor.view.HeaderView;
+import com.android.contacts.views.editor.view.ViewTypes;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+public class HeaderViewModel extends BaseViewModel {
+    private boolean mCollapsed;
+
+    public HeaderViewModel(Context context, DisplayRawContact rawContact) {
+        super(context, rawContact);
+    }
+
+    public boolean isCollapsed() {
+        return mCollapsed;
+    }
+
+    public void setCollapsed(boolean collapsed) {
+        mCollapsed = collapsed;
+    }
+
+    @Override
+    public int getEntryType() {
+        return ViewTypes.RAW_CONTACT_HEADER;
+    }
+
+    @Override
+    public View getView(LayoutInflater inflater, View convertView, ViewGroup parent) {
+        final HeaderView result = convertView != null
+                ? (HeaderView) convertView
+                : HeaderView.inflate(inflater, parent, false);
+
+        CharSequence accountType = getRawContact().getSource().getDisplayLabel(getContext());
+        if (TextUtils.isEmpty(accountType)) {
+            accountType = getContext().getString(R.string.account_phone);
+        }
+        final String accountName = getRawContact().getAccountName();
+
+        final String accountTypeDisplay;
+        if (TextUtils.isEmpty(accountName)) {
+            accountTypeDisplay = getContext().getString(R.string.account_type_format,
+                    accountType);
+        } else {
+            accountTypeDisplay = getContext().getString(R.string.account_type_and_name,
+                    accountType, accountName);
+        }
+
+        result.setCaptionText(accountTypeDisplay);
+        result.setLogo(getRawContact().getSource().getDisplayIcon(getContext()));
+
+        return result;
+    }
+}
diff --git a/src/com/android/contacts/views/editor/viewModel/PhoneViewModel.java b/src/com/android/contacts/views/editor/viewModel/PhoneViewModel.java
new file mode 100644
index 0000000..496bf2a
--- /dev/null
+++ b/src/com/android/contacts/views/editor/viewModel/PhoneViewModel.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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.views.editor.viewModel;
+
+import com.android.contacts.views.editor.DisplayRawContact;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+
+public class PhoneViewModel extends FieldAndTypeViewModel {
+    private PhoneViewModel(Context context, DisplayRawContact rawContact, long dataId,
+            ContentValues contentValues, int titleResId) {
+        super(context, rawContact, dataId, contentValues, titleResId, Phone.NUMBER, Phone.TYPE,
+                Phone.LABEL);
+    }
+
+    public static PhoneViewModel createForExisting(Context context, DisplayRawContact rawContact,
+            long dataId, ContentValues contentValues, int titleResId) {
+        return new PhoneViewModel(context, rawContact, dataId, contentValues, titleResId);
+    }
+
+    @Override
+    protected CharSequence getTypeDisplayLabel() {
+        return Phone.getTypeLabel(getContext().getResources(), getType(), getLabel());
+    }
+}