Initial move of dialer features from contacts app.

Bug: 6993891
Change-Id: I758ce359ca7e87a1d184303822979318be171921
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..ce67d75
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,40 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+# This should become ContactsCommon
+contacts_common_dir := ../Contacts
+
+src_dirs := src $(contacts_common_dir)/src
+res_dirs := res $(contacts_common_dir)/res
+
+LOCAL_SRC_FILES := $(call all-java-files-under, $(src_dirs))
+LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs))
+
+LOCAL_AAPT_FLAGS := \
+    --auto-add-overlay \
+    --extra-packages com.android.contacts
+
+LOCAL_JAVA_LIBRARIES := telephony-common
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    com.android.phone.common \
+    com.android.phone.shared \
+    com.android.vcard \
+    android-common \
+    guava \
+    android-support-v13 \
+    android-support-v4 \
+    android-ex-variablespeed \
+
+LOCAL_REQUIRED_MODULES := libvariablespeed
+
+LOCAL_PACKAGE_NAME := Dialer
+LOCAL_CERTIFICATE := shared
+
+LOCAL_PROGUARD_FLAG_FILES := proguard.flags
+
+include $(BUILD_PACKAGE)
+
+# Use the folloing include to make our test apk.
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644
index 0000000..5e0e63f
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,249 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2006 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.dialer"
+    android:sharedUserId="android.uid.shared">
+
+    <uses-permission android:name="android.permission.CALL_PRIVILEGED" />
+    <uses-permission android:name="android.permission.READ_CONTACTS" />
+    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
+    <uses-permission android:name="android.permission.READ_CALL_LOG" />
+    <uses-permission android:name="android.permission.WRITE_CALL_LOG" />
+    <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
+    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+    <uses-permission android:name="android.permission.READ_PROFILE" />
+    <uses-permission android:name="android.permission.WRITE_PROFILE" />
+    <uses-permission android:name="android.permission.READ_SOCIAL_STREAM" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.NFC" />
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
+    <uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
+    <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH.mail" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+    <uses-permission android:name="android.permission.USE_CREDENTIALS" />
+    <uses-permission android:name="android.permission.VIBRATE" />
+    <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
+    <uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL" />
+    <uses-permission android:name="com.android.voicemail.permission.READ_WRITE_ALL_VOICEMAIL" />
+    <uses-permission android:name="android.permission.ALLOW_ANY_CODEC_FOR_PLAYBACK" />
+    <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
+    <!-- allow broadcasting secret code intents that reboot the phone -->
+    <uses-permission android:name="android.permission.REBOOT" />
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+
+    <application
+        android:label="@string/applicationLabel"
+        android:icon="@mipmap/ic_launcher_contacts"
+        android:taskAffinity="android.task.contacts"
+        android:hardwareAccelerated="true"
+    >
+
+        <!-- Intercept Dialer Intents for devices without a phone.
+        This activity should have the same intent filters as the DialtactsActivity,
+        so that its capturing the same events. Omit android.intent.category.LAUNCHER, because we
+        don't want this to show up in the Launcher. The priorities of the intent-filters
+        are set lower, so that the user does not see a disambig dialog -->
+        <activity
+            android:name=".NonPhoneActivity"
+            android:theme="@style/NonPhoneActivityTheme"
+        >
+            <intent-filter android:priority="-1">
+                <action android:name="android.intent.action.DIAL" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:mimeType="vnd.android.cursor.item/phone" />
+                <data android:mimeType="vnd.android.cursor.item/person" />
+            </intent-filter>
+            <intent-filter android:priority="-1">
+                <action android:name="android.intent.action.DIAL" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="voicemail" />
+            </intent-filter>
+            <intent-filter android:priority="-1">
+                <action android:name="android.intent.action.DIAL" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <intent-filter android:priority="-1">
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+            </intent-filter>
+            <intent-filter android:priority="-1">
+                <action android:name="android.intent.action.VIEW" />
+                <action android:name="android.intent.action.DIAL" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="tel" />
+            </intent-filter>
+            <intent-filter android:priority="-1">
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:mimeType="vnd.android.cursor.dir/calls" />
+            </intent-filter>
+            <intent-filter android:priority="-1">
+                <action android:name="android.intent.action.CALL_BUTTON" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+            </intent-filter>
+        </activity>
+
+        <!-- The entrance point for Phone UI.
+             stateAlwaysHidden is set to suppress keyboard show up on
+             dialpad screen. -->
+        <activity android:name=".DialtactsActivity"
+            android:label="@string/launcherDialer"
+            android:theme="@style/DialtactsTheme"
+            android:uiOptions="splitActionBarWhenNarrow"
+            android:launchMode="singleTask"
+            android:clearTaskOnLaunch="true"
+            android:icon="@mipmap/ic_launcher_phone"
+            android:screenOrientation="nosensor"
+            android:enabled="@*android:bool/config_voice_capable"
+            android:taskAffinity="android.task.contacts.phone"
+            android:windowSoftInputMode="stateAlwaysHidden|adjustNothing">
+            <intent-filter>
+                <action android:name="android.intent.action.DIAL" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:mimeType="vnd.android.cursor.item/phone" />
+                <data android:mimeType="vnd.android.cursor.item/person" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.DIAL" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="voicemail" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.DIAL" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.LAUNCHER" />
+                <category android:name="android.intent.category.BROWSABLE" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <action android:name="android.intent.action.DIAL" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="tel" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:mimeType="vnd.android.cursor.dir/calls" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.CALL_BUTTON" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+            </intent-filter>
+            <!-- This was never intended to be public, but is here for backward
+                 compatibility.  Use Intent.ACTION_DIAL instead. -->
+            <intent-filter>
+                <action android:name="com.android.phone.action.TOUCH_DIALER" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.TAB" />
+            </intent-filter>
+            <intent-filter android:label="@string/recentCallsIconLabel">
+                <action android:name="com.android.phone.action.RECENT_CALLS" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.TAB" />
+            </intent-filter>
+        </activity>
+
+        <activity android:name="com.android.dialer.CallDetailActivity"
+                  android:label="@string/callDetailTitle"
+                  android:theme="@style/CallDetailActivityTheme"
+                  android:screenOrientation="portrait"
+                  android:icon="@mipmap/ic_launcher_phone"
+                  android:taskAffinity="android.task.contacts.phone"
+            >
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:mimeType="vnd.android.cursor.item/calls"/>
+            </intent-filter>
+        </activity>
+
+        <activity android:name="com.android.contacts.common.test.FragmentTestActivity">
+            <intent-filter>
+                <category android:name="android.intent.category.TEST"/>
+            </intent-filter>
+        </activity>
+
+        <!-- Backwards compatibility: "Phone" from Gingerbread and earlier -->
+        <activity-alias android:name="DialtactsActivity"
+            android:targetActivity=".DialtactsActivity"
+            android:exported="true"
+        />
+
+        <!-- Backwards compatibility: "Call log" from Gingerbread and earlier -->
+        <activity-alias android:name="RecentCallsListActivity"
+            android:targetActivity=".DialtactsActivity"
+            android:exported="true"
+        />
+
+        <!-- Backwards compatibility: "Call log" from ICS -->
+        <activity-alias android:name=".activities.CallLogActivity"
+            android:targetActivity=".DialtactsActivity"
+            android:exported="true"
+        />
+
+        <receiver android:name=".calllog.CallLogReceiver"
+            android:enabled="@*android:bool/config_voice_capable">
+            <intent-filter>
+                <action android:name="android.intent.action.NEW_VOICEMAIL" />
+                <data
+                    android:scheme="content"
+                    android:host="com.android.voicemail"
+                    android:mimeType="vnd.android.cursor.item/voicemail"
+                />
+            </intent-filter>
+            <intent-filter android:priority="100">
+                 <action android:name="android.intent.action.BOOT_COMPLETED"/>
+            </intent-filter>
+        </receiver>
+
+        <service
+            android:name=".calllog.CallLogNotificationsService"
+            android:exported="false"
+        />
+
+        <!-- Service that is exclusively for the Phone application that sends out a view
+             notification. This service might be removed in future versions of the app  -->
+        <service android:name=".ViewNotificationService"
+            android:permission="android.permission.WRITE_CONTACTS"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="com.android.contacts.VIEW_NOTIFICATION" />
+                <data android:mimeType="vnd.android.cursor.item/contact" />
+            </intent-filter>
+        </service>
+    </application>
+</manifest>
diff --git a/proguard.flags b/proguard.flags
new file mode 100644
index 0000000..9e9ed64
--- /dev/null
+++ b/proguard.flags
@@ -0,0 +1,16 @@
+# Xml files containing onClick (menus and layouts) require that proguard not
+# remove their handlers.
+-keepclassmembers class * extends android.app.Activity {
+  public void *(android.view.View);
+  public void *(android.view.MenuItem);
+}
+
+# Any class or method annotated with NeededForTesting or NeededForReflection.
+-keep @com.android.contacts.test.NeededForTesting class *
+-keep @com.android.contacts.test.NeededForReflection class *
+-keepclassmembers class * {
+@com.android.contacts.test.NeededForTesting *;
+@com.android.contacts.test.NeededForReflection *;
+}
+
+-verbose
diff --git a/res/layout-land/dialpad_fragment.xml b/res/layout-land/dialpad_fragment.xml
new file mode 100644
index 0000000..d1cf3a4
--- /dev/null
+++ b/res/layout-land/dialpad_fragment.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/top"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" >
+
+    <LinearLayout
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="3"
+        android:orientation="vertical" >
+
+        <LinearLayout
+            android:id="@+id/digits_container"
+            android:layout_width="match_parent"
+            android:layout_height="0px"
+            android:layout_weight="@integer/dialpad_layout_weight_digits"
+            android:layout_marginTop="@dimen/dialpad_vertical_margin"
+            android:background="@drawable/dialpad_background"
+            android:gravity="center">
+
+            <com.android.dialer.dialpad.DigitsEditText
+                android:id="@+id/digits"
+                android:layout_width="0dip"
+                android:layout_weight="1"
+                android:layout_height="match_parent"
+                android:gravity="center"
+                android:textAppearance="@style/DialtactsDigitsTextAppearance"
+                android:textColor="?android:attr/textColorPrimary"
+                android:nextFocusRight="@+id/overflow_menu"
+                android:background="@android:color/transparent" />
+
+            <ImageButton
+                android:id="@+id/deleteButton"
+                android:layout_width="56dip"
+                android:layout_height="match_parent"
+                android:layout_gravity="center_vertical"
+                android:gravity="center"
+                android:state_enabled="false"
+                android:background="?android:attr/selectableItemBackground"
+                android:contentDescription="@string/description_delete_button"
+                android:src="@drawable/ic_dial_action_delete" />
+
+
+        </LinearLayout>
+        <!-- "Dialpad chooser" UI, shown only when the user brings up the
+         Dialer while a call is already in progress.
+         When this UI is visible, the other Dialer elements
+         (the textfield and button) are hidden. -->
+        <ListView android:id="@+id/dialpadChooser"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:footerDividersEnabled="true" />
+
+        <!-- Keypad section -->
+        <include layout="@layout/dialpad" />
+    </LinearLayout>
+    <View
+       android:layout_width="@dimen/dialpad_center_margin"
+       android:layout_height="match_parent"
+       android:background="#66000000"/>
+    <RelativeLayout
+        android:id="@+id/dialButtonContainer"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_weight="2"
+        android:background="@drawable/dialpad_background">
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/dialpad_button_margin"
+            android:layout_above="@id/dialButton"
+            android:background="#33000000" />
+        <ImageButton android:id="@+id/dialButton"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/call_button_height"
+            android:layout_alignParentBottom="true"
+            android:state_enabled="false"
+            android:background="@drawable/btn_call"
+            android:contentDescription="@string/description_dial_button"
+            android:src="@drawable/ic_dial_action_call" />
+    </RelativeLayout>
+</LinearLayout>
diff --git a/res/layout-land/dialtacts_activity.xml b/res/layout-land/dialtacts_activity.xml
new file mode 100644
index 0000000..f43fe5f
--- /dev/null
+++ b/res/layout-land/dialtacts_activity.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_marginTop="?android:attr/actionBarSize"
+    android:id="@+id/dialtacts_frame"
+    >
+    <android.support.v4.view.ViewPager
+        android:id="@+id/pager"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+</FrameLayout>
diff --git a/res/layout/call_detail.xml b/res/layout/call_detail.xml
new file mode 100644
index 0000000..8f38a19
--- /dev/null
+++ b/res/layout/call_detail.xml
@@ -0,0 +1,218 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:ex="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/call_detail"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:visibility="gone"
+    android:background="@android:color/black"
+>
+    <!--
+      The list view is under everything.
+      It contains a first header element which is hidden under the controls UI.
+      When scrolling, the controls move up until the name bar hits the top.
+      -->
+    <ListView
+        android:id="@+id/history"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentLeft="true"
+        android:layout_alignParentTop="true"
+    />
+
+    <!-- All the controls which are part of the pinned header are in this layout. -->
+    <RelativeLayout
+        android:id="@+id/controls"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_alignParentLeft="true"
+        android:layout_alignParentTop="true"
+    >
+        <FrameLayout
+            android:id="@+id/voicemail_status"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_alignParentLeft="true"
+            android:layout_alignParentTop="true"
+            android:visibility="gone"
+        >
+            <include layout="@layout/call_log_voicemail_status"/>
+        </FrameLayout>
+
+        <view
+            class="com.android.contacts.widget.ProportionalLayout"
+            android:id="@+id/contact_background_sizer"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_alignParentLeft="true"
+            android:layout_below="@id/voicemail_status"
+            ex:ratio="0.5"
+            ex:direction="widthToHeight"
+        >
+            <ImageView
+                android:id="@+id/contact_background"
+                android:layout_width="match_parent"
+                android:layout_height="0dip"
+                android:adjustViewBounds="true"
+                android:scaleType="centerCrop"
+            />
+        </view>
+        <LinearLayout
+            android:id="@+id/blue_separator"
+            android:layout_width="match_parent"
+            android:layout_height="1dip"
+            android:background="@android:color/holo_blue_light"
+            android:layout_below="@+id/contact_background_sizer"
+        />
+        <View
+            android:id="@+id/photo_text_bar"
+            android:layout_width="match_parent"
+            android:layout_height="42dip"
+            android:background="#7F000000"
+            android:layout_alignParentLeft="true"
+            android:layout_alignBottom="@id/contact_background_sizer"
+        />
+        <ImageView
+            android:id="@+id/main_action"
+            android:layout_width="wrap_content"
+            android:layout_height="0dip"
+            android:scaleType="center"
+            android:layout_alignRight="@id/photo_text_bar"
+            android:layout_alignBottom="@id/photo_text_bar"
+            android:layout_alignTop="@id/photo_text_bar"
+            android:layout_marginRight="@dimen/call_log_outer_margin"
+        />
+        <TextView
+            android:id="@+id/header_text"
+            android:layout_width="wrap_content"
+            android:layout_height="0dip"
+            android:layout_alignLeft="@id/photo_text_bar"
+            android:layout_toLeftOf="@id/main_action"
+            android:layout_alignTop="@id/photo_text_bar"
+            android:layout_alignBottom="@id/photo_text_bar"
+            android:layout_marginRight="@dimen/call_log_inner_margin"
+            android:layout_marginLeft="@dimen/call_detail_contact_name_margin"
+            android:gravity="center_vertical"
+            android:textColor="?attr/call_log_primary_text_color"
+            android:textAppearance="?android:attr/textAppearanceMedium"
+            android:singleLine="true"
+        />
+        <ImageButton
+            android:id="@+id/main_action_push_layer"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_alignLeft="@id/contact_background_sizer"
+            android:layout_alignTop="@id/contact_background_sizer"
+            android:layout_alignRight="@id/contact_background_sizer"
+            android:layout_alignBottom="@id/contact_background_sizer"
+            android:background="?android:attr/selectableItemBackground"
+        />
+        <LinearLayout
+            android:id="@+id/voicemail_container"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:paddingBottom="@dimen/call_detail_button_spacing"
+            android:layout_below="@id/blue_separator"
+        >
+            <!-- The voicemail fragment will be put here. -->
+        </LinearLayout>
+        <FrameLayout
+            android:id="@+id/call_and_sms"
+            android:layout_width="match_parent"
+            android:layout_height="@dimen/call_log_list_item_height"
+            android:layout_marginBottom="@dimen/call_detail_button_spacing"
+            android:layout_below="@id/voicemail_container"
+            android:gravity="center_vertical"
+            android:background="@drawable/dialpad_background"
+        >
+            <LinearLayout
+                android:id="@+id/call_and_sms_main_action"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:orientation="horizontal"
+                android:focusable="true"
+                android:background="?android:attr/selectableItemBackground"
+                >
+
+                <LinearLayout
+                    android:layout_width="0dip"
+                    android:layout_height="match_parent"
+                    android:layout_weight="1"
+                    android:paddingLeft="@dimen/call_log_indent_margin"
+                    android:orientation="vertical"
+                    android:gravity="center_vertical"
+                >
+
+                    <TextView android:id="@+id/call_and_sms_text"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:paddingRight="@dimen/call_log_icon_margin"
+                        android:textAppearance="?android:attr/textAppearanceMedium"
+                        android:textColor="?attr/call_log_primary_text_color"
+                        android:singleLine="true"
+                        android:ellipsize="end"
+                    />
+
+                    <TextView android:id="@+id/call_and_sms_label"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:paddingRight="@dimen/call_log_icon_margin"
+                        android:textAppearance="?android:attr/textAppearanceSmall"
+                        android:textColor="?attr/call_log_primary_text_color"
+                        android:textAllCaps="true"
+                        android:singleLine="true"
+                        android:ellipsize="end"
+                    />
+                </LinearLayout>
+
+                <View android:id="@+id/call_and_sms_divider"
+                    android:layout_width="1px"
+                    android:layout_height="32dip"
+                    android:background="@drawable/ic_divider_dashed_holo_dark"
+                    android:layout_gravity="center_vertical"
+                />
+
+                <ImageView android:id="@+id/call_and_sms_icon"
+                    android:layout_width="@color/call_log_voicemail_highlight_color"
+                    android:layout_height="match_parent"
+                    android:paddingLeft="@dimen/call_log_inner_margin"
+                    android:paddingRight="@dimen/call_log_outer_margin"
+                    android:gravity="center"
+                    android:scaleType="centerInside"
+                    android:focusable="true"
+                    android:background="?android:attr/selectableItemBackground"
+                />
+            </LinearLayout>
+        </FrameLayout>
+    </RelativeLayout>
+
+    <!--
+         Used to hide the UI when playing a voicemail and the proximity sensor
+         is detecting something near the screen.
+      -->
+    <View
+        android:id="@+id/blank"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_alignParentLeft="true"
+        android:layout_alignParentTop="true"
+        android:background="@android:color/black"
+        android:visibility="gone"
+        android:clickable="true"
+    />
+</RelativeLayout>
diff --git a/res/layout/call_detail_history_header.xml b/res/layout/call_detail_history_header.xml
new file mode 100644
index 0000000..09047c5
--- /dev/null
+++ b/res/layout/call_detail_history_header.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<!-- This layout is supposed to match the content of the controls in call_detail.xml  -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:ex="http://schemas.android.com/apk/res-auto"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <!-- Contact photo. -->
+    <view
+        class="com.android.contacts.widget.ProportionalLayout"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentLeft="true"
+        android:layout_below="@id/voicemail_status"
+        ex:ratio="0.5"
+        ex:direction="widthToHeight"
+    >
+        <!-- Proportional layout requires a view in it. -->
+        <View
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+        />
+    </view>
+    <!-- Separator line -->
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1dip"
+    />
+    <!-- Voicemail controls -->
+    <!-- TODO: Make the height be based on a constant. -->
+    <View
+        android:id="@+id/header_voicemail_container"
+        android:layout_width="match_parent"
+        android:layout_height="140dip"
+        android:layout_marginBottom="@dimen/call_detail_button_spacing"
+    />
+    <!-- Call and SMS -->
+    <View
+        android:id="@+id/header_call_and_sms_container"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/call_log_list_item_height"
+    />
+
+</LinearLayout>
diff --git a/res/layout/call_detail_history_item.xml b/res/layout/call_detail_history_item.xml
new file mode 100644
index 0000000..01b9517
--- /dev/null
+++ b/res/layout/call_detail_history_item.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="@dimen/call_log_list_item_height"
+    android:paddingTop="@dimen/call_log_inner_margin"
+    android:paddingBottom="@dimen/call_log_inner_margin"
+    android:paddingLeft="@dimen/call_log_indent_margin"
+    android:paddingRight="@dimen/call_log_outer_margin"
+    android:orientation="vertical"
+>
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+    >
+        <view
+            class="com.android.dialer.calllog.CallTypeIconsView"
+            android:id="@+id/call_type_icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+        />
+        <TextView
+            android:id="@+id/call_type_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="@dimen/call_log_icon_margin"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:textColor="@color/secondary_text_color"
+        />
+    </LinearLayout>
+    <TextView
+        android:id="@+id/date"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:textColor="@color/secondary_text_color"
+    />
+    <TextView
+        android:id="@+id/duration"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:textColor="@color/secondary_text_color"
+    />
+</LinearLayout>
diff --git a/res/layout/call_log_fragment.xml b/res/layout/call_log_fragment.xml
new file mode 100644
index 0000000..34b4b7f
--- /dev/null
+++ b/res/layout/call_log_fragment.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<!-- Layout parameters are set programmatically. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    style="@style/FragmentActionBarPadding"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:divider="?android:attr/dividerHorizontal"
+    android:showDividers="end">
+
+    <FrameLayout
+        android:id="@+id/voicemail_status"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:visibility="gone">
+        <include layout="@layout/call_log_voicemail_status"
+    />
+    </FrameLayout>
+
+    <FrameLayout>
+        <TextView
+            android:id="@+id/filter_status"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:paddingLeft="@dimen/call_log_outer_margin"
+            android:paddingRight="@dimen/call_log_outer_margin"
+            android:paddingTop="@dimen/call_log_inner_margin"
+            android:paddingBottom="@dimen/call_log_inner_margin"
+            android:layout_alignParentLeft="true"
+            android:layout_alignParentBottom="true"
+            android:visibility="gone"
+            />
+        <View
+            android:id="@+id/call_log_divider"
+            android:layout_width="match_parent"
+            android:layout_height="1px"
+            android:layout_marginLeft="@dimen/call_log_outer_margin"
+            android:layout_marginRight="@dimen/call_log_outer_margin"
+            android:layout_gravity="bottom"
+            android:background="#55ffffff"
+            />
+    </FrameLayout>
+    <FrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+        <ListView android:id="@android:id/list"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:fadingEdge="none"
+            android:scrollbarStyle="outsideOverlay"
+            android:divider="@null"
+        />
+        <TextView android:id="@android:id/empty"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:text="@string/recentCalls_empty"
+            android:gravity="center"
+            android:layout_marginTop="@dimen/empty_message_top_margin"
+            android:textColor="?android:attr/textColorSecondary"
+            android:textAppearance="?android:attr/textAppearanceLarge"
+        />
+    </FrameLayout>
+</LinearLayout>
diff --git a/res/layout/call_log_list_item.xml b/res/layout/call_log_list_item.xml
new file mode 100644
index 0000000..8564c0d
--- /dev/null
+++ b/res/layout/call_log_list_item.xml
@@ -0,0 +1,167 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<view
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    class="com.android.dialer.calllog.CallLogListItemView"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+>
+    <!--
+        This layout may represent either a call log item or one of the
+        headers in the call log.
+
+        The former will make the @id/call_log_item visible and the
+        @id/call_log_header gone.
+
+        The latter will make the @id/call_log_header visible and the
+        @id/call_log_item gone
+    -->
+
+        <LinearLayout
+            android:id="@+id/primary_action_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_centerVertical="true"
+            android:layout_marginLeft="@dimen/call_log_outer_margin"
+            android:layout_marginRight="@dimen/call_log_outer_margin"
+            android:orientation="horizontal"
+            android:gravity="center_vertical"
+            android:background="?android:attr/selectableItemBackground"
+            android:focusable="true"
+            android:nextFocusRight="@+id/secondary_action_icon"
+            android:nextFocusLeft="@+id/quick_contact_photo"
+        >
+            <QuickContactBadge
+                android:id="@+id/quick_contact_photo"
+                android:layout_width="@dimen/call_log_list_contact_photo_size"
+                android:layout_height="@dimen/call_log_list_contact_photo_size"
+                android:nextFocusRight="@id/primary_action_view"
+                android:layout_alignParentLeft="true"
+                android:layout_centerVertical="true"
+                android:focusable="true"
+            />
+            <LinearLayout
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:paddingTop="@dimen/call_log_inner_margin"
+                android:paddingBottom="@dimen/call_log_inner_margin"
+                android:orientation="vertical"
+                android:gravity="center_vertical"
+                android:layout_marginLeft="@dimen/call_log_inner_margin"
+            >
+                <TextView
+                    android:id="@+id/name"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginRight="@dimen/call_log_icon_margin"
+                    android:textColor="?attr/call_log_primary_text_color"
+                    android:textSize="18sp"
+                    android:singleLine="true"
+                />
+                <LinearLayout
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal"
+                >
+                    <TextView
+                        android:id="@+id/number"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginRight="@dimen/call_log_icon_margin"
+                        android:textColor="?attr/call_log_secondary_text_color"
+                        android:textSize="14sp"
+                        android:singleLine="true"
+                        android:ellipsize="marquee"
+                        />
+                    <TextView
+                        android:id="@+id/label"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginRight="@dimen/call_log_icon_margin"
+                        android:textColor="?attr/call_log_secondary_text_color"
+                        android:textStyle="bold"
+                        android:textSize="14sp"
+                        android:singleLine="true"
+                        android:ellipsize="marquee"
+                        />
+                    </LinearLayout>
+                <LinearLayout
+                    android:id="@+id/call_type"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal"
+                >
+                    <view
+                        class="com.android.dialer.calllog.CallTypeIconsView"
+                        android:id="@+id/call_type_icons"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginRight="@dimen/call_log_icon_margin"
+                        android:layout_gravity="center_vertical"
+                    />
+                    <TextView
+                        android:id="@+id/call_count_and_date"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_marginRight="@dimen/call_log_icon_margin"
+                        android:layout_gravity="center_vertical"
+                        android:textColor="?attr/call_log_secondary_text_color"
+                        android:textSize="14sp"
+                        android:singleLine="true"
+                    />
+                </LinearLayout>
+            </LinearLayout>
+            <View
+                android:id="@+id/divider"
+                android:layout_width="1px"
+                android:layout_height="@dimen/call_log_call_action_size"
+                android:background="@drawable/ic_divider_dashed_holo_dark"
+                android:layout_gravity="center_vertical"
+            />
+            <ImageButton
+                android:id="@+id/secondary_action_icon"
+                android:layout_width="@dimen/call_log_call_action_width"
+                android:layout_height="match_parent"
+                android:paddingLeft="@dimen/call_log_inner_margin"
+                android:paddingTop="@dimen/call_log_inner_margin"
+                android:paddingBottom="@dimen/call_log_inner_margin"
+                android:paddingRight="@dimen/call_log_inner_margin"
+                android:scaleType="center"
+                android:background="?android:attr/selectableItemBackground"
+                android:nextFocusLeft="@id/primary_action_view"
+            />
+        </LinearLayout>
+
+    <TextView
+        android:id="@+id/call_log_header"
+        style="@style/ContactListSeparatorTextViewStyle"
+        android:layout_marginLeft="@dimen/call_log_outer_margin"
+        android:layout_marginRight="@dimen/call_log_outer_margin"
+        android:paddingTop="@dimen/call_log_inner_margin"
+        android:paddingBottom="@dimen/call_log_inner_margin" />
+
+    <View
+        android:id="@+id/call_log_divider"
+        android:layout_width="match_parent"
+        android:layout_height="1px"
+        android:layout_marginLeft="@dimen/call_log_outer_margin"
+        android:layout_marginRight="@dimen/call_log_outer_margin"
+        android:background="#55ffffff"
+    />
+</view>
diff --git a/res/layout/call_log_voicemail_status.xml b/res/layout/call_log_voicemail_status.xml
new file mode 100644
index 0000000..191c821
--- /dev/null
+++ b/res/layout/call_log_voicemail_status.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="?attr/call_log_voicemail_status_height"
+        android:background="?attr/call_log_voicemail_status_background_color"
+    >
+        <TextView
+            android:id="@+id/voicemail_status_message"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:layout_weight="1"
+            android:paddingLeft="@dimen/call_log_outer_margin"
+            android:paddingRight="@dimen/call_log_inner_margin"
+            android:textAppearance="?android:attr/textAppearanceMedium"
+            android:textColor="?attr/call_log_voicemail_status_text_color"
+        />
+        <TextView
+            android:id="@+id/voicemail_status_action"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:gravity="center_vertical"
+            android:paddingLeft="@dimen/call_log_inner_margin"
+            android:paddingRight="@dimen/call_log_outer_margin"
+            android:textAppearance="?android:attr/textAppearanceMedium"
+            android:textColor="?attr/call_log_voicemail_status_action_text_color"
+            android:background="?android:attr/selectableItemBackground"
+            android:clickable="true"
+        />
+    </LinearLayout>
+</merge>
diff --git a/res/layout/dialpad.xml b/res/layout/dialpad.xml
new file mode 100644
index 0000000..3ccb42d
--- /dev/null
+++ b/res/layout/dialpad.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2006 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.
+-->
+
+<!-- Dialpad in the Phone app. -->
+<TableLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/dialpad"
+    android:layout_width="match_parent"
+    android:layout_height="0px"
+    android:layout_weight="@integer/dialpad_layout_weight_dialpad"
+    android:layout_gravity="center_horizontal"
+    android:layout_marginTop="@dimen/dialpad_vertical_margin"
+    android:paddingLeft="5dip"
+    android:paddingRight="5dip"
+    android:paddingBottom="10dip"
+    android:background="@drawable/dialpad_background">
+
+    <TableRow
+         android:layout_height="0px"
+         android:layout_weight="1">
+        <com.android.dialer.dialpad.DialpadImageButton
+            android:id="@+id/one" style="@style/DialtactsDialpadButtonStyle"
+            android:src="@drawable/dial_num_1_wht"
+            android:contentDescription="@string/description_image_button_one" />
+        <com.android.dialer.dialpad.DialpadImageButton
+            android:id="@+id/two" style="@style/DialtactsDialpadButtonStyle"
+            android:src="@drawable/dial_num_2_wht"
+            android:contentDescription="@string/description_image_button_two" />
+        <com.android.dialer.dialpad.DialpadImageButton
+            android:id="@+id/three" style="@style/DialtactsDialpadButtonStyle"
+            android:src="@drawable/dial_num_3_wht"
+            android:contentDescription="@string/description_image_button_three" />
+    </TableRow>
+
+    <TableRow
+         android:layout_height="0px"
+         android:layout_weight="1">
+        <com.android.dialer.dialpad.DialpadImageButton
+            android:id="@+id/four" style="@style/DialtactsDialpadButtonStyle"
+            android:src="@drawable/dial_num_4_wht"
+            android:contentDescription="@string/description_image_button_four" />
+        <com.android.dialer.dialpad.DialpadImageButton
+            android:id="@+id/five" style="@style/DialtactsDialpadButtonStyle"
+            android:src="@drawable/dial_num_5_wht"
+            android:contentDescription="@string/description_image_button_five" />
+        <com.android.dialer.dialpad.DialpadImageButton
+            android:id="@+id/six" style="@style/DialtactsDialpadButtonStyle"
+            android:src="@drawable/dial_num_6_wht"
+            android:contentDescription="@string/description_image_button_six" />
+    </TableRow>
+
+    <TableRow
+         android:layout_height="0px"
+         android:layout_weight="1">
+        <com.android.dialer.dialpad.DialpadImageButton
+            android:id="@+id/seven" style="@style/DialtactsDialpadButtonStyle"
+            android:src="@drawable/dial_num_7_wht"
+            android:contentDescription="@string/description_image_button_seven" />
+        <com.android.dialer.dialpad.DialpadImageButton
+            android:id="@+id/eight" style="@style/DialtactsDialpadButtonStyle"
+            android:src="@drawable/dial_num_8_wht"
+            android:contentDescription="@string/description_image_button_eight" />
+        <com.android.dialer.dialpad.DialpadImageButton
+            android:id="@+id/nine" style="@style/DialtactsDialpadButtonStyle"
+            android:src="@drawable/dial_num_9_wht"
+            android:contentDescription="@string/description_image_button_nine" />
+    </TableRow>
+
+    <TableRow
+         android:layout_height="0px"
+         android:layout_weight="1">
+        <com.android.dialer.dialpad.DialpadImageButton
+            android:id="@+id/star" style="@style/DialtactsDialpadButtonStyle"
+            android:src="@drawable/dial_num_star_wht"
+            android:contentDescription="@string/description_image_button_star" />
+        <com.android.dialer.dialpad.DialpadImageButton
+            android:id="@+id/zero" style="@style/DialtactsDialpadButtonStyle"
+            android:src="@drawable/dial_num_0_wht"
+            android:contentDescription="@string/description_image_button_zero" />
+        <com.android.dialer.dialpad.DialpadImageButton
+            android:id="@+id/pound" style="@style/DialtactsDialpadButtonStyle"
+            android:src="@drawable/dial_num_pound_wht"
+            android:contentDescription="@string/description_image_button_pound" />
+    </TableRow>
+</TableLayout>
diff --git a/res/layout/dialpad_chooser_list_item.xml b/res/layout/dialpad_chooser_list_item.xml
new file mode 100644
index 0000000..853ca47
--- /dev/null
+++ b/res/layout/dialpad_chooser_list_item.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+  
+          http://www.apache.org/licenses/LICENSE-2.0
+  
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<!-- Layout of a single item in the Dialer's "Dialpad chooser" UI. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="horizontal"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <ImageView android:id="@+id/icon"
+        android:layout_width="64dp"
+        android:layout_height="64dp"
+        android:scaleType="center" />
+
+    <TextView android:id="@+id/text"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:layout_gravity="center_vertical"
+        android:layout_width="0dip"
+        android:layout_weight="1"
+        android:layout_height="wrap_content" />
+
+</LinearLayout>
diff --git a/res/layout/dialpad_fragment.xml b/res/layout/dialpad_fragment.xml
new file mode 100644
index 0000000..6423638
--- /dev/null
+++ b/res/layout/dialpad_fragment.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/top"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:paddingLeft="@dimen/dialpad_horizontal_margin"
+    android:paddingRight="@dimen/dialpad_horizontal_margin">
+
+    <!-- Text field and possibly soft menu button above the keypad where
+         the digits are displayed. -->
+    <LinearLayout
+        android:id="@+id/digits_container"
+        android:layout_width="match_parent"
+        android:layout_height="0px"
+        android:layout_weight="@integer/dialpad_layout_weight_digits"
+        android:layout_marginTop="@dimen/dialpad_vertical_margin"
+        android:gravity="center"
+        android:background="@drawable/dialpad_background" >
+
+        <com.android.dialer.dialpad.DigitsEditText
+            android:id="@+id/digits"
+            android:layout_width="0dip"
+            android:layout_weight="1"
+            android:layout_height="match_parent"
+            android:gravity="center"
+            android:textAppearance="@style/DialtactsDigitsTextAppearance"
+            android:textColor="?android:attr/textColorPrimary"
+            android:nextFocusRight="@+id/overflow_menu"
+            android:background="@android:color/transparent" />
+
+        <ImageButton
+            android:id="@+id/deleteButton"
+            android:layout_width="56dip"
+            android:layout_height="match_parent"
+            android:layout_gravity="center_vertical"
+            android:gravity="center"
+            android:state_enabled="false"
+            android:background="?android:attr/selectableItemBackground"
+            android:contentDescription="@string/description_delete_button"
+            android:src="@drawable/ic_dial_action_delete" />
+    </LinearLayout>
+
+    <!-- Keypad section -->
+    <include layout="@layout/dialpad" />
+
+    <View
+       android:layout_width="match_parent"
+       android:layout_height="@dimen/dialpad_vertical_margin"
+       android:background="#66000000"/>
+
+    <!-- left and right paddings will be modified by the code. See DialpadFragment. -->
+    <FrameLayout
+        android:id="@+id/dialButtonContainer"
+        android:layout_width="match_parent"
+        android:layout_height="0px"
+        android:layout_weight="@integer/dialpad_layout_weight_additional_buttons"
+        android:layout_gravity="center_horizontal"
+        android:background="@drawable/dialpad_background">
+
+        <ImageButton
+            android:id="@+id/dialButton"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_gravity="center"
+            android:state_enabled="false"
+            android:background="@drawable/btn_call"
+            android:contentDescription="@string/description_dial_button"
+            android:src="@drawable/ic_dial_action_call" />
+
+    </FrameLayout>
+
+    <!-- "Dialpad chooser" UI, shown only when the user brings up the
+         Dialer while a call is already in progress.
+         When this UI is visible, the other Dialer elements
+         (the textfield/button and the dialpad) are hidden. -->
+    <ListView android:id="@+id/dialpadChooser"
+        android:layout_width="match_parent"
+        android:layout_height="1dip"
+        android:layout_weight="1"
+    />
+
+</LinearLayout>
diff --git a/res/layout/dialtacts_activity.xml b/res/layout/dialtacts_activity.xml
new file mode 100644
index 0000000..35fa00f
--- /dev/null
+++ b/res/layout/dialtacts_activity.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2006 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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_marginTop="?android:attr/actionBarSize"
+    android:id="@+id/dialtacts_frame"
+    >
+    <android.support.v4.view.ViewPager
+        android:id="@+id/pager"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+    <ImageButton
+         android:id="@+id/searchButton"
+         android:layout_width="wrap_content"
+         android:layout_height="?android:attr/actionBarSize"
+         android:layout_gravity="bottom|left"
+         android:state_enabled="false"
+         android:background="?android:attr/selectableItemBackground"
+         android:contentDescription="@string/description_search_button"
+         android:src="@drawable/ic_dial_action_search"/>
+
+    <ImageButton
+         android:id="@+id/overflow_menu"
+         android:layout_width="wrap_content"
+         android:layout_height="?android:attr/actionBarSize"
+         android:layout_gravity="bottom|right"
+         android:src="@drawable/ic_menu_overflow"
+         android:contentDescription="@string/action_menu_overflow_description"
+         android:nextFocusLeft="@id/digits"
+         android:background="?android:attr/selectableItemBackground"/>
+</FrameLayout>
diff --git a/res/layout/dialtacts_custom_action_bar.xml b/res/layout/dialtacts_custom_action_bar.xml
new file mode 100644
index 0000000..0af8eaa
--- /dev/null
+++ b/res/layout/dialtacts_custom_action_bar.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<!-- Dimensions are set at runtime in ActionBarAdapter -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="0dip"
+    android:layout_height="0dip"
+    android:orientation="horizontal">
+
+    <SearchView
+        android:id="@+id/search_view"
+        android:layout_width="0px"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:iconifiedByDefault="false"
+        android:inputType="textFilter" />
+
+    <ImageButton
+        android:id="@+id/search_option"
+        android:layout_width="wrap_content"
+        android:paddingLeft="4dip"
+        android:paddingRight="4dip"
+        android:layout_height="match_parent"
+        android:layout_alignParentRight="true"
+        android:src="@drawable/ic_menu_overflow"
+        android:background="?android:attr/selectableItemBackground"
+        android:visibility="gone" />
+
+</LinearLayout>
diff --git a/res/values/strings.xml b/res/values/strings.xml
new file mode 100644
index 0000000..2daa236
--- /dev/null
+++ b/res/values/strings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2012 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
+  -->
+
+
+<resources>
+    <!-- Directory partition name -->
+    <string name="applicationLabel">Dialer</string>
+
+    <!-- Title for the activity that dials the phone.  This is the name
+         used in the Launcher icon. -->
+    <string name="launcherDialer">Phone</string>
+
+</resources>
diff --git a/src/com/android/dialer/CallDetailActivity.java b/src/com/android/dialer/CallDetailActivity.java
new file mode 100644
index 0000000..f4ca213
--- /dev/null
+++ b/src/com/android/dialer/CallDetailActivity.java
@@ -0,0 +1,942 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.provider.Contacts.Intents.Insert;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.VoicemailContract.Voicemails;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.ActionMode;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.contacts.BackScrollManager;
+import com.android.contacts.BackScrollManager.ScrollableHeader;
+import com.android.contacts.ContactPhotoManager;
+import com.android.contacts.ContactsUtils;
+import com.android.contacts.ProximitySensorAware;
+import com.android.contacts.ProximitySensorManager;
+import com.android.contacts.R;
+import com.android.dialer.calllog.CallDetailHistoryAdapter;
+import com.android.dialer.calllog.CallTypeHelper;
+import com.android.dialer.calllog.ContactInfo;
+import com.android.dialer.calllog.ContactInfoHelper;
+import com.android.dialer.calllog.PhoneNumberHelper;
+import com.android.contacts.format.FormatUtils;
+import com.android.contacts.util.AsyncTaskExecutor;
+import com.android.contacts.util.AsyncTaskExecutors;
+import com.android.contacts.util.ClipboardUtils;
+import com.android.contacts.util.Constants;
+import com.android.dialer.voicemail.VoicemailPlaybackFragment;
+import com.android.dialer.voicemail.VoicemailStatusHelper;
+import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage;
+import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
+
+import java.util.List;
+
+/**
+ * Displays the details of a specific call log entry.
+ * <p>
+ * This activity can be either started with the URI of a single call log entry, or with the
+ * {@link #EXTRA_CALL_LOG_IDS} extra to specify a group of call log entries.
+ */
+public class CallDetailActivity extends Activity implements ProximitySensorAware {
+    private static final String TAG = "CallDetail";
+
+    /** The time to wait before enabling the blank the screen due to the proximity sensor. */
+    private static final long PROXIMITY_BLANK_DELAY_MILLIS = 100;
+    /** The time to wait before disabling the blank the screen due to the proximity sensor. */
+    private static final long PROXIMITY_UNBLANK_DELAY_MILLIS = 500;
+
+    /** The enumeration of {@link AsyncTask} objects used in this class. */
+    public enum Tasks {
+        MARK_VOICEMAIL_READ,
+        DELETE_VOICEMAIL_AND_FINISH,
+        REMOVE_FROM_CALL_LOG_AND_FINISH,
+        UPDATE_PHONE_CALL_DETAILS,
+    }
+
+    /** A long array extra containing ids of call log entries to display. */
+    public static final String EXTRA_CALL_LOG_IDS = "EXTRA_CALL_LOG_IDS";
+    /** If we are started with a voicemail, we'll find the uri to play with this extra. */
+    public static final String EXTRA_VOICEMAIL_URI = "EXTRA_VOICEMAIL_URI";
+    /** If we should immediately start playback of the voicemail, this extra will be set to true. */
+    public static final String EXTRA_VOICEMAIL_START_PLAYBACK = "EXTRA_VOICEMAIL_START_PLAYBACK";
+    /** If the activity was triggered from a notification. */
+    public static final String EXTRA_FROM_NOTIFICATION = "EXTRA_FROM_NOTIFICATION";
+
+    private CallTypeHelper mCallTypeHelper;
+    private PhoneNumberHelper mPhoneNumberHelper;
+    private PhoneCallDetailsHelper mPhoneCallDetailsHelper;
+    private TextView mHeaderTextView;
+    private View mHeaderOverlayView;
+    private ImageView mMainActionView;
+    private ImageButton mMainActionPushLayerView;
+    private ImageView mContactBackgroundView;
+    private AsyncTaskExecutor mAsyncTaskExecutor;
+    private ContactInfoHelper mContactInfoHelper;
+
+    private String mNumber = null;
+    private String mDefaultCountryIso;
+
+    /* package */ LayoutInflater mInflater;
+    /* package */ Resources mResources;
+    /** Helper to load contact photos. */
+    private ContactPhotoManager mContactPhotoManager;
+    /** Helper to make async queries to content resolver. */
+    private CallDetailActivityQueryHandler mAsyncQueryHandler;
+    /** Helper to get voicemail status messages. */
+    private VoicemailStatusHelper mVoicemailStatusHelper;
+    // Views related to voicemail status message.
+    private View mStatusMessageView;
+    private TextView mStatusMessageText;
+    private TextView mStatusMessageAction;
+
+    /** Whether we should show "edit number before call" in the options menu. */
+    private boolean mHasEditNumberBeforeCallOption;
+    /** Whether we should show "trash" in the options menu. */
+    private boolean mHasTrashOption;
+    /** Whether we should show "remove from call log" in the options menu. */
+    private boolean mHasRemoveFromCallLogOption;
+
+    private ProximitySensorManager mProximitySensorManager;
+    private final ProximitySensorListener mProximitySensorListener = new ProximitySensorListener();
+
+    /**
+     * The action mode used when the phone number is selected.  This will be non-null only when the
+     * phone number is selected.
+     */
+    private ActionMode mPhoneNumberActionMode;
+
+    private CharSequence mPhoneNumberLabelToCopy;
+    private CharSequence mPhoneNumberToCopy;
+
+    /** Listener to changes in the proximity sensor state. */
+    private class ProximitySensorListener implements ProximitySensorManager.Listener {
+        /** Used to show a blank view and hide the action bar. */
+        private final Runnable mBlankRunnable = new Runnable() {
+            @Override
+            public void run() {
+                View blankView = findViewById(R.id.blank);
+                blankView.setVisibility(View.VISIBLE);
+                getActionBar().hide();
+            }
+        };
+        /** Used to remove the blank view and show the action bar. */
+        private final Runnable mUnblankRunnable = new Runnable() {
+            @Override
+            public void run() {
+                View blankView = findViewById(R.id.blank);
+                blankView.setVisibility(View.GONE);
+                getActionBar().show();
+            }
+        };
+
+        @Override
+        public synchronized void onNear() {
+            clearPendingRequests();
+            postDelayed(mBlankRunnable, PROXIMITY_BLANK_DELAY_MILLIS);
+        }
+
+        @Override
+        public synchronized void onFar() {
+            clearPendingRequests();
+            postDelayed(mUnblankRunnable, PROXIMITY_UNBLANK_DELAY_MILLIS);
+        }
+
+        /** Removed any delayed requests that may be pending. */
+        public synchronized void clearPendingRequests() {
+            View blankView = findViewById(R.id.blank);
+            blankView.removeCallbacks(mBlankRunnable);
+            blankView.removeCallbacks(mUnblankRunnable);
+        }
+
+        /** Post a {@link Runnable} with a delay on the main thread. */
+        private synchronized void postDelayed(Runnable runnable, long delayMillis) {
+            // Post these instead of executing immediately so that:
+            // - They are guaranteed to be executed on the main thread.
+            // - If the sensor values changes rapidly for some time, the UI will not be
+            //   updated immediately.
+            View blankView = findViewById(R.id.blank);
+            blankView.postDelayed(runnable, delayMillis);
+        }
+    }
+
+    static final String[] CALL_LOG_PROJECTION = new String[] {
+        CallLog.Calls.DATE,
+        CallLog.Calls.DURATION,
+        CallLog.Calls.NUMBER,
+        CallLog.Calls.TYPE,
+        CallLog.Calls.COUNTRY_ISO,
+        CallLog.Calls.GEOCODED_LOCATION,
+    };
+
+    static final int DATE_COLUMN_INDEX = 0;
+    static final int DURATION_COLUMN_INDEX = 1;
+    static final int NUMBER_COLUMN_INDEX = 2;
+    static final int CALL_TYPE_COLUMN_INDEX = 3;
+    static final int COUNTRY_ISO_COLUMN_INDEX = 4;
+    static final int GEOCODED_LOCATION_COLUMN_INDEX = 5;
+
+    private final View.OnClickListener mPrimaryActionListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View view) {
+            if (finishPhoneNumerSelectedActionModeIfShown()) {
+                return;
+            }
+            startActivity(((ViewEntry) view.getTag()).primaryIntent);
+        }
+    };
+
+    private final View.OnClickListener mSecondaryActionListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View view) {
+            if (finishPhoneNumerSelectedActionModeIfShown()) {
+                return;
+            }
+            startActivity(((ViewEntry) view.getTag()).secondaryIntent);
+        }
+    };
+
+    private final View.OnLongClickListener mPrimaryLongClickListener =
+            new View.OnLongClickListener() {
+        @Override
+        public boolean onLongClick(View v) {
+            if (finishPhoneNumerSelectedActionModeIfShown()) {
+                return true;
+            }
+            startPhoneNumberSelectedActionMode(v);
+            return true;
+        }
+    };
+
+    @Override
+    protected void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+
+        setContentView(R.layout.call_detail);
+
+        mAsyncTaskExecutor = AsyncTaskExecutors.createThreadPoolExecutor();
+        mInflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
+        mResources = getResources();
+
+        mCallTypeHelper = new CallTypeHelper(getResources());
+        mPhoneNumberHelper = new PhoneNumberHelper(mResources);
+        mPhoneCallDetailsHelper = new PhoneCallDetailsHelper(mResources, mCallTypeHelper,
+                mPhoneNumberHelper);
+        mVoicemailStatusHelper = new VoicemailStatusHelperImpl();
+        mAsyncQueryHandler = new CallDetailActivityQueryHandler(this);
+        mHeaderTextView = (TextView) findViewById(R.id.header_text);
+        mHeaderOverlayView = findViewById(R.id.photo_text_bar);
+        mStatusMessageView = findViewById(R.id.voicemail_status);
+        mStatusMessageText = (TextView) findViewById(R.id.voicemail_status_message);
+        mStatusMessageAction = (TextView) findViewById(R.id.voicemail_status_action);
+        mMainActionView = (ImageView) findViewById(R.id.main_action);
+        mMainActionPushLayerView = (ImageButton) findViewById(R.id.main_action_push_layer);
+        mContactBackgroundView = (ImageView) findViewById(R.id.contact_background);
+        mDefaultCountryIso = ContactsUtils.getCurrentCountryIso(this);
+        mContactPhotoManager = ContactPhotoManager.getInstance(this);
+        mProximitySensorManager = new ProximitySensorManager(this, mProximitySensorListener);
+        mContactInfoHelper = new ContactInfoHelper(this, ContactsUtils.getCurrentCountryIso(this));
+        configureActionBar();
+        optionallyHandleVoicemail();
+        if (getIntent().getBooleanExtra(EXTRA_FROM_NOTIFICATION, false)) {
+            closeSystemDialogs();
+        }
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        updateData(getCallLogEntryUris());
+    }
+
+    /**
+     * Handle voicemail playback or hide voicemail ui.
+     * <p>
+     * If the Intent used to start this Activity contains the suitable extras, then start voicemail
+     * playback.  If it doesn't, then hide the voicemail ui.
+     */
+    private void optionallyHandleVoicemail() {
+        View voicemailContainer = findViewById(R.id.voicemail_container);
+        if (hasVoicemail()) {
+            // Has voicemail: add the voicemail fragment.  Add suitable arguments to set the uri
+            // to play and optionally start the playback.
+            // Do a query to fetch the voicemail status messages.
+            VoicemailPlaybackFragment playbackFragment = new VoicemailPlaybackFragment();
+            Bundle fragmentArguments = new Bundle();
+            fragmentArguments.putParcelable(EXTRA_VOICEMAIL_URI, getVoicemailUri());
+            if (getIntent().getBooleanExtra(EXTRA_VOICEMAIL_START_PLAYBACK, false)) {
+                fragmentArguments.putBoolean(EXTRA_VOICEMAIL_START_PLAYBACK, true);
+            }
+            playbackFragment.setArguments(fragmentArguments);
+            voicemailContainer.setVisibility(View.VISIBLE);
+            getFragmentManager().beginTransaction()
+                    .add(R.id.voicemail_container, playbackFragment).commitAllowingStateLoss();
+            mAsyncQueryHandler.startVoicemailStatusQuery(getVoicemailUri());
+            markVoicemailAsRead(getVoicemailUri());
+        } else {
+            // No voicemail uri: hide the status view.
+            mStatusMessageView.setVisibility(View.GONE);
+            voicemailContainer.setVisibility(View.GONE);
+        }
+    }
+
+    private boolean hasVoicemail() {
+        return getVoicemailUri() != null;
+    }
+
+    private Uri getVoicemailUri() {
+        return getIntent().getParcelableExtra(EXTRA_VOICEMAIL_URI);
+    }
+
+    private void markVoicemailAsRead(final Uri voicemailUri) {
+        mAsyncTaskExecutor.submit(Tasks.MARK_VOICEMAIL_READ, new AsyncTask<Void, Void, Void>() {
+            @Override
+            public Void doInBackground(Void... params) {
+                ContentValues values = new ContentValues();
+                values.put(Voicemails.IS_READ, true);
+                getContentResolver().update(voicemailUri, values,
+                        Voicemails.IS_READ + " = 0", null);
+                return null;
+            }
+        });
+    }
+
+    /**
+     * Returns the list of URIs to show.
+     * <p>
+     * There are two ways the URIs can be provided to the activity: as the data on the intent, or as
+     * a list of ids in the call log added as an extra on the URI.
+     * <p>
+     * If both are available, the data on the intent takes precedence.
+     */
+    private Uri[] getCallLogEntryUris() {
+        Uri uri = getIntent().getData();
+        if (uri != null) {
+            // If there is a data on the intent, it takes precedence over the extra.
+            return new Uri[]{ uri };
+        }
+        long[] ids = getIntent().getLongArrayExtra(EXTRA_CALL_LOG_IDS);
+        Uri[] uris = new Uri[ids.length];
+        for (int index = 0; index < ids.length; ++index) {
+            uris[index] = ContentUris.withAppendedId(Calls.CONTENT_URI_WITH_VOICEMAIL, ids[index]);
+        }
+        return uris;
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_CALL: {
+                // Make sure phone isn't already busy before starting direct call
+                TelephonyManager tm = (TelephonyManager)
+                        getSystemService(Context.TELEPHONY_SERVICE);
+                if (tm.getCallState() == TelephonyManager.CALL_STATE_IDLE) {
+                    startActivity(ContactsUtils.getCallIntent(
+                            Uri.fromParts(Constants.SCHEME_TEL, mNumber, null)));
+                    return true;
+                }
+            }
+        }
+
+        return super.onKeyDown(keyCode, event);
+    }
+
+    /**
+     * Update user interface with details of given call.
+     *
+     * @param callUris URIs into {@link CallLog.Calls} of the calls to be displayed
+     */
+    private void updateData(final Uri... callUris) {
+        class UpdateContactDetailsTask extends AsyncTask<Void, Void, PhoneCallDetails[]> {
+            @Override
+            public PhoneCallDetails[] doInBackground(Void... params) {
+                // TODO: All phone calls correspond to the same person, so we can make a single
+                // lookup.
+                final int numCalls = callUris.length;
+                PhoneCallDetails[] details = new PhoneCallDetails[numCalls];
+                try {
+                    for (int index = 0; index < numCalls; ++index) {
+                        details[index] = getPhoneCallDetailsForUri(callUris[index]);
+                    }
+                    return details;
+                } catch (IllegalArgumentException e) {
+                    // Something went wrong reading in our primary data.
+                    Log.w(TAG, "invalid URI starting call details", e);
+                    return null;
+                }
+            }
+
+            @Override
+            public void onPostExecute(PhoneCallDetails[] details) {
+                if (details == null) {
+                    // Somewhere went wrong: we're going to bail out and show error to users.
+                    Toast.makeText(CallDetailActivity.this, R.string.toast_call_detail_error,
+                            Toast.LENGTH_SHORT).show();
+                    finish();
+                    return;
+                }
+
+                // We know that all calls are from the same number and the same contact, so pick the
+                // first.
+                PhoneCallDetails firstDetails = details[0];
+                mNumber = firstDetails.number.toString();
+                final Uri contactUri = firstDetails.contactUri;
+                final Uri photoUri = firstDetails.photoUri;
+
+                // Set the details header, based on the first phone call.
+                mPhoneCallDetailsHelper.setCallDetailsHeader(mHeaderTextView, firstDetails);
+
+                // Cache the details about the phone number.
+                final boolean canPlaceCallsTo = mPhoneNumberHelper.canPlaceCallsTo(mNumber);
+                final boolean isVoicemailNumber = mPhoneNumberHelper.isVoicemailNumber(mNumber);
+                final boolean isSipNumber = mPhoneNumberHelper.isSipNumber(mNumber);
+
+                // Let user view contact details if they exist, otherwise add option to create new
+                // contact from this number.
+                final Intent mainActionIntent;
+                final int mainActionIcon;
+                final String mainActionDescription;
+
+                final CharSequence nameOrNumber;
+                if (!TextUtils.isEmpty(firstDetails.name)) {
+                    nameOrNumber = firstDetails.name;
+                } else {
+                    nameOrNumber = firstDetails.number;
+                }
+
+                if (contactUri != null) {
+                    mainActionIntent = new Intent(Intent.ACTION_VIEW, contactUri);
+                    // This will launch People's detail contact screen, so we probably want to
+                    // treat it as a separate People task.
+                    mainActionIntent.setFlags(
+                            Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+                    mainActionIcon = R.drawable.ic_contacts_holo_dark;
+                    mainActionDescription =
+                            getString(R.string.description_view_contact, nameOrNumber);
+                } else if (isVoicemailNumber) {
+                    mainActionIntent = null;
+                    mainActionIcon = 0;
+                    mainActionDescription = null;
+                } else if (isSipNumber) {
+                    // TODO: This item is currently disabled for SIP addresses, because
+                    // the Insert.PHONE extra only works correctly for PSTN numbers.
+                    //
+                    // To fix this for SIP addresses, we need to:
+                    // - define ContactsContract.Intents.Insert.SIP_ADDRESS, and use it here if
+                    //   the current number is a SIP address
+                    // - update the contacts UI code to handle Insert.SIP_ADDRESS by
+                    //   updating the SipAddress field
+                    // and then we can remove the "!isSipNumber" check above.
+                    mainActionIntent = null;
+                    mainActionIcon = 0;
+                    mainActionDescription = null;
+                } else if (canPlaceCallsTo) {
+                    mainActionIntent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
+                    mainActionIntent.setType(Contacts.CONTENT_ITEM_TYPE);
+                    mainActionIntent.putExtra(Insert.PHONE, mNumber);
+                    mainActionIcon = R.drawable.ic_add_contact_holo_dark;
+                    mainActionDescription = getString(R.string.description_add_contact);
+                } else {
+                    // If we cannot call the number, when we probably cannot add it as a contact either.
+                    // This is usually the case of private, unknown, or payphone numbers.
+                    mainActionIntent = null;
+                    mainActionIcon = 0;
+                    mainActionDescription = null;
+                }
+
+                if (mainActionIntent == null) {
+                    mMainActionView.setVisibility(View.INVISIBLE);
+                    mMainActionPushLayerView.setVisibility(View.GONE);
+                    mHeaderTextView.setVisibility(View.INVISIBLE);
+                    mHeaderOverlayView.setVisibility(View.INVISIBLE);
+                } else {
+                    mMainActionView.setVisibility(View.VISIBLE);
+                    mMainActionView.setImageResource(mainActionIcon);
+                    mMainActionPushLayerView.setVisibility(View.VISIBLE);
+                    mMainActionPushLayerView.setOnClickListener(new View.OnClickListener() {
+                        @Override
+                        public void onClick(View v) {
+                            startActivity(mainActionIntent);
+                        }
+                    });
+                    mMainActionPushLayerView.setContentDescription(mainActionDescription);
+                    mHeaderTextView.setVisibility(View.VISIBLE);
+                    mHeaderOverlayView.setVisibility(View.VISIBLE);
+                }
+
+                // This action allows to call the number that places the call.
+                if (canPlaceCallsTo) {
+                    final CharSequence displayNumber =
+                            mPhoneNumberHelper.getDisplayNumber(
+                                    firstDetails.number, firstDetails.formattedNumber);
+
+                    ViewEntry entry = new ViewEntry(
+                            getString(R.string.menu_callNumber,
+                                    FormatUtils.forceLeftToRight(displayNumber)),
+                                    ContactsUtils.getCallIntent(mNumber),
+                                    getString(R.string.description_call, nameOrNumber));
+
+                    // Only show a label if the number is shown and it is not a SIP address.
+                    if (!TextUtils.isEmpty(firstDetails.name)
+                            && !TextUtils.isEmpty(firstDetails.number)
+                            && !PhoneNumberUtils.isUriNumber(firstDetails.number.toString())) {
+                        entry.label = Phone.getTypeLabel(mResources, firstDetails.numberType,
+                                firstDetails.numberLabel);
+                    }
+
+                    // The secondary action allows to send an SMS to the number that placed the
+                    // call.
+                    if (mPhoneNumberHelper.canSendSmsTo(mNumber)) {
+                        entry.setSecondaryAction(
+                                R.drawable.ic_text_holo_dark,
+                                new Intent(Intent.ACTION_SENDTO,
+                                           Uri.fromParts("sms", mNumber, null)),
+                                getString(R.string.description_send_text_message, nameOrNumber));
+                    }
+
+                    configureCallButton(entry);
+                    mPhoneNumberToCopy = displayNumber;
+                    mPhoneNumberLabelToCopy = entry.label;
+                } else {
+                    disableCallButton();
+                    mPhoneNumberToCopy = null;
+                    mPhoneNumberLabelToCopy = null;
+                }
+
+                mHasEditNumberBeforeCallOption =
+                        canPlaceCallsTo && !isSipNumber && !isVoicemailNumber;
+                mHasTrashOption = hasVoicemail();
+                mHasRemoveFromCallLogOption = !hasVoicemail();
+                invalidateOptionsMenu();
+
+                ListView historyList = (ListView) findViewById(R.id.history);
+                historyList.setAdapter(
+                        new CallDetailHistoryAdapter(CallDetailActivity.this, mInflater,
+                                mCallTypeHelper, details, hasVoicemail(), canPlaceCallsTo,
+                                findViewById(R.id.controls)));
+                BackScrollManager.bind(
+                        new ScrollableHeader() {
+                            private View mControls = findViewById(R.id.controls);
+                            private View mPhoto = findViewById(R.id.contact_background_sizer);
+                            private View mHeader = findViewById(R.id.photo_text_bar);
+                            private View mSeparator = findViewById(R.id.blue_separator);
+
+                            @Override
+                            public void setOffset(int offset) {
+                                mControls.setY(-offset);
+                            }
+
+                            @Override
+                            public int getMaximumScrollableHeaderOffset() {
+                                // We can scroll the photo out, but we should keep the header if
+                                // present.
+                                if (mHeader.getVisibility() == View.VISIBLE) {
+                                    return mPhoto.getHeight() - mHeader.getHeight();
+                                } else {
+                                    // If the header is not present, we should also scroll out the
+                                    // separator line.
+                                    return mPhoto.getHeight() + mSeparator.getHeight();
+                                }
+                            }
+                        },
+                        historyList);
+                loadContactPhotos(photoUri);
+                findViewById(R.id.call_detail).setVisibility(View.VISIBLE);
+            }
+        }
+        mAsyncTaskExecutor.submit(Tasks.UPDATE_PHONE_CALL_DETAILS, new UpdateContactDetailsTask());
+    }
+
+    /** Return the phone call details for a given call log URI. */
+    private PhoneCallDetails getPhoneCallDetailsForUri(Uri callUri) {
+        ContentResolver resolver = getContentResolver();
+        Cursor callCursor = resolver.query(callUri, CALL_LOG_PROJECTION, null, null, null);
+        try {
+            if (callCursor == null || !callCursor.moveToFirst()) {
+                throw new IllegalArgumentException("Cannot find content: " + callUri);
+            }
+
+            // Read call log specifics.
+            String number = callCursor.getString(NUMBER_COLUMN_INDEX);
+            long date = callCursor.getLong(DATE_COLUMN_INDEX);
+            long duration = callCursor.getLong(DURATION_COLUMN_INDEX);
+            int callType = callCursor.getInt(CALL_TYPE_COLUMN_INDEX);
+            String countryIso = callCursor.getString(COUNTRY_ISO_COLUMN_INDEX);
+            final String geocode = callCursor.getString(GEOCODED_LOCATION_COLUMN_INDEX);
+
+            if (TextUtils.isEmpty(countryIso)) {
+                countryIso = mDefaultCountryIso;
+            }
+
+            // Formatted phone number.
+            final CharSequence formattedNumber;
+            // Read contact specifics.
+            final CharSequence nameText;
+            final int numberType;
+            final CharSequence numberLabel;
+            final Uri photoUri;
+            final Uri lookupUri;
+            // If this is not a regular number, there is no point in looking it up in the contacts.
+            ContactInfo info =
+                    mPhoneNumberHelper.canPlaceCallsTo(number)
+                    && !mPhoneNumberHelper.isVoicemailNumber(number)
+                            ? mContactInfoHelper.lookupNumber(number, countryIso)
+                            : null;
+            if (info == null) {
+                formattedNumber = mPhoneNumberHelper.getDisplayNumber(number, null);
+                nameText = "";
+                numberType = 0;
+                numberLabel = "";
+                photoUri = null;
+                lookupUri = null;
+            } else {
+                formattedNumber = info.formattedNumber;
+                nameText = info.name;
+                numberType = info.type;
+                numberLabel = info.label;
+                photoUri = info.photoUri;
+                lookupUri = info.lookupUri;
+            }
+            return new PhoneCallDetails(number, formattedNumber, countryIso, geocode,
+                    new int[]{ callType }, date, duration,
+                    nameText, numberType, numberLabel, lookupUri, photoUri);
+        } finally {
+            if (callCursor != null) {
+                callCursor.close();
+            }
+        }
+    }
+
+    /** Load the contact photos and places them in the corresponding views. */
+    private void loadContactPhotos(Uri photoUri) {
+        mContactPhotoManager.loadPhoto(mContactBackgroundView, photoUri,
+                mContactBackgroundView.getWidth(), true);
+    }
+
+    static final class ViewEntry {
+        public final String text;
+        public final Intent primaryIntent;
+        /** The description for accessibility of the primary action. */
+        public final String primaryDescription;
+
+        public CharSequence label = null;
+        /** Icon for the secondary action. */
+        public int secondaryIcon = 0;
+        /** Intent for the secondary action. If not null, an icon must be defined. */
+        public Intent secondaryIntent = null;
+        /** The description for accessibility of the secondary action. */
+        public String secondaryDescription = null;
+
+        public ViewEntry(String text, Intent intent, String description) {
+            this.text = text;
+            primaryIntent = intent;
+            primaryDescription = description;
+        }
+
+        public void setSecondaryAction(int icon, Intent intent, String description) {
+            secondaryIcon = icon;
+            secondaryIntent = intent;
+            secondaryDescription = description;
+        }
+    }
+
+    /** Disables the call button area, e.g., for private numbers. */
+    private void disableCallButton() {
+        findViewById(R.id.call_and_sms).setVisibility(View.GONE);
+    }
+
+    /** Configures the call button area using the given entry. */
+    private void configureCallButton(ViewEntry entry) {
+        View convertView = findViewById(R.id.call_and_sms);
+        convertView.setVisibility(View.VISIBLE);
+
+        ImageView icon = (ImageView) convertView.findViewById(R.id.call_and_sms_icon);
+        View divider = convertView.findViewById(R.id.call_and_sms_divider);
+        TextView text = (TextView) convertView.findViewById(R.id.call_and_sms_text);
+
+        View mainAction = convertView.findViewById(R.id.call_and_sms_main_action);
+        mainAction.setOnClickListener(mPrimaryActionListener);
+        mainAction.setTag(entry);
+        mainAction.setContentDescription(entry.primaryDescription);
+        mainAction.setOnLongClickListener(mPrimaryLongClickListener);
+
+        if (entry.secondaryIntent != null) {
+            icon.setOnClickListener(mSecondaryActionListener);
+            icon.setImageResource(entry.secondaryIcon);
+            icon.setVisibility(View.VISIBLE);
+            icon.setTag(entry);
+            icon.setContentDescription(entry.secondaryDescription);
+            divider.setVisibility(View.VISIBLE);
+        } else {
+            icon.setVisibility(View.GONE);
+            divider.setVisibility(View.GONE);
+        }
+        text.setText(entry.text);
+
+        TextView label = (TextView) convertView.findViewById(R.id.call_and_sms_label);
+        if (TextUtils.isEmpty(entry.label)) {
+            label.setVisibility(View.GONE);
+        } else {
+            label.setText(entry.label);
+            label.setVisibility(View.VISIBLE);
+        }
+    }
+
+    protected void updateVoicemailStatusMessage(Cursor statusCursor) {
+        if (statusCursor == null) {
+            mStatusMessageView.setVisibility(View.GONE);
+            return;
+        }
+        final StatusMessage message = getStatusMessage(statusCursor);
+        if (message == null || !message.showInCallDetails()) {
+            mStatusMessageView.setVisibility(View.GONE);
+            return;
+        }
+
+        mStatusMessageView.setVisibility(View.VISIBLE);
+        mStatusMessageText.setText(message.callDetailsMessageId);
+        if (message.actionMessageId != -1) {
+            mStatusMessageAction.setText(message.actionMessageId);
+        }
+        if (message.actionUri != null) {
+            mStatusMessageAction.setClickable(true);
+            mStatusMessageAction.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    startActivity(new Intent(Intent.ACTION_VIEW, message.actionUri));
+                }
+            });
+        } else {
+            mStatusMessageAction.setClickable(false);
+        }
+    }
+
+    private StatusMessage getStatusMessage(Cursor statusCursor) {
+        List<StatusMessage> messages = mVoicemailStatusHelper.getStatusMessages(statusCursor);
+        if (messages.size() == 0) {
+            return null;
+        }
+        // There can only be a single status message per source package, so num of messages can
+        // at most be 1.
+        if (messages.size() > 1) {
+            Log.w(TAG, String.format("Expected 1, found (%d) num of status messages." +
+                    " Will use the first one.", messages.size()));
+        }
+        return messages.get(0);
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        getMenuInflater().inflate(R.menu.call_details_options, menu);
+        return super.onCreateOptionsMenu(menu);
+    }
+
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        // This action deletes all elements in the group from the call log.
+        // We don't have this action for voicemails, because you can just use the trash button.
+        menu.findItem(R.id.menu_remove_from_call_log).setVisible(mHasRemoveFromCallLogOption);
+        menu.findItem(R.id.menu_edit_number_before_call).setVisible(mHasEditNumberBeforeCallOption);
+        menu.findItem(R.id.menu_trash).setVisible(mHasTrashOption);
+        return super.onPrepareOptionsMenu(menu);
+    }
+
+    @Override
+    public boolean onMenuItemSelected(int featureId, MenuItem item) {
+        switch (item.getItemId()) {
+            case android.R.id.home: {
+                onHomeSelected();
+                return true;
+            }
+
+            // All the options menu items are handled by onMenu... methods.
+            default:
+                throw new IllegalArgumentException();
+        }
+    }
+
+    public void onMenuRemoveFromCallLog(MenuItem menuItem) {
+        final StringBuilder callIds = new StringBuilder();
+        for (Uri callUri : getCallLogEntryUris()) {
+            if (callIds.length() != 0) {
+                callIds.append(",");
+            }
+            callIds.append(ContentUris.parseId(callUri));
+        }
+        mAsyncTaskExecutor.submit(Tasks.REMOVE_FROM_CALL_LOG_AND_FINISH,
+                new AsyncTask<Void, Void, Void>() {
+                    @Override
+                    public Void doInBackground(Void... params) {
+                        getContentResolver().delete(Calls.CONTENT_URI_WITH_VOICEMAIL,
+                                Calls._ID + " IN (" + callIds + ")", null);
+                        return null;
+                    }
+
+                    @Override
+                    public void onPostExecute(Void result) {
+                        finish();
+                    }
+                });
+    }
+
+    public void onMenuEditNumberBeforeCall(MenuItem menuItem) {
+        startActivity(new Intent(Intent.ACTION_DIAL, ContactsUtils.getCallUri(mNumber)));
+    }
+
+    public void onMenuTrashVoicemail(MenuItem menuItem) {
+        final Uri voicemailUri = getVoicemailUri();
+        mAsyncTaskExecutor.submit(Tasks.DELETE_VOICEMAIL_AND_FINISH,
+                new AsyncTask<Void, Void, Void>() {
+                    @Override
+                    public Void doInBackground(Void... params) {
+                        getContentResolver().delete(voicemailUri, null, null);
+                        return null;
+                    }
+                    @Override
+                    public void onPostExecute(Void result) {
+                        finish();
+                    }
+                });
+    }
+
+    private void configureActionBar() {
+        ActionBar actionBar = getActionBar();
+        if (actionBar != null) {
+            actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME);
+        }
+    }
+
+    /** Invoked when the user presses the home button in the action bar. */
+    private void onHomeSelected() {
+        Intent intent = new Intent(Intent.ACTION_VIEW, Calls.CONTENT_URI);
+        // This will open the call log even if the detail view has been opened directly.
+        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        startActivity(intent);
+        finish();
+    }
+
+    @Override
+    protected void onPause() {
+        // Immediately stop the proximity sensor.
+        disableProximitySensor(false);
+        mProximitySensorListener.clearPendingRequests();
+        super.onPause();
+    }
+
+    @Override
+    public void enableProximitySensor() {
+        mProximitySensorManager.enable();
+    }
+
+    @Override
+    public void disableProximitySensor(boolean waitForFarState) {
+        mProximitySensorManager.disable(waitForFarState);
+    }
+
+    /**
+     * If the phone number is selected, unselect it and return {@code true}.
+     * Otherwise, just {@code false}.
+     */
+    private boolean finishPhoneNumerSelectedActionModeIfShown() {
+        if (mPhoneNumberActionMode == null) return false;
+        mPhoneNumberActionMode.finish();
+        return true;
+    }
+
+    private void startPhoneNumberSelectedActionMode(View targetView) {
+        mPhoneNumberActionMode = startActionMode(new PhoneNumberActionModeCallback(targetView));
+    }
+
+    private void closeSystemDialogs() {
+        sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+    }
+
+    private class PhoneNumberActionModeCallback implements ActionMode.Callback {
+        private final View mTargetView;
+        private final Drawable mOriginalViewBackground;
+
+        public PhoneNumberActionModeCallback(View targetView) {
+            mTargetView = targetView;
+
+            // Highlight the phone number view.  Remember the old background, and put a new one.
+            mOriginalViewBackground = mTargetView.getBackground();
+            mTargetView.setBackgroundColor(getResources().getColor(R.color.item_selected));
+        }
+
+        @Override
+        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+            if (TextUtils.isEmpty(mPhoneNumberToCopy)) return false;
+
+            getMenuInflater().inflate(R.menu.call_details_cab, menu);
+            return true;
+        }
+
+        @Override
+        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+            return true;
+        }
+
+        @Override
+        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+            switch (item.getItemId()) {
+                case R.id.copy_phone_number:
+                    ClipboardUtils.copyText(CallDetailActivity.this, mPhoneNumberLabelToCopy,
+                            mPhoneNumberToCopy, true);
+                    mode.finish(); // Close the CAB
+                    return true;
+            }
+            return false;
+        }
+
+        @Override
+        public void onDestroyActionMode(ActionMode mode) {
+            mPhoneNumberActionMode = null;
+
+            // Restore the view background.
+            mTargetView.setBackground(mOriginalViewBackground);
+        }
+    }
+}
diff --git a/src/com/android/dialer/CallDetailActivityQueryHandler.java b/src/com/android/dialer/CallDetailActivityQueryHandler.java
new file mode 100644
index 0000000..08510f9
--- /dev/null
+++ b/src/com/android/dialer/CallDetailActivityQueryHandler.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2011 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.dialer;
+
+import android.content.AsyncQueryHandler;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.VoicemailContract.Status;
+import android.provider.VoicemailContract.Voicemails;
+import android.util.Log;
+
+import com.android.common.io.MoreCloseables;
+import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
+
+/**
+ * Class used by {@link CallDetailActivity} to fire async content resolver queries.
+ */
+public class CallDetailActivityQueryHandler extends AsyncQueryHandler {
+    private static final String TAG = "CallDetail";
+    private static final int QUERY_VOICEMAIL_CONTENT_TOKEN = 101;
+    private static final int QUERY_VOICEMAIL_STATUS_TOKEN = 102;
+
+    private final String[] VOICEMAIL_CONTENT_PROJECTION = new String[] {
+        Voicemails.SOURCE_PACKAGE,
+        Voicemails.HAS_CONTENT
+    };
+    private static final int SOURCE_PACKAGE_COLUMN_INDEX = 0;
+    private static final int HAS_CONTENT_COLUMN_INDEX = 1;
+
+    private final CallDetailActivity mCallDetailActivity;
+
+    public CallDetailActivityQueryHandler(CallDetailActivity callDetailActivity) {
+        super(callDetailActivity.getContentResolver());
+        mCallDetailActivity = callDetailActivity;
+    }
+
+    /**
+     * Fires a query to update voicemail status for the given voicemail record. On completion of the
+     * query a call to {@link CallDetailActivity#updateVoicemailStatusMessage(Cursor)} is made.
+     * <p>
+     * if this is a voicemail record then it makes up to two asynchronous content resolver queries.
+     * The first one to fetch voicemail content details and check if the voicemail record has audio.
+     * If the voicemail record does not have an audio yet then it fires the second query to get the
+     * voicemail status of the associated source.
+     */
+    public void startVoicemailStatusQuery(Uri voicemailUri) {
+        startQuery(QUERY_VOICEMAIL_CONTENT_TOKEN, null, voicemailUri, VOICEMAIL_CONTENT_PROJECTION,
+                null, null, null);
+    }
+
+    @Override
+    protected synchronized void onQueryComplete(int token, Object cookie, Cursor cursor) {
+        try {
+            if (token == QUERY_VOICEMAIL_CONTENT_TOKEN) {
+                // Query voicemail status only if this voicemail record does not have audio.
+                if (moveToFirst(cursor) && hasNoAudio(cursor)) {
+                    startQuery(QUERY_VOICEMAIL_STATUS_TOKEN, null,
+                            Status.buildSourceUri(getSourcePackage(cursor)),
+                            VoicemailStatusHelperImpl.PROJECTION, null, null, null);
+                } else {
+                    // nothing to show in status
+                    mCallDetailActivity.updateVoicemailStatusMessage(null);
+                }
+            } else if (token == QUERY_VOICEMAIL_STATUS_TOKEN) {
+                mCallDetailActivity.updateVoicemailStatusMessage(cursor);
+            } else {
+                Log.w(TAG, "Unknown query completed: ignoring: " + token);
+            }
+        } finally {
+            MoreCloseables.closeQuietly(cursor);
+        }
+    }
+
+    /** Check that the cursor is non-null and can be moved to first. */
+    private boolean moveToFirst(Cursor cursor) {
+        if (cursor == null || !cursor.moveToFirst()) {
+            Log.e(TAG, "Cursor not valid, could not move to first");
+            return false;
+        }
+        return true;
+    }
+
+    private boolean hasNoAudio(Cursor voicemailCursor) {
+        return voicemailCursor.getInt(HAS_CONTENT_COLUMN_INDEX) == 0;
+    }
+
+    private String getSourcePackage(Cursor voicemailCursor) {
+        return voicemailCursor.getString(SOURCE_PACKAGE_COLUMN_INDEX);
+    }
+}
diff --git a/src/com/android/dialer/DialtactsActivity.java b/src/com/android/dialer/DialtactsActivity.java
new file mode 100644
index 0000000..380b265
--- /dev/null
+++ b/src/com/android/dialer/DialtactsActivity.java
@@ -0,0 +1,1267 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer;
+
+import android.app.ActionBar;
+import android.app.ActionBar.LayoutParams;
+import android.app.ActionBar.Tab;
+import android.app.ActionBar.TabListener;
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.preference.PreferenceManager;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Intents.UI;
+import android.support.v13.app.FragmentPagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.support.v4.view.ViewPager.OnPageChangeListener;
+import android.text.TextUtils;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnFocusChangeListener;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.PopupMenu;
+import android.widget.SearchView;
+import android.widget.SearchView.OnCloseListener;
+import android.widget.SearchView.OnQueryTextListener;
+
+import com.android.contacts.ContactsUtils;
+import com.android.contacts.R;
+import com.android.contacts.activities.TransactionSafeActivity;
+import com.android.dialer.calllog.CallLogFragment;
+import com.android.dialer.dialpad.DialpadFragment;
+import com.android.contacts.interactions.PhoneNumberInteraction;
+import com.android.contacts.list.ContactListFilterController;
+import com.android.contacts.list.ContactListFilterController.ContactListFilterListener;
+import com.android.contacts.list.ContactListItemView;
+import com.android.contacts.list.OnPhoneNumberPickerActionListener;
+import com.android.contacts.list.PhoneFavoriteFragment;
+import com.android.contacts.list.PhoneNumberPickerFragment;
+import com.android.contacts.util.AccountFilterUtil;
+import com.android.contacts.util.Constants;
+import com.android.internal.telephony.ITelephony;
+
+/**
+ * The dialer activity that has one tab with the virtual 12key
+ * dialer, a tab with recent calls in it, a tab with the contacts and
+ * a tab with the favorite. This is the container and the tabs are
+ * embedded using intents.
+ * The dialer tab's title is 'phone', a more common name (see strings.xml).
+ */
+public class DialtactsActivity extends TransactionSafeActivity
+        implements View.OnClickListener {
+    private static final String TAG = "DialtactsActivity";
+
+    public static final boolean DEBUG = false;
+
+    /** Used to open Call Setting */
+    private static final String PHONE_PACKAGE = "com.android.phone";
+    private static final String CALL_SETTINGS_CLASS_NAME =
+            "com.android.phone.CallFeaturesSetting";
+
+    /** @see #getCallOrigin() */
+    private static final String CALL_ORIGIN_DIALTACTS =
+            "com.android.dialer.DialtactsActivity";
+
+    /**
+     * Just for backward compatibility. Should behave as same as {@link Intent#ACTION_DIAL}.
+     */
+    private static final String ACTION_TOUCH_DIALER = "com.android.phone.action.TOUCH_DIALER";
+
+    /** Used both by {@link ActionBar} and {@link ViewPagerAdapter} */
+    private static final int TAB_INDEX_DIALER = 0;
+    private static final int TAB_INDEX_CALL_LOG = 1;
+    private static final int TAB_INDEX_FAVORITES = 2;
+
+    private static final int TAB_INDEX_COUNT = 3;
+
+    private SharedPreferences mPrefs;
+
+    /** Last manually selected tab index */
+    private static final String PREF_LAST_MANUALLY_SELECTED_TAB =
+            "DialtactsActivity_last_manually_selected_tab";
+    private static final int PREF_LAST_MANUALLY_SELECTED_TAB_DEFAULT = TAB_INDEX_DIALER;
+
+    private static final int SUBACTIVITY_ACCOUNT_FILTER = 1;
+
+    public class ViewPagerAdapter extends FragmentPagerAdapter {
+        public ViewPagerAdapter(FragmentManager fm) {
+            super(fm);
+        }
+
+        @Override
+        public Fragment getItem(int position) {
+            switch (position) {
+                case TAB_INDEX_DIALER:
+                    return new DialpadFragment();
+                case TAB_INDEX_CALL_LOG:
+                    return new CallLogFragment();
+                case TAB_INDEX_FAVORITES:
+                    return new PhoneFavoriteFragment();
+            }
+            throw new IllegalStateException("No fragment at position " + position);
+        }
+
+        @Override
+        public void setPrimaryItem(ViewGroup container, int position, Object object) {
+            // The parent's setPrimaryItem() also calls setMenuVisibility(), so we want to know
+            // when it happens.
+            if (DEBUG) {
+                Log.d(TAG, "FragmentPagerAdapter#setPrimaryItem(), position: " + position);
+            }
+            super.setPrimaryItem(container, position, object);
+        }
+
+        @Override
+        public int getCount() {
+            return TAB_INDEX_COUNT;
+        }
+    }
+
+    /**
+     * True when the app detects user's drag event. This variable should not become true when
+     * mUserTabClick is true.
+     *
+     * During user's drag or tab click, we shouldn't show fake buttons but just show real
+     * ActionBar at the bottom of the screen, for transition animation.
+     */
+    boolean mDuringSwipe = false;
+    /**
+     * True when the app detects user's tab click (at the top of the screen). This variable should
+     * not become true when mDuringSwipe is true.
+     *
+     * During user's drag or tab click, we shouldn't show fake buttons but just show real
+     * ActionBar at the bottom of the screen, for transition animation.
+     */
+    boolean mUserTabClick = false;
+
+    private class PageChangeListener implements OnPageChangeListener {
+        private int mCurrentPosition = -1;
+        /**
+         * Used during page migration, to remember the next position {@link #onPageSelected(int)}
+         * specified.
+         */
+        private int mNextPosition = -1;
+
+        @Override
+        public void onPageScrolled(
+                int position, float positionOffset, int positionOffsetPixels) {
+        }
+
+        @Override
+        public void onPageSelected(int position) {
+            if (DEBUG) Log.d(TAG, "onPageSelected: position: " + position);
+            final ActionBar actionBar = getActionBar();
+            if (mDialpadFragment != null) {
+                if (mDuringSwipe && position == TAB_INDEX_DIALER) {
+                    // TODO: Figure out if we want this or not. Right now
+                    // - with this call, both fake buttons and real action bar overlap
+                    // - without this call, there's tiny flicker happening to search/menu buttons.
+                    // If we can reduce the flicker without this call, it would be much better.
+                    // updateFakeMenuButtonsVisibility(true);
+                }
+            }
+
+            if (mCurrentPosition == position) {
+                Log.w(TAG, "Previous position and next position became same (" + position + ")");
+            }
+
+            actionBar.selectTab(actionBar.getTabAt(position));
+            mNextPosition = position;
+        }
+
+        public void setCurrentPosition(int position) {
+            mCurrentPosition = position;
+        }
+
+        public int getCurrentPosition() {
+            return mCurrentPosition;
+        }
+
+        @Override
+        public void onPageScrollStateChanged(int state) {
+            switch (state) {
+                case ViewPager.SCROLL_STATE_IDLE: {
+                    if (mNextPosition == -1) {
+                        // This happens when the user drags the screen just after launching the
+                        // application, and settle down the same screen without actually swiping it.
+                        // At that moment mNextPosition is apparently -1 yet, and we expect it
+                        // being updated by onPageSelected(), which is *not* called if the user
+                        // settle down the exact same tab after the dragging.
+                        if (DEBUG) {
+                            Log.d(TAG, "Next position is not specified correctly. Use current tab ("
+                                    + mViewPager.getCurrentItem() + ")");
+                        }
+                        mNextPosition = mViewPager.getCurrentItem();
+                    }
+                    if (DEBUG) {
+                        Log.d(TAG, "onPageScrollStateChanged() with SCROLL_STATE_IDLE. "
+                                + "mCurrentPosition: " + mCurrentPosition
+                                + ", mNextPosition: " + mNextPosition);
+                    }
+                    // Interpret IDLE as the end of migration (both swipe and tab click)
+                    mDuringSwipe = false;
+                    mUserTabClick = false;
+
+                    updateFakeMenuButtonsVisibility(mNextPosition == TAB_INDEX_DIALER);
+                    sendFragmentVisibilityChange(mCurrentPosition, false);
+                    sendFragmentVisibilityChange(mNextPosition, true);
+
+                    invalidateOptionsMenu();
+
+                    mCurrentPosition = mNextPosition;
+                    break;
+                }
+                case ViewPager.SCROLL_STATE_DRAGGING: {
+                    if (DEBUG) Log.d(TAG, "onPageScrollStateChanged() with SCROLL_STATE_DRAGGING");
+                    mDuringSwipe = true;
+                    mUserTabClick = false;
+                    break;
+                }
+                case ViewPager.SCROLL_STATE_SETTLING: {
+                    if (DEBUG) Log.d(TAG, "onPageScrollStateChanged() with SCROLL_STATE_SETTLING");
+                    mDuringSwipe = true;
+                    mUserTabClick = false;
+                    break;
+                }
+                default:
+                    break;
+            }
+        }
+    }
+
+    private String mFilterText;
+
+    /** Enables horizontal swipe between Fragments. */
+    private ViewPager mViewPager;
+    private final PageChangeListener mPageChangeListener = new PageChangeListener();
+    private DialpadFragment mDialpadFragment;
+    private CallLogFragment mCallLogFragment;
+    private PhoneFavoriteFragment mPhoneFavoriteFragment;
+
+    private View mSearchButton;
+    private View mMenuButton;
+
+    private final ContactListFilterListener mContactListFilterListener =
+            new ContactListFilterListener() {
+        @Override
+        public void onContactListFilterChanged() {
+            boolean doInvalidateOptionsMenu = false;
+
+            if (mPhoneFavoriteFragment != null && mPhoneFavoriteFragment.isAdded()) {
+                mPhoneFavoriteFragment.setFilter(mContactListFilterController.getFilter());
+                doInvalidateOptionsMenu = true;
+            }
+
+            if (mSearchFragment != null && mSearchFragment.isAdded()) {
+                mSearchFragment.setFilter(mContactListFilterController.getFilter());
+                doInvalidateOptionsMenu = true;
+            } else {
+                Log.w(TAG, "Search Fragment isn't available when ContactListFilter is changed");
+            }
+
+            if (doInvalidateOptionsMenu) {
+                invalidateOptionsMenu();
+            }
+        }
+    };
+
+    private final TabListener mTabListener = new TabListener() {
+        @Override
+        public void onTabUnselected(Tab tab, FragmentTransaction ft) {
+            if (DEBUG) Log.d(TAG, "onTabUnselected(). tab: " + tab);
+        }
+
+        @Override
+        public void onTabSelected(Tab tab, FragmentTransaction ft) {
+            if (DEBUG) {
+                Log.d(TAG, "onTabSelected(). tab: " + tab + ", mDuringSwipe: " + mDuringSwipe);
+            }
+            // When the user swipes the screen horizontally, this method will be called after
+            // ViewPager.SCROLL_STATE_DRAGGING and ViewPager.SCROLL_STATE_SETTLING events, while
+            // when the user clicks a tab at the ActionBar at the top, this will be called before
+            // them. This logic interprets the order difference as a difference of the user action.
+            if (!mDuringSwipe) {
+                if (DEBUG) {
+                    Log.d(TAG, "Tab select. from: " + mPageChangeListener.getCurrentPosition()
+                            + ", to: " + tab.getPosition());
+                }
+                if (mDialpadFragment != null) {
+                    updateFakeMenuButtonsVisibility(tab.getPosition() == TAB_INDEX_DIALER);
+                }
+                mUserTabClick = true;
+            }
+
+            if (mViewPager.getCurrentItem() != tab.getPosition()) {
+                mViewPager.setCurrentItem(tab.getPosition(), true);
+            }
+
+            // During the call, we don't remember the tab position.
+            if (!DialpadFragment.phoneIsInUse()) {
+                // Remember this tab index. This function is also called, if the tab is set
+                // automatically in which case the setter (setCurrentTab) has to set this to its old
+                // value afterwards
+                mLastManuallySelectedFragment = tab.getPosition();
+            }
+        }
+
+        @Override
+        public void onTabReselected(Tab tab, FragmentTransaction ft) {
+            if (DEBUG) Log.d(TAG, "onTabReselected");
+        }
+    };
+
+    /**
+     * Fragment for searching phone numbers. Unlike the other Fragments, this doesn't correspond
+     * to tab but is shown by a search action.
+     */
+    private PhoneNumberPickerFragment mSearchFragment;
+    /**
+     * True when this Activity is in its search UI (with a {@link SearchView} and
+     * {@link PhoneNumberPickerFragment}).
+     */
+    private boolean mInSearchUi;
+    private SearchView mSearchView;
+
+    private final OnClickListener mFilterOptionClickListener = new OnClickListener() {
+        @Override
+        public void onClick(View view) {
+            final PopupMenu popupMenu = new PopupMenu(DialtactsActivity.this, view);
+            final Menu menu = popupMenu.getMenu();
+            popupMenu.inflate(R.menu.dialtacts_search_options);
+            final MenuItem filterOptionMenuItem = menu.findItem(R.id.filter_option);
+            filterOptionMenuItem.setOnMenuItemClickListener(mFilterOptionsMenuItemClickListener);
+            final MenuItem addContactOptionMenuItem = menu.findItem(R.id.add_contact);
+            addContactOptionMenuItem.setIntent(
+                    new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI));
+            popupMenu.show();
+        }
+    };
+
+    /**
+     * The index of the Fragment (or, the tab) that has last been manually selected.
+     * This value does not keep track of programmatically set Tabs (e.g. Call Log after a Call)
+     */
+    private int mLastManuallySelectedFragment;
+
+    private ContactListFilterController mContactListFilterController;
+    private OnMenuItemClickListener mFilterOptionsMenuItemClickListener =
+            new OnMenuItemClickListener() {
+        @Override
+        public boolean onMenuItemClick(MenuItem item) {
+            AccountFilterUtil.startAccountFilterActivityForResult(
+                    DialtactsActivity.this, SUBACTIVITY_ACCOUNT_FILTER,
+                    mContactListFilterController.getFilter());
+            return true;
+        }
+    };
+
+    private OnMenuItemClickListener mSearchMenuItemClickListener =
+            new OnMenuItemClickListener() {
+        @Override
+        public boolean onMenuItemClick(MenuItem item) {
+            enterSearchUi();
+            return true;
+        }
+    };
+
+    /**
+     * Listener used when one of phone numbers in search UI is selected. This will initiate a
+     * phone call using the phone number.
+     */
+    private final OnPhoneNumberPickerActionListener mPhoneNumberPickerActionListener =
+            new OnPhoneNumberPickerActionListener() {
+                @Override
+                public void onPickPhoneNumberAction(Uri dataUri) {
+                    // Specify call-origin so that users will see the previous tab instead of
+                    // CallLog screen (search UI will be automatically exited).
+                    PhoneNumberInteraction.startInteractionForPhoneCall(
+                            DialtactsActivity.this, dataUri, getCallOrigin());
+                }
+
+                @Override
+                public void onShortcutIntentCreated(Intent intent) {
+                    Log.w(TAG, "Unsupported intent has come (" + intent + "). Ignoring.");
+                }
+
+                @Override
+                public void onHomeInActionBarSelected() {
+                    exitSearchUi();
+                }
+    };
+
+    /**
+     * Listener used to send search queries to the phone search fragment.
+     */
+    private final OnQueryTextListener mPhoneSearchQueryTextListener =
+            new OnQueryTextListener() {
+                @Override
+                public boolean onQueryTextSubmit(String query) {
+                    View view = getCurrentFocus();
+                    if (view != null) {
+                        hideInputMethod(view);
+                        view.clearFocus();
+                    }
+                    return true;
+                }
+
+                @Override
+                public boolean onQueryTextChange(String newText) {
+                    // Show search result with non-empty text. Show a bare list otherwise.
+                    if (mSearchFragment != null) {
+                        mSearchFragment.setQueryString(newText, true);
+                    }
+                    return true;
+                }
+    };
+
+    /**
+     * Listener used to handle the "close" button on the right side of {@link SearchView}.
+     * If some text is in the search view, this will clean it up. Otherwise this will exit
+     * the search UI and let users go back to usual Phone UI.
+     *
+     * This does _not_ handle back button.
+     */
+    private final OnCloseListener mPhoneSearchCloseListener =
+            new OnCloseListener() {
+                @Override
+                public boolean onClose() {
+                    if (!TextUtils.isEmpty(mSearchView.getQuery())) {
+                        mSearchView.setQuery(null, true);
+                    }
+                    return true;
+                }
+    };
+
+    private final View.OnLayoutChangeListener mFirstLayoutListener
+            = new View.OnLayoutChangeListener() {
+        @Override
+        public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
+                int oldTop, int oldRight, int oldBottom) {
+            v.removeOnLayoutChangeListener(this); // Unregister self.
+            addSearchFragment();
+        }
+    };
+
+    @Override
+    protected void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+
+        final Intent intent = getIntent();
+        fixIntent(intent);
+
+        setContentView(R.layout.dialtacts_activity);
+
+        mContactListFilterController = ContactListFilterController.getInstance(this);
+        mContactListFilterController.addListener(mContactListFilterListener);
+
+        findViewById(R.id.dialtacts_frame).addOnLayoutChangeListener(mFirstLayoutListener);
+
+        mViewPager = (ViewPager) findViewById(R.id.pager);
+        mViewPager.setAdapter(new ViewPagerAdapter(getFragmentManager()));
+        mViewPager.setOnPageChangeListener(mPageChangeListener);
+        mViewPager.setOffscreenPageLimit(2);
+
+        // Do same width calculation as ActionBar does
+        DisplayMetrics dm = getResources().getDisplayMetrics();
+        int minCellSize = getResources().getDimensionPixelSize(R.dimen.fake_menu_button_min_width);
+        int cellCount = dm.widthPixels / minCellSize;
+        int fakeMenuItemWidth = dm.widthPixels / cellCount;
+        if (DEBUG) Log.d(TAG, "The size of fake menu buttons (in pixel): " + fakeMenuItemWidth);
+
+        // Soft menu button should appear only when there's no hardware menu button.
+        mMenuButton = findViewById(R.id.overflow_menu);
+        if (mMenuButton != null) {
+            mMenuButton.setMinimumWidth(fakeMenuItemWidth);
+            if (ViewConfiguration.get(this).hasPermanentMenuKey()) {
+                // This is required for dialpad button's layout, so must not use GONE here.
+                mMenuButton.setVisibility(View.INVISIBLE);
+            } else {
+                mMenuButton.setOnClickListener(this);
+            }
+        }
+        mSearchButton = findViewById(R.id.searchButton);
+        if (mSearchButton != null) {
+            mSearchButton.setMinimumWidth(fakeMenuItemWidth);
+            mSearchButton.setOnClickListener(this);
+        }
+
+        // Setup the ActionBar tabs (the order matches the tab-index contants TAB_INDEX_*)
+        setupDialer();
+        setupCallLog();
+        setupFavorites();
+        getActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+        getActionBar().setDisplayShowTitleEnabled(false);
+        getActionBar().setDisplayShowHomeEnabled(false);
+
+        // Load the last manually loaded tab
+        mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+        mLastManuallySelectedFragment = mPrefs.getInt(PREF_LAST_MANUALLY_SELECTED_TAB,
+                PREF_LAST_MANUALLY_SELECTED_TAB_DEFAULT);
+        if (mLastManuallySelectedFragment >= TAB_INDEX_COUNT) {
+            // Stored value may have exceeded the number of current tabs. Reset it.
+            mLastManuallySelectedFragment = PREF_LAST_MANUALLY_SELECTED_TAB_DEFAULT;
+        }
+
+        setCurrentTab(intent);
+
+        if (UI.FILTER_CONTACTS_ACTION.equals(intent.getAction())
+                && icicle == null) {
+            setupFilterText(intent);
+        }
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        if (mPhoneFavoriteFragment != null) {
+            mPhoneFavoriteFragment.setFilter(mContactListFilterController.getFilter());
+        }
+        if (mSearchFragment != null) {
+            mSearchFragment.setFilter(mContactListFilterController.getFilter());
+        }
+
+        if (mDuringSwipe || mUserTabClick) {
+            if (DEBUG) Log.d(TAG, "reset buggy flag state..");
+            mDuringSwipe = false;
+            mUserTabClick = false;
+        }
+
+        final int currentPosition = mPageChangeListener.getCurrentPosition();
+        if (DEBUG) {
+            Log.d(TAG, "onStart(). current position: " + mPageChangeListener.getCurrentPosition()
+                    + ". Reset all menu visibility state.");
+        }
+        updateFakeMenuButtonsVisibility(currentPosition == TAB_INDEX_DIALER && !mInSearchUi);
+        for (int i = 0; i < TAB_INDEX_COUNT; i++) {
+            sendFragmentVisibilityChange(i, i == currentPosition);
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        mContactListFilterController.removeListener(mContactListFilterListener);
+    }
+
+    @Override
+    public void onClick(View view) {
+        switch (view.getId()) {
+            case R.id.searchButton: {
+                enterSearchUi();
+                break;
+            }
+            case R.id.overflow_menu: {
+                if (mDialpadFragment != null) {
+                    PopupMenu popup = mDialpadFragment.constructPopupMenu(view);
+                    if (popup != null) {
+                        popup.show();
+                    }
+                } else {
+                    Log.w(TAG, "DialpadFragment is null during onClick() event for " + view);
+                }
+                break;
+            }
+            default: {
+                Log.wtf(TAG, "Unexpected onClick event from " + view);
+                break;
+            }
+        }
+    }
+
+    /**
+     * Add search fragment.  Note this is called during onLayout, so there's some restrictions,
+     * such as executePendingTransaction can't be used in it.
+     */
+    private void addSearchFragment() {
+        // In order to take full advantage of "fragment deferred start", we need to create the
+        // search fragment after all other fragments are created.
+        // The other fragments are created by the ViewPager on the first onMeasure().
+        // We use the first onLayout call, which is after onMeasure().
+
+        // Just return if the fragment is already created, which happens after configuration
+        // changes.
+        if (mSearchFragment != null) return;
+
+        final FragmentTransaction ft = getFragmentManager().beginTransaction();
+        final Fragment searchFragment = new PhoneNumberPickerFragment();
+
+        searchFragment.setUserVisibleHint(false);
+        ft.add(R.id.dialtacts_frame, searchFragment);
+        ft.hide(searchFragment);
+        ft.commitAllowingStateLoss();
+    }
+
+    private void prepareSearchView() {
+        final View searchViewLayout =
+                getLayoutInflater().inflate(R.layout.dialtacts_custom_action_bar, null);
+        mSearchView = (SearchView) searchViewLayout.findViewById(R.id.search_view);
+        mSearchView.setOnQueryTextListener(mPhoneSearchQueryTextListener);
+        mSearchView.setOnCloseListener(mPhoneSearchCloseListener);
+        // Since we're using a custom layout for showing SearchView instead of letting the
+        // search menu icon do that job, we need to manually configure the View so it looks
+        // "shown via search menu".
+        // - it should be iconified by default
+        // - it should not be iconified at this time
+        // See also comments for onActionViewExpanded()/onActionViewCollapsed()
+        mSearchView.setIconifiedByDefault(true);
+        mSearchView.setQueryHint(getString(R.string.hint_findContacts));
+        mSearchView.setIconified(false);
+        mSearchView.setOnQueryTextFocusChangeListener(new OnFocusChangeListener() {
+            @Override
+            public void onFocusChange(View view, boolean hasFocus) {
+                if (hasFocus) {
+                    showInputMethod(view.findFocus());
+                }
+            }
+        });
+
+        if (!ViewConfiguration.get(this).hasPermanentMenuKey()) {
+            // Filter option menu should be shown on the right side of SearchView.
+            final View filterOptionView = searchViewLayout.findViewById(R.id.search_option);
+            filterOptionView.setVisibility(View.VISIBLE);
+            filterOptionView.setOnClickListener(mFilterOptionClickListener);
+        }
+
+        getActionBar().setCustomView(searchViewLayout,
+                new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
+    }
+
+    @Override
+    public void onAttachFragment(Fragment fragment) {
+        // This method can be called before onCreate(), at which point we cannot rely on ViewPager.
+        // In that case, we will setup the "current position" soon after the ViewPager is ready.
+        final int currentPosition = mViewPager != null ? mViewPager.getCurrentItem() : -1;
+
+        if (fragment instanceof DialpadFragment) {
+            mDialpadFragment = (DialpadFragment) fragment;
+        } else if (fragment instanceof CallLogFragment) {
+            mCallLogFragment = (CallLogFragment) fragment;
+        } else if (fragment instanceof PhoneFavoriteFragment) {
+            mPhoneFavoriteFragment = (PhoneFavoriteFragment) fragment;
+            mPhoneFavoriteFragment.setListener(mPhoneFavoriteListener);
+            if (mContactListFilterController != null
+                    && mContactListFilterController.getFilter() != null) {
+                mPhoneFavoriteFragment.setFilter(mContactListFilterController.getFilter());
+            }
+        } else if (fragment instanceof PhoneNumberPickerFragment) {
+            mSearchFragment = (PhoneNumberPickerFragment) fragment;
+            mSearchFragment.setOnPhoneNumberPickerActionListener(mPhoneNumberPickerActionListener);
+            mSearchFragment.setQuickContactEnabled(true);
+            mSearchFragment.setDarkTheme(true);
+            mSearchFragment.setPhotoPosition(ContactListItemView.PhotoPosition.LEFT);
+            mSearchFragment.setUseCallableUri(true);
+            if (mContactListFilterController != null
+                    && mContactListFilterController.getFilter() != null) {
+                mSearchFragment.setFilter(mContactListFilterController.getFilter());
+            }
+            // Here we assume that we're not on the search mode, so let's hide the fragment.
+            //
+            // We get here either when the fragment is created (normal case), or after configuration
+            // changes.  In the former case, we're not in search mode because we can only
+            // enter search mode if the fragment is created.  (see enterSearchUi())
+            // In the latter case we're not in search mode either because we don't retain
+            // mInSearchUi -- ideally we should but at this point it's not supported.
+            mSearchFragment.setUserVisibleHint(false);
+            // After configuration changes fragments will forget their "hidden" state, so make
+            // sure to hide it.
+            if (!mSearchFragment.isHidden()) {
+                final FragmentTransaction transaction = getFragmentManager().beginTransaction();
+                transaction.hide(mSearchFragment);
+                transaction.commitAllowingStateLoss();
+            }
+        }
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+
+        mPrefs.edit().putInt(PREF_LAST_MANUALLY_SELECTED_TAB, mLastManuallySelectedFragment)
+                .apply();
+    }
+
+    private void fixIntent(Intent intent) {
+        // This should be cleaned up: the call key used to send an Intent
+        // that just said to go to the recent calls list.  It now sends this
+        // abstract action, but this class hasn't been rewritten to deal with it.
+        if (Intent.ACTION_CALL_BUTTON.equals(intent.getAction())) {
+            intent.setDataAndType(Calls.CONTENT_URI, Calls.CONTENT_TYPE);
+            intent.putExtra("call_key", true);
+            setIntent(intent);
+        }
+    }
+
+    private void setupDialer() {
+        final Tab tab = getActionBar().newTab();
+        tab.setContentDescription(R.string.dialerIconLabel);
+        tab.setTabListener(mTabListener);
+        tab.setIcon(R.drawable.ic_tab_dialer);
+        getActionBar().addTab(tab);
+    }
+
+    private void setupCallLog() {
+        final Tab tab = getActionBar().newTab();
+        tab.setContentDescription(R.string.recentCallsIconLabel);
+        tab.setIcon(R.drawable.ic_tab_recent);
+        tab.setTabListener(mTabListener);
+        getActionBar().addTab(tab);
+    }
+
+    private void setupFavorites() {
+        final Tab tab = getActionBar().newTab();
+        tab.setContentDescription(R.string.contactsFavoritesLabel);
+        tab.setIcon(R.drawable.ic_tab_all);
+        tab.setTabListener(mTabListener);
+        getActionBar().addTab(tab);
+    }
+
+    /**
+     * Returns true if the intent is due to hitting the green send key (hardware call button:
+     * KEYCODE_CALL) while in a call.
+     *
+     * @param intent the intent that launched this activity
+     * @param recentCallsRequest true if the intent is requesting to view recent calls
+     * @return true if the intent is due to hitting the green send key while in a call
+     */
+    private boolean isSendKeyWhileInCall(final Intent intent,
+            final boolean recentCallsRequest) {
+        // If there is a call in progress go to the call screen
+        if (recentCallsRequest) {
+            final boolean callKey = intent.getBooleanExtra("call_key", false);
+
+            try {
+                ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone"));
+                if (callKey && phone != null && phone.showCallScreen()) {
+                    return true;
+                }
+            } catch (RemoteException e) {
+                Log.e(TAG, "Failed to handle send while in call", e);
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Sets the current tab based on the intent's request type
+     *
+     * @param intent Intent that contains information about which tab should be selected
+     */
+    private void setCurrentTab(Intent intent) {
+        // If we got here by hitting send and we're in call forward along to the in-call activity
+        boolean recentCallsRequest = Calls.CONTENT_TYPE.equals(intent.resolveType(
+            getContentResolver()));
+        if (isSendKeyWhileInCall(intent, recentCallsRequest)) {
+            finish();
+            return;
+        }
+
+        // Remember the old manually selected tab index so that it can be restored if it is
+        // overwritten by one of the programmatic tab selections
+        final int savedTabIndex = mLastManuallySelectedFragment;
+
+        final int tabIndex;
+        if (DialpadFragment.phoneIsInUse() || isDialIntent(intent)) {
+            tabIndex = TAB_INDEX_DIALER;
+        } else if (recentCallsRequest) {
+            tabIndex = TAB_INDEX_CALL_LOG;
+        } else {
+            tabIndex = mLastManuallySelectedFragment;
+        }
+
+        final int previousItemIndex = mViewPager.getCurrentItem();
+        mViewPager.setCurrentItem(tabIndex, false /* smoothScroll */);
+        if (previousItemIndex != tabIndex) {
+            sendFragmentVisibilityChange(previousItemIndex, false /* not visible */ );
+        }
+        mPageChangeListener.setCurrentPosition(tabIndex);
+        sendFragmentVisibilityChange(tabIndex, true /* visible */ );
+
+        // Restore to the previous manual selection
+        mLastManuallySelectedFragment = savedTabIndex;
+        mDuringSwipe = false;
+        mUserTabClick = false;
+    }
+
+    @Override
+    public void onNewIntent(Intent newIntent) {
+        setIntent(newIntent);
+        fixIntent(newIntent);
+        setCurrentTab(newIntent);
+        final String action = newIntent.getAction();
+        if (UI.FILTER_CONTACTS_ACTION.equals(action)) {
+            setupFilterText(newIntent);
+        }
+        if (mInSearchUi || (mSearchFragment != null && mSearchFragment.isVisible())) {
+            exitSearchUi();
+        }
+
+        if (mViewPager.getCurrentItem() == TAB_INDEX_DIALER) {
+            if (mDialpadFragment != null) {
+                mDialpadFragment.configureScreenFromIntent(newIntent);
+            } else {
+                Log.e(TAG, "DialpadFragment isn't ready yet when the tab is already selected.");
+            }
+        } else if (mViewPager.getCurrentItem() == TAB_INDEX_CALL_LOG) {
+            if (mCallLogFragment != null) {
+                mCallLogFragment.configureScreenFromIntent(newIntent);
+            } else {
+                Log.e(TAG, "CallLogFragment isn't ready yet when the tab is already selected.");
+            }
+        }
+        invalidateOptionsMenu();
+    }
+
+    /** Returns true if the given intent contains a phone number to populate the dialer with */
+    private boolean isDialIntent(Intent intent) {
+        final String action = intent.getAction();
+        if (Intent.ACTION_DIAL.equals(action) || ACTION_TOUCH_DIALER.equals(action)) {
+            return true;
+        }
+        if (Intent.ACTION_VIEW.equals(action)) {
+            final Uri data = intent.getData();
+            if (data != null && Constants.SCHEME_TEL.equals(data.getScheme())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns an appropriate call origin for this Activity. May return null when no call origin
+     * should be used (e.g. when some 3rd party application launched the screen. Call origin is
+     * for remembering the tab in which the user made a phone call, so the external app's DIAL
+     * request should not be counted.)
+     */
+    public String getCallOrigin() {
+        return !isDialIntent(getIntent()) ? CALL_ORIGIN_DIALTACTS : null;
+    }
+
+    /**
+     * Retrieves the filter text stored in {@link #setupFilterText(Intent)}.
+     * This text originally came from a FILTER_CONTACTS_ACTION intent received
+     * by this activity. The stored text will then be cleared after after this
+     * method returns.
+     *
+     * @return The stored filter text
+     */
+    public String getAndClearFilterText() {
+        String filterText = mFilterText;
+        mFilterText = null;
+        return filterText;
+    }
+
+    /**
+     * Stores the filter text associated with a FILTER_CONTACTS_ACTION intent.
+     * This is so child activities can check if they are supposed to display a filter.
+     *
+     * @param intent The intent received in {@link #onNewIntent(Intent)}
+     */
+    private void setupFilterText(Intent intent) {
+        // If the intent was relaunched from history, don't apply the filter text.
+        if ((intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) {
+            return;
+        }
+        String filter = intent.getStringExtra(UI.FILTER_TEXT_EXTRA_KEY);
+        if (filter != null && filter.length() > 0) {
+            mFilterText = filter;
+        }
+    }
+
+    @Override
+    public void onBackPressed() {
+        if (mInSearchUi) {
+            // We should let the user go back to usual screens with tabs.
+            exitSearchUi();
+        } else if (isTaskRoot()) {
+            // Instead of stopping, simply push this to the back of the stack.
+            // This is only done when running at the top of the stack;
+            // otherwise, we have been launched by someone else so need to
+            // allow the user to go back to the caller.
+            moveTaskToBack(false);
+        } else {
+            super.onBackPressed();
+        }
+    }
+
+    private final PhoneFavoriteFragment.Listener mPhoneFavoriteListener =
+            new PhoneFavoriteFragment.Listener() {
+        @Override
+        public void onContactSelected(Uri contactUri) {
+            PhoneNumberInteraction.startInteractionForPhoneCall(
+                    DialtactsActivity.this, contactUri, getCallOrigin());
+        }
+
+        @Override
+        public void onCallNumberDirectly(String phoneNumber) {
+            Intent intent = ContactsUtils.getCallIntent(phoneNumber, getCallOrigin());
+            startActivity(intent);
+        }
+    };
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        MenuInflater inflater = getMenuInflater();
+        inflater.inflate(R.menu.dialtacts_options, menu);
+
+        // set up intents and onClick listeners
+        final MenuItem callSettingsMenuItem = menu.findItem(R.id.menu_call_settings);
+        final MenuItem searchMenuItem = menu.findItem(R.id.search_on_action_bar);
+        final MenuItem filterOptionMenuItem = menu.findItem(R.id.filter_option);
+        final MenuItem addContactOptionMenuItem = menu.findItem(R.id.add_contact);
+
+        callSettingsMenuItem.setIntent(DialtactsActivity.getCallSettingsIntent());
+        searchMenuItem.setOnMenuItemClickListener(mSearchMenuItemClickListener);
+        filterOptionMenuItem.setOnMenuItemClickListener(mFilterOptionsMenuItemClickListener);
+        addContactOptionMenuItem.setIntent(
+                new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI));
+
+        return true;
+    }
+
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        if (mInSearchUi) {
+            prepareOptionsMenuInSearchMode(menu);
+        } else {
+            // get reference to the currently selected tab
+            final Tab tab = getActionBar().getSelectedTab();
+            if (tab != null) {
+                switch(tab.getPosition()) {
+                    case TAB_INDEX_DIALER:
+                        prepareOptionsMenuForDialerTab(menu);
+                        break;
+                    case TAB_INDEX_CALL_LOG:
+                        prepareOptionsMenuForCallLogTab(menu);
+                        break;
+                    case TAB_INDEX_FAVORITES:
+                        prepareOptionsMenuForFavoritesTab(menu);
+                        break;
+                }
+            }
+        }
+        return true;
+    }
+
+    private void prepareOptionsMenuInSearchMode(Menu menu) {
+        // get references to menu items
+        final MenuItem searchMenuItem = menu.findItem(R.id.search_on_action_bar);
+        final MenuItem filterOptionMenuItem = menu.findItem(R.id.filter_option);
+        final MenuItem addContactOptionMenuItem = menu.findItem(R.id.add_contact);
+        final MenuItem callSettingsMenuItem = menu.findItem(R.id.menu_call_settings);
+        final MenuItem emptyRightMenuItem = menu.findItem(R.id.empty_right_menu_item);
+
+        // prepare the menu items
+        searchMenuItem.setVisible(false);
+        filterOptionMenuItem.setVisible(ViewConfiguration.get(this).hasPermanentMenuKey());
+        addContactOptionMenuItem.setVisible(false);
+        callSettingsMenuItem.setVisible(false);
+        emptyRightMenuItem.setVisible(false);
+    }
+
+    private void prepareOptionsMenuForDialerTab(Menu menu) {
+        if (DEBUG) {
+            Log.d(TAG, "onPrepareOptionsMenu(dialer). swipe: " + mDuringSwipe
+                    + ", user tab click: " + mUserTabClick);
+        }
+
+        // get references to menu items
+        final MenuItem searchMenuItem = menu.findItem(R.id.search_on_action_bar);
+        final MenuItem filterOptionMenuItem = menu.findItem(R.id.filter_option);
+        final MenuItem addContactOptionMenuItem = menu.findItem(R.id.add_contact);
+        final MenuItem callSettingsMenuItem = menu.findItem(R.id.menu_call_settings);
+        final MenuItem emptyRightMenuItem = menu.findItem(R.id.empty_right_menu_item);
+
+        // prepare the menu items
+        filterOptionMenuItem.setVisible(false);
+        addContactOptionMenuItem.setVisible(false);
+        if (mDuringSwipe || mUserTabClick) {
+            // During horizontal movement, the real ActionBar menu items are shown
+            searchMenuItem.setVisible(true);
+            callSettingsMenuItem.setVisible(true);
+            // When there is a permanent menu key, there is no overflow icon on the right of
+            // the action bar which would force the search menu item (if it is visible) to the
+            // left.  This is the purpose of showing the emptyRightMenuItem.
+            emptyRightMenuItem.setVisible(ViewConfiguration.get(this).hasPermanentMenuKey());
+        } else {
+            // This is when the user is looking at the dialer pad.  In this case, the real
+            // ActionBar is hidden and fake menu items are shown.
+            // Except in landscape, in which case the real search menu item is shown.
+            searchMenuItem.setVisible(ContactsUtils.isLandscape(this));
+            // If a permanent menu key is available, then we need to show the call settings item
+            // so that the call settings item can be invoked by the permanent menu key.
+            callSettingsMenuItem.setVisible(ViewConfiguration.get(this).hasPermanentMenuKey());
+            emptyRightMenuItem.setVisible(false);
+        }
+    }
+
+    private void prepareOptionsMenuForCallLogTab(Menu menu) {
+        // get references to menu items
+        final MenuItem searchMenuItem = menu.findItem(R.id.search_on_action_bar);
+        final MenuItem filterOptionMenuItem = menu.findItem(R.id.filter_option);
+        final MenuItem addContactOptionMenuItem = menu.findItem(R.id.add_contact);
+        final MenuItem callSettingsMenuItem = menu.findItem(R.id.menu_call_settings);
+        final MenuItem emptyRightMenuItem = menu.findItem(R.id.empty_right_menu_item);
+
+        // prepare the menu items
+        searchMenuItem.setVisible(true);
+        filterOptionMenuItem.setVisible(false);
+        addContactOptionMenuItem.setVisible(false);
+        callSettingsMenuItem.setVisible(true);
+        emptyRightMenuItem.setVisible(ViewConfiguration.get(this).hasPermanentMenuKey());
+    }
+
+    private void prepareOptionsMenuForFavoritesTab(Menu menu) {
+        // get references to menu items
+        final MenuItem searchMenuItem = menu.findItem(R.id.search_on_action_bar);
+        final MenuItem filterOptionMenuItem = menu.findItem(R.id.filter_option);
+        final MenuItem addContactOptionMenuItem = menu.findItem(R.id.add_contact);
+        final MenuItem callSettingsMenuItem = menu.findItem(R.id.menu_call_settings);
+        final MenuItem emptyRightMenuItem = menu.findItem(R.id.empty_right_menu_item);
+
+        // prepare the menu items
+        searchMenuItem.setVisible(true);
+        filterOptionMenuItem.setVisible(true);
+        addContactOptionMenuItem.setVisible(true);
+        callSettingsMenuItem.setVisible(true);
+        emptyRightMenuItem.setVisible(false);
+    }
+
+    @Override
+    public void startSearch(String initialQuery, boolean selectInitialQuery,
+            Bundle appSearchData, boolean globalSearch) {
+        if (mSearchFragment != null && mSearchFragment.isAdded() && !globalSearch) {
+            if (mInSearchUi) {
+                if (mSearchView.hasFocus()) {
+                    showInputMethod(mSearchView.findFocus());
+                } else {
+                    mSearchView.requestFocus();
+                }
+            } else {
+                enterSearchUi();
+            }
+        } else {
+            super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
+        }
+    }
+
+    /**
+     * Hides every tab and shows search UI for phone lookup.
+     */
+    private void enterSearchUi() {
+        if (mSearchFragment == null) {
+            // We add the search fragment dynamically in the first onLayoutChange() and
+            // mSearchFragment is set sometime later when the fragment transaction is actually
+            // executed, which means there's a window when users are able to hit the (physical)
+            // search key but mSearchFragment is still null.
+            // It's quite hard to handle this case right, so let's just ignore the search key
+            // in this case.  Users can just hit it again and it will work this time.
+            return;
+        }
+        if (mSearchView == null) {
+            prepareSearchView();
+        }
+
+        final ActionBar actionBar = getActionBar();
+
+        final Tab tab = actionBar.getSelectedTab();
+
+        // User can search during the call, but we don't want to remember the status.
+        if (tab != null && !DialpadFragment.phoneIsInUse()) {
+            mLastManuallySelectedFragment = tab.getPosition();
+        }
+
+        mSearchView.setQuery(null, true);
+
+        actionBar.setDisplayShowCustomEnabled(true);
+        actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+        actionBar.setDisplayShowHomeEnabled(true);
+        actionBar.setDisplayHomeAsUpEnabled(true);
+
+        updateFakeMenuButtonsVisibility(false);
+
+        for (int i = 0; i < TAB_INDEX_COUNT; i++) {
+            sendFragmentVisibilityChange(i, false /* not visible */ );
+        }
+
+        // Show the search fragment and hide everything else.
+        mSearchFragment.setUserVisibleHint(true);
+        final FragmentTransaction transaction = getFragmentManager().beginTransaction();
+        transaction.show(mSearchFragment);
+        transaction.commitAllowingStateLoss();
+        mViewPager.setVisibility(View.GONE);
+
+        // We need to call this and onActionViewCollapsed() manually, since we are using a custom
+        // layout instead of asking the search menu item to take care of SearchView.
+        mSearchView.onActionViewExpanded();
+        mInSearchUi = true;
+    }
+
+    private void showInputMethod(View view) {
+        InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
+        if (imm != null) {
+            if (!imm.showSoftInput(view, 0)) {
+                Log.w(TAG, "Failed to show soft input method.");
+            }
+        }
+    }
+
+    private void hideInputMethod(View view) {
+        InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
+        if (imm != null && view != null) {
+            imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
+        }
+    }
+
+    /**
+     * Goes back to usual Phone UI with tags. Previously selected Tag and associated Fragment
+     * should be automatically focused again.
+     */
+    private void exitSearchUi() {
+        final ActionBar actionBar = getActionBar();
+
+        // Hide the search fragment, if exists.
+        if (mSearchFragment != null) {
+            mSearchFragment.setUserVisibleHint(false);
+
+            final FragmentTransaction transaction = getFragmentManager().beginTransaction();
+            transaction.hide(mSearchFragment);
+            transaction.commitAllowingStateLoss();
+        }
+
+        // We want to hide SearchView and show Tabs. Also focus on previously selected one.
+        actionBar.setDisplayShowCustomEnabled(false);
+        actionBar.setDisplayShowHomeEnabled(false);
+        actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+
+        for (int i = 0; i < TAB_INDEX_COUNT; i++) {
+            sendFragmentVisibilityChange(i, i == mViewPager.getCurrentItem());
+        }
+
+        // Before exiting the search screen, reset swipe state.
+        mDuringSwipe = false;
+        mUserTabClick = false;
+
+        mViewPager.setVisibility(View.VISIBLE);
+
+        hideInputMethod(getCurrentFocus());
+
+        // Request to update option menu.
+        invalidateOptionsMenu();
+
+        // See comments in onActionViewExpanded()
+        mSearchView.onActionViewCollapsed();
+        mInSearchUi = false;
+    }
+
+    private Fragment getFragmentAt(int position) {
+        switch (position) {
+            case TAB_INDEX_DIALER:
+                return mDialpadFragment;
+            case TAB_INDEX_CALL_LOG:
+                return mCallLogFragment;
+            case TAB_INDEX_FAVORITES:
+                return mPhoneFavoriteFragment;
+            default:
+                throw new IllegalStateException("Unknown fragment index: " + position);
+        }
+    }
+
+    private void sendFragmentVisibilityChange(int position, boolean visibility) {
+        if (DEBUG) {
+            Log.d(TAG, "sendFragmentVisibiltyChange(). position: " + position
+                    + ", visibility: " + visibility);
+        }
+        // Position can be -1 initially. See PageChangeListener.
+        if (position >= 0) {
+            final Fragment fragment = getFragmentAt(position);
+            if (fragment != null) {
+                fragment.setMenuVisibility(visibility);
+                fragment.setUserVisibleHint(visibility);
+            }
+        }
+    }
+
+    /**
+     * Update visibility of the search button and menu button at the bottom.
+     * They should be invisible when bottom ActionBar's real items are available, and be visible
+     * otherwise.
+     *
+     * @param visible True when visible.
+     */
+    private void updateFakeMenuButtonsVisibility(boolean visible) {
+        // Note: Landscape mode does not have the fake menu and search buttons.
+        if (DEBUG) {
+            Log.d(TAG, "updateFakeMenuButtonVisibility(" + visible + ")");
+        }
+
+        if (mSearchButton != null) {
+            if (visible) {
+                mSearchButton.setVisibility(View.VISIBLE);
+            } else {
+                mSearchButton.setVisibility(View.INVISIBLE);
+            }
+        }
+        if (mMenuButton != null) {
+            if (visible && !ViewConfiguration.get(this).hasPermanentMenuKey()) {
+                mMenuButton.setVisibility(View.VISIBLE);
+            } else {
+                mMenuButton.setVisibility(View.INVISIBLE);
+            }
+        }
+    }
+
+    /** Returns an Intent to launch Call Settings screen */
+    public static Intent getCallSettingsIntent() {
+        final Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.setClassName(PHONE_PACKAGE, CALL_SETTINGS_CLASS_NAME);
+        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        return intent;
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (resultCode != Activity.RESULT_OK) {
+            return;
+        }
+        switch (requestCode) {
+            case SUBACTIVITY_ACCOUNT_FILTER: {
+                AccountFilterUtil.handleAccountFilterResult(
+                        mContactListFilterController, resultCode, data);
+            }
+            break;
+        }
+    }
+}
diff --git a/src/com/android/dialer/NonPhoneActivity.java b/src/com/android/dialer/NonPhoneActivity.java
new file mode 100644
index 0000000..c7a744e
--- /dev/null
+++ b/src/com/android/dialer/NonPhoneActivity.java
@@ -0,0 +1,109 @@
+/*
+ * 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.dialer;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Intents.Insert;
+import android.text.TextUtils;
+
+import com.android.contacts.ContactsActivity;
+import com.android.contacts.R;
+import com.android.contacts.util.Constants;
+
+/**
+ * Activity that intercepts DIAL and VIEW intents for phone numbers for devices that can not
+ * be used as a phone. This allows the user to see the phone number
+ */
+public class NonPhoneActivity extends ContactsActivity {
+
+    private static final String PHONE_NUMBER_KEY = "PHONE_NUMBER";
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        final String phoneNumber = getPhoneNumber();
+        if (TextUtils.isEmpty(phoneNumber)) {
+            finish();
+            return;
+        }
+
+        final NonPhoneDialogFragment fragment = new NonPhoneDialogFragment();
+        Bundle bundle = new Bundle();
+        bundle.putString(PHONE_NUMBER_KEY, phoneNumber);
+        fragment.setArguments(bundle);
+        getFragmentManager().beginTransaction().add(fragment, "Fragment").commitAllowingStateLoss();
+    }
+
+    private String getPhoneNumber() {
+        if (getIntent() == null) return null;
+        final Uri data = getIntent().getData();
+        if (data == null) return null;
+        final String scheme = data.getScheme();
+        if (!Constants.SCHEME_TEL.equals(scheme)) return null;
+        return getIntent().getData().getSchemeSpecificPart();
+    }
+
+    public static final class NonPhoneDialogFragment extends DialogFragment
+            implements OnClickListener {
+        @Override
+        public Dialog onCreateDialog(Bundle savedInstanceState) {
+            final AlertDialog alertDialog;
+            alertDialog = new AlertDialog.Builder(getActivity(), R.style.NonPhoneDialogTheme)
+                    .create();
+            alertDialog.setTitle(R.string.non_phone_caption);
+            alertDialog.setMessage(getArgumentPhoneNumber());
+            alertDialog.setButton(DialogInterface.BUTTON_POSITIVE,
+                    getActivity().getString(R.string.non_phone_add_to_contacts), this);
+            alertDialog.setButton(DialogInterface.BUTTON_NEGATIVE,
+                    getActivity().getString(R.string.non_phone_close), this);
+            return alertDialog;
+        }
+
+        @Override
+        public void onClick(DialogInterface dialog, int which) {
+            if (which == DialogInterface.BUTTON_POSITIVE) {
+                final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
+                intent.setType(Contacts.CONTENT_ITEM_TYPE);
+                intent.putExtra(Insert.PHONE, getArgumentPhoneNumber());
+                startActivity(intent);
+            }
+            dismiss();
+        }
+
+        private String getArgumentPhoneNumber() {
+            return getArguments().getString(PHONE_NUMBER_KEY);
+        }
+
+        @Override
+        public void onDismiss(DialogInterface dialog) {
+            super.onDismiss(dialog);
+            // During screen rotation, getActivity returns null. In this case we do not
+            // want to close the Activity anyway
+            final Activity activity = getActivity();
+            if (activity != null) activity.finish();
+        }
+    }
+}
diff --git a/src/com/android/dialer/PhoneCallDetails.java b/src/com/android/dialer/PhoneCallDetails.java
new file mode 100644
index 0000000..45c29e4
--- /dev/null
+++ b/src/com/android/dialer/PhoneCallDetails.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2011 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.dialer;
+
+import android.net.Uri;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+
+/**
+ * The details of a phone call to be shown in the UI.
+ */
+public class PhoneCallDetails {
+    /** The number of the other party involved in the call. */
+    public final CharSequence number;
+    /** The formatted version of {@link #number}. */
+    public final CharSequence formattedNumber;
+    /** The country corresponding with the phone number. */
+    public final String countryIso;
+    /** The geocoded location for the phone number. */
+    public final String geocode;
+    /**
+     * The type of calls, as defined in the call log table, e.g., {@link Calls#INCOMING_TYPE}.
+     * <p>
+     * There might be multiple types if this represents a set of entries grouped together.
+     */
+    public final int[] callTypes;
+    /** The date of the call, in milliseconds since the epoch. */
+    public final long date;
+    /** The duration of the call in milliseconds, or 0 for missed calls. */
+    public final long duration;
+    /** The name of the contact, or the empty string. */
+    public final CharSequence name;
+    /** The type of phone, e.g., {@link Phone#TYPE_HOME}, 0 if not available. */
+    public final int numberType;
+    /** The custom label associated with the phone number in the contact, or the empty string. */
+    public final CharSequence numberLabel;
+    /** The URI of the contact associated with this phone call. */
+    public final Uri contactUri;
+    /**
+     * The photo URI of the picture of the contact that is associated with this phone call or
+     * null if there is none.
+     * <p>
+     * This is meant to store the high-res photo only.
+     */
+    public final Uri photoUri;
+
+    /** Create the details for a call with a number not associated with a contact. */
+    public PhoneCallDetails(CharSequence number, CharSequence formattedNumber,
+            String countryIso, String geocode, int[] callTypes, long date, long duration) {
+        this(number, formattedNumber, countryIso, geocode, callTypes, date, duration, "", 0, "",
+                null, null);
+    }
+
+    /** Create the details for a call with a number associated with a contact. */
+    public PhoneCallDetails(CharSequence number, CharSequence formattedNumber,
+            String countryIso, String geocode, int[] callTypes, long date, long duration,
+            CharSequence name, int numberType, CharSequence numberLabel, Uri contactUri,
+            Uri photoUri) {
+        this.number = number;
+        this.formattedNumber = formattedNumber;
+        this.countryIso = countryIso;
+        this.geocode = geocode;
+        this.callTypes = callTypes;
+        this.date = date;
+        this.duration = duration;
+        this.name = name;
+        this.numberType = numberType;
+        this.numberLabel = numberLabel;
+        this.contactUri = contactUri;
+        this.photoUri = photoUri;
+    }
+}
diff --git a/src/com/android/dialer/PhoneCallDetailsHelper.java b/src/com/android/dialer/PhoneCallDetailsHelper.java
new file mode 100644
index 0000000..8433ebc
--- /dev/null
+++ b/src/com/android/dialer/PhoneCallDetailsHelper.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2011 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.dialer;
+
+import android.content.res.Resources;
+import android.graphics.Typeface;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.telephony.PhoneNumberUtils;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.contacts.R;
+import com.android.dialer.calllog.CallTypeHelper;
+import com.android.dialer.calllog.PhoneNumberHelper;
+import com.android.contacts.test.NeededForTesting;
+
+/**
+ * Helper class to fill in the views in {@link PhoneCallDetailsViews}.
+ */
+public class PhoneCallDetailsHelper {
+    /** The maximum number of icons will be shown to represent the call types in a group. */
+    private static final int MAX_CALL_TYPE_ICONS = 3;
+
+    private final Resources mResources;
+    /** The injected current time in milliseconds since the epoch. Used only by tests. */
+    private Long mCurrentTimeMillisForTest;
+    // Helper classes.
+    private final CallTypeHelper mCallTypeHelper;
+    private final PhoneNumberHelper mPhoneNumberHelper;
+
+    /**
+     * Creates a new instance of the helper.
+     * <p>
+     * Generally you should have a single instance of this helper in any context.
+     *
+     * @param resources used to look up strings
+     */
+    public PhoneCallDetailsHelper(Resources resources, CallTypeHelper callTypeHelper,
+            PhoneNumberHelper phoneNumberHelper) {
+        mResources = resources;
+        mCallTypeHelper = callTypeHelper;
+        mPhoneNumberHelper = phoneNumberHelper;
+    }
+
+    /** Fills the call details views with content. */
+    public void setPhoneCallDetails(PhoneCallDetailsViews views, PhoneCallDetails details,
+            boolean isHighlighted) {
+        // Display up to a given number of icons.
+        views.callTypeIcons.clear();
+        int count = details.callTypes.length;
+        for (int index = 0; index < count && index < MAX_CALL_TYPE_ICONS; ++index) {
+            views.callTypeIcons.add(details.callTypes[index]);
+        }
+        views.callTypeIcons.setVisibility(View.VISIBLE);
+
+        // Show the total call count only if there are more than the maximum number of icons.
+        final Integer callCount;
+        if (count > MAX_CALL_TYPE_ICONS) {
+            callCount = count;
+        } else {
+            callCount = null;
+        }
+        // The color to highlight the count and date in, if any. This is based on the first call.
+        Integer highlightColor =
+                isHighlighted ? mCallTypeHelper.getHighlightedColor(details.callTypes[0]) : null;
+
+        // The date of this call, relative to the current time.
+        CharSequence dateText =
+            DateUtils.getRelativeTimeSpanString(details.date,
+                    getCurrentTimeMillis(),
+                    DateUtils.MINUTE_IN_MILLIS,
+                    DateUtils.FORMAT_ABBREV_RELATIVE);
+
+        // Set the call count and date.
+        setCallCountAndDate(views, callCount, dateText, highlightColor);
+
+        CharSequence numberFormattedLabel = null;
+        // Only show a label if the number is shown and it is not a SIP address.
+        if (!TextUtils.isEmpty(details.number)
+                && !PhoneNumberUtils.isUriNumber(details.number.toString())) {
+            numberFormattedLabel = Phone.getTypeLabel(mResources, details.numberType,
+                    details.numberLabel);
+        }
+
+        final CharSequence nameText;
+        final CharSequence numberText;
+        final CharSequence labelText;
+        final CharSequence displayNumber =
+            mPhoneNumberHelper.getDisplayNumber(details.number, details.formattedNumber);
+        if (TextUtils.isEmpty(details.name)) {
+            nameText = displayNumber;
+            if (TextUtils.isEmpty(details.geocode)
+                    || mPhoneNumberHelper.isVoicemailNumber(details.number)) {
+                numberText = mResources.getString(R.string.call_log_empty_gecode);
+            } else {
+                numberText = details.geocode;
+            }
+            labelText = null;
+        } else {
+            nameText = details.name;
+            numberText = displayNumber;
+            labelText = numberFormattedLabel;
+        }
+
+        views.nameView.setText(nameText);
+        views.numberView.setText(numberText);
+        views.labelView.setText(labelText);
+        views.labelView.setVisibility(TextUtils.isEmpty(labelText) ? View.GONE : View.VISIBLE);
+    }
+
+    /** Sets the text of the header view for the details page of a phone call. */
+    public void setCallDetailsHeader(TextView nameView, PhoneCallDetails details) {
+        final CharSequence nameText;
+        final CharSequence displayNumber =
+                mPhoneNumberHelper.getDisplayNumber(details.number,
+                        mResources.getString(R.string.recentCalls_addToContact));
+        if (TextUtils.isEmpty(details.name)) {
+            nameText = displayNumber;
+        } else {
+            nameText = details.name;
+        }
+
+        nameView.setText(nameText);
+    }
+
+    @NeededForTesting
+    public void setCurrentTimeForTest(long currentTimeMillis) {
+        mCurrentTimeMillisForTest = currentTimeMillis;
+    }
+
+    /**
+     * Returns the current time in milliseconds since the epoch.
+     * <p>
+     * It can be injected in tests using {@link #setCurrentTimeForTest(long)}.
+     */
+    private long getCurrentTimeMillis() {
+        if (mCurrentTimeMillisForTest == null) {
+            return System.currentTimeMillis();
+        } else {
+            return mCurrentTimeMillisForTest;
+        }
+    }
+
+    /** Sets the call count and date. */
+    private void setCallCountAndDate(PhoneCallDetailsViews views, Integer callCount,
+            CharSequence dateText, Integer highlightColor) {
+        // Combine the count (if present) and the date.
+        final CharSequence text;
+        if (callCount != null) {
+            text = mResources.getString(
+                    R.string.call_log_item_count_and_date, callCount.intValue(), dateText);
+        } else {
+            text = dateText;
+        }
+
+        // Apply the highlight color if present.
+        final CharSequence formattedText;
+        if (highlightColor != null) {
+            formattedText = addBoldAndColor(text, highlightColor);
+        } else {
+            formattedText = text;
+        }
+
+        views.callTypeAndDate.setText(formattedText);
+    }
+
+    /** Creates a SpannableString for the given text which is bold and in the given color. */
+    private CharSequence addBoldAndColor(CharSequence text, int color) {
+        int flags = Spanned.SPAN_INCLUSIVE_INCLUSIVE;
+        SpannableString result = new SpannableString(text);
+        result.setSpan(new StyleSpan(Typeface.BOLD), 0, text.length(), flags);
+        result.setSpan(new ForegroundColorSpan(color), 0, text.length(), flags);
+        return result;
+    }
+}
diff --git a/src/com/android/dialer/PhoneCallDetailsViews.java b/src/com/android/dialer/PhoneCallDetailsViews.java
new file mode 100644
index 0000000..5824658
--- /dev/null
+++ b/src/com/android/dialer/PhoneCallDetailsViews.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2011 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.dialer;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.contacts.R;
+import com.android.dialer.calllog.CallTypeIconsView;
+
+/**
+ * Encapsulates the views that are used to display the details of a phone call in the call log.
+ */
+public final class PhoneCallDetailsViews {
+    public final TextView nameView;
+    public final View callTypeView;
+    public final CallTypeIconsView callTypeIcons;
+    public final TextView callTypeAndDate;
+    public final TextView numberView;
+    public final TextView labelView;
+
+    private PhoneCallDetailsViews(TextView nameView, View callTypeView,
+            CallTypeIconsView callTypeIcons, TextView callTypeAndDate, TextView numberView,
+            TextView labelView) {
+        this.nameView = nameView;
+        this.callTypeView = callTypeView;
+        this.callTypeIcons = callTypeIcons;
+        this.callTypeAndDate = callTypeAndDate;
+        this.numberView = numberView;
+        this.labelView = labelView;
+    }
+
+    /**
+     * Create a new instance by extracting the elements from the given view.
+     * <p>
+     * The view should contain three text views with identifiers {@code R.id.name},
+     * {@code R.id.date}, and {@code R.id.number}, and a linear layout with identifier
+     * {@code R.id.call_types}.
+     */
+    public static PhoneCallDetailsViews fromView(View view) {
+        return new PhoneCallDetailsViews((TextView) view.findViewById(R.id.name),
+                view.findViewById(R.id.call_type),
+                (CallTypeIconsView) view.findViewById(R.id.call_type_icons),
+                (TextView) view.findViewById(R.id.call_count_and_date),
+                (TextView) view.findViewById(R.id.number),
+                (TextView) view.findViewById(R.id.label));
+    }
+
+    public static PhoneCallDetailsViews createForTest(Context context) {
+        return new PhoneCallDetailsViews(
+                new TextView(context),
+                new View(context),
+                new CallTypeIconsView(context),
+                new TextView(context),
+                new TextView(context),
+                new TextView(context));
+    }
+}
diff --git a/src/com/android/dialer/ViewNotificationService.java b/src/com/android/dialer/ViewNotificationService.java
new file mode 100644
index 0000000..4fdb815
--- /dev/null
+++ b/src/com/android/dialer/ViewNotificationService.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2012 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.dialer;
+
+import android.app.Service;
+import android.content.Intent;
+import android.content.Loader;
+import android.content.Loader.OnLoadCompleteListener;
+import android.os.IBinder;
+import android.util.Log;
+
+import com.android.contacts.model.Contact;
+import com.android.contacts.model.ContactLoader;
+
+
+/**
+ * Service that sends out a view notification for a contact. At the moment, this is only
+ * supposed to be used by the Phone app
+ */
+public class ViewNotificationService extends Service {
+    private static final String TAG = ViewNotificationService.class.getSimpleName();
+
+    private static final boolean DEBUG = false;
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, final int startId) {
+        if (DEBUG) { Log.d(TAG, "onHandleIntent(). Intent: " + intent); }
+
+        // We simply need to start a Loader here. When its done, it will send out the
+        // View-Notification automatically.
+        final ContactLoader contactLoader = new ContactLoader(this, intent.getData(), true);
+        contactLoader.registerListener(0, new OnLoadCompleteListener<Contact>() {
+            @Override
+            public void onLoadComplete(Loader<Contact> loader, Contact data) {
+                try {
+                    loader.reset();
+                } catch (RuntimeException e) {
+                    Log.e(TAG, "Error reseting loader", e);
+                }
+                try {
+                    // This is not 100% accurate actually. If we get several calls quickly,
+                    // we might be stopping out-of-order, in which case the call with the last
+                    // startId will stop this service. In practice, this shouldn't be a problem,
+                    // as this service is supposed to be called by the Phone app which only sends
+                    // out the notification once per phonecall. And even if there is a problem,
+                    // the worst that should happen is a missing view notification
+                    stopSelfResult(startId);
+                } catch (RuntimeException e) {
+                    Log.e(TAG, "Error stopping service", e);
+                }
+            }
+        });
+        contactLoader.startLoading();
+        return START_REDELIVER_INTENT;
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
+    }
+}
diff --git a/src/com/android/dialer/calllog/CallDetailHistoryAdapter.java b/src/com/android/dialer/calllog/CallDetailHistoryAdapter.java
new file mode 100644
index 0000000..38dc727
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallDetailHistoryAdapter.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import android.content.Context;
+import android.provider.CallLog.Calls;
+import android.text.format.DateUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+import com.android.dialer.PhoneCallDetails;
+import com.android.contacts.R;
+
+/**
+ * Adapter for a ListView containing history items from the details of a call.
+ */
+public class CallDetailHistoryAdapter extends BaseAdapter {
+    /** The top element is a blank header, which is hidden under the rest of the UI. */
+    private static final int VIEW_TYPE_HEADER = 0;
+    /** Each history item shows the detail of a call. */
+    private static final int VIEW_TYPE_HISTORY_ITEM = 1;
+
+    private final Context mContext;
+    private final LayoutInflater mLayoutInflater;
+    private final CallTypeHelper mCallTypeHelper;
+    private final PhoneCallDetails[] mPhoneCallDetails;
+    /** Whether the voicemail controls are shown. */
+    private final boolean mShowVoicemail;
+    /** Whether the call and SMS controls are shown. */
+    private final boolean mShowCallAndSms;
+    /** The controls that are shown on top of the history list. */
+    private final View mControls;
+    /** The listener to changes of focus of the header. */
+    private View.OnFocusChangeListener mHeaderFocusChangeListener =
+            new View.OnFocusChangeListener() {
+        @Override
+        public void onFocusChange(View v, boolean hasFocus) {
+            // When the header is focused, focus the controls above it instead.
+            if (hasFocus) {
+                mControls.requestFocus();
+            }
+        }
+    };
+
+    public CallDetailHistoryAdapter(Context context, LayoutInflater layoutInflater,
+            CallTypeHelper callTypeHelper, PhoneCallDetails[] phoneCallDetails,
+            boolean showVoicemail, boolean showCallAndSms, View controls) {
+        mContext = context;
+        mLayoutInflater = layoutInflater;
+        mCallTypeHelper = callTypeHelper;
+        mPhoneCallDetails = phoneCallDetails;
+        mShowVoicemail = showVoicemail;
+        mShowCallAndSms = showCallAndSms;
+        mControls = controls;
+    }
+
+    @Override
+    public boolean isEnabled(int position) {
+        // None of history will be clickable.
+        return false;
+    }
+
+    @Override
+    public int getCount() {
+        return mPhoneCallDetails.length + 1;
+    }
+
+    @Override
+    public Object getItem(int position) {
+        if (position == 0) {
+            return null;
+        }
+        return mPhoneCallDetails[position - 1];
+    }
+
+    @Override
+    public long getItemId(int position) {
+        if (position == 0) {
+            return -1;
+        }
+        return position - 1;
+    }
+
+    @Override
+    public int getViewTypeCount() {
+        return 2;
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        if (position == 0) {
+            return VIEW_TYPE_HEADER;
+        }
+        return VIEW_TYPE_HISTORY_ITEM;
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        if (position == 0) {
+            final View header = convertView == null
+                    ? mLayoutInflater.inflate(R.layout.call_detail_history_header, parent, false)
+                    : convertView;
+            // Voicemail controls are only shown in the main UI if there is a voicemail.
+            View voicemailContainer = header.findViewById(R.id.header_voicemail_container);
+            voicemailContainer.setVisibility(mShowVoicemail ? View.VISIBLE : View.GONE);
+            // Call and SMS controls are only shown in the main UI if there is a known number.
+            View callAndSmsContainer = header.findViewById(R.id.header_call_and_sms_container);
+            callAndSmsContainer.setVisibility(mShowCallAndSms ? View.VISIBLE : View.GONE);
+            header.setFocusable(true);
+            header.setOnFocusChangeListener(mHeaderFocusChangeListener);
+            return header;
+        }
+
+        // Make sure we have a valid convertView to start with
+        final View result  = convertView == null
+                ? mLayoutInflater.inflate(R.layout.call_detail_history_item, parent, false)
+                : convertView;
+
+        PhoneCallDetails details = mPhoneCallDetails[position - 1];
+        CallTypeIconsView callTypeIconView =
+                (CallTypeIconsView) result.findViewById(R.id.call_type_icon);
+        TextView callTypeTextView = (TextView) result.findViewById(R.id.call_type_text);
+        TextView dateView = (TextView) result.findViewById(R.id.date);
+        TextView durationView = (TextView) result.findViewById(R.id.duration);
+
+        int callType = details.callTypes[0];
+        callTypeIconView.clear();
+        callTypeIconView.add(callType);
+        callTypeTextView.setText(mCallTypeHelper.getCallTypeText(callType));
+        // Set the date.
+        CharSequence dateValue = DateUtils.formatDateRange(mContext, details.date, details.date,
+                DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE |
+                DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_YEAR);
+        dateView.setText(dateValue);
+        // Set the duration
+        if (callType == Calls.MISSED_TYPE || callType == Calls.VOICEMAIL_TYPE) {
+            durationView.setVisibility(View.GONE);
+        } else {
+            durationView.setVisibility(View.VISIBLE);
+            durationView.setText(formatDuration(details.duration));
+        }
+
+        return result;
+    }
+
+    private String formatDuration(long elapsedSeconds) {
+        long minutes = 0;
+        long seconds = 0;
+
+        if (elapsedSeconds >= 60) {
+            minutes = elapsedSeconds / 60;
+            elapsedSeconds -= minutes * 60;
+        }
+        seconds = elapsedSeconds;
+
+        return mContext.getString(R.string.callDetailsDurationFormat, minutes, seconds);
+    }
+}
diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java
new file mode 100644
index 0000000..217f597
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallLogAdapter.java
@@ -0,0 +1,802 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.PhoneLookup;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+
+import com.android.common.widget.GroupingListAdapter;
+import com.android.contacts.ContactPhotoManager;
+import com.android.dialer.PhoneCallDetails;
+import com.android.dialer.PhoneCallDetailsHelper;
+import com.android.contacts.R;
+import com.android.dialer.util.ExpirableCache;
+import com.android.contacts.util.UriUtils;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
+
+import java.util.LinkedList;
+
+/**
+ * Adapter class to fill in data for the Call Log.
+ */
+/*package*/ class CallLogAdapter extends GroupingListAdapter
+        implements ViewTreeObserver.OnPreDrawListener, CallLogGroupBuilder.GroupCreator {
+    /** Interface used to initiate a refresh of the content. */
+    public interface CallFetcher {
+        public void fetchCalls();
+    }
+
+    /**
+     * Stores a phone number of a call with the country code where it originally occurred.
+     * <p>
+     * Note the country does not necessarily specifies the country of the phone number itself, but
+     * it is the country in which the user was in when the call was placed or received.
+     */
+    private static final class NumberWithCountryIso {
+        public final String number;
+        public final String countryIso;
+
+        public NumberWithCountryIso(String number, String countryIso) {
+            this.number = number;
+            this.countryIso = countryIso;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o == null) return false;
+            if (!(o instanceof NumberWithCountryIso)) return false;
+            NumberWithCountryIso other = (NumberWithCountryIso) o;
+            return TextUtils.equals(number, other.number)
+                    && TextUtils.equals(countryIso, other.countryIso);
+        }
+
+        @Override
+        public int hashCode() {
+            return (number == null ? 0 : number.hashCode())
+                    ^ (countryIso == null ? 0 : countryIso.hashCode());
+        }
+    }
+
+    /** The time in millis to delay starting the thread processing requests. */
+    private static final int START_PROCESSING_REQUESTS_DELAY_MILLIS = 1000;
+
+    /** The size of the cache of contact info. */
+    private static final int CONTACT_INFO_CACHE_SIZE = 100;
+
+    private final Context mContext;
+    private final ContactInfoHelper mContactInfoHelper;
+    private final CallFetcher mCallFetcher;
+    private ViewTreeObserver mViewTreeObserver = null;
+
+    /**
+     * A cache of the contact details for the phone numbers in the call log.
+     * <p>
+     * The content of the cache is expired (but not purged) whenever the application comes to
+     * the foreground.
+     * <p>
+     * The key is number with the country in which the call was placed or received.
+     */
+    private ExpirableCache<NumberWithCountryIso, ContactInfo> mContactInfoCache;
+
+    /**
+     * A request for contact details for the given number.
+     */
+    private static final class ContactInfoRequest {
+        /** The number to look-up. */
+        public final String number;
+        /** The country in which a call to or from this number was placed or received. */
+        public final String countryIso;
+        /** The cached contact information stored in the call log. */
+        public final ContactInfo callLogInfo;
+
+        public ContactInfoRequest(String number, String countryIso, ContactInfo callLogInfo) {
+            this.number = number;
+            this.countryIso = countryIso;
+            this.callLogInfo = callLogInfo;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) return true;
+            if (obj == null) return false;
+            if (!(obj instanceof ContactInfoRequest)) return false;
+
+            ContactInfoRequest other = (ContactInfoRequest) obj;
+
+            if (!TextUtils.equals(number, other.number)) return false;
+            if (!TextUtils.equals(countryIso, other.countryIso)) return false;
+            if (!Objects.equal(callLogInfo, other.callLogInfo)) return false;
+
+            return true;
+        }
+
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + ((callLogInfo == null) ? 0 : callLogInfo.hashCode());
+            result = prime * result + ((countryIso == null) ? 0 : countryIso.hashCode());
+            result = prime * result + ((number == null) ? 0 : number.hashCode());
+            return result;
+        }
+    }
+
+    /**
+     * List of requests to update contact details.
+     * <p>
+     * Each request is made of a phone number to look up, and the contact info currently stored in
+     * the call log for this number.
+     * <p>
+     * The requests are added when displaying the contacts and are processed by a background
+     * thread.
+     */
+    private final LinkedList<ContactInfoRequest> mRequests;
+
+    private boolean mLoading = true;
+    private static final int REDRAW = 1;
+    private static final int START_THREAD = 2;
+
+    private QueryThread mCallerIdThread;
+
+    /** Instance of helper class for managing views. */
+    private final CallLogListItemHelper mCallLogViewsHelper;
+
+    /** Helper to set up contact photos. */
+    private final ContactPhotoManager mContactPhotoManager;
+    /** Helper to parse and process phone numbers. */
+    private PhoneNumberHelper mPhoneNumberHelper;
+    /** Helper to group call log entries. */
+    private final CallLogGroupBuilder mCallLogGroupBuilder;
+
+    /** Can be set to true by tests to disable processing of requests. */
+    private volatile boolean mRequestProcessingDisabled = false;
+
+    /** Listener for the primary action in the list, opens the call details. */
+    private final View.OnClickListener mPrimaryActionListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View view) {
+            IntentProvider intentProvider = (IntentProvider) view.getTag();
+            if (intentProvider != null) {
+                mContext.startActivity(intentProvider.getIntent(mContext));
+            }
+        }
+    };
+    /** Listener for the secondary action in the list, either call or play. */
+    private final View.OnClickListener mSecondaryActionListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View view) {
+            IntentProvider intentProvider = (IntentProvider) view.getTag();
+            if (intentProvider != null) {
+                mContext.startActivity(intentProvider.getIntent(mContext));
+            }
+        }
+    };
+
+    @Override
+    public boolean onPreDraw() {
+        // We only wanted to listen for the first draw (and this is it).
+        unregisterPreDrawListener();
+
+        // Only schedule a thread-creation message if the thread hasn't been
+        // created yet. This is purely an optimization, to queue fewer messages.
+        if (mCallerIdThread == null) {
+            mHandler.sendEmptyMessageDelayed(START_THREAD, START_PROCESSING_REQUESTS_DELAY_MILLIS);
+        }
+
+        return true;
+    }
+
+    private Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case REDRAW:
+                    notifyDataSetChanged();
+                    break;
+                case START_THREAD:
+                    startRequestProcessing();
+                    break;
+            }
+        }
+    };
+
+    CallLogAdapter(Context context, CallFetcher callFetcher,
+            ContactInfoHelper contactInfoHelper) {
+        super(context);
+
+        mContext = context;
+        mCallFetcher = callFetcher;
+        mContactInfoHelper = contactInfoHelper;
+
+        mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE);
+        mRequests = new LinkedList<ContactInfoRequest>();
+
+        Resources resources = mContext.getResources();
+        CallTypeHelper callTypeHelper = new CallTypeHelper(resources);
+
+        mContactPhotoManager = ContactPhotoManager.getInstance(mContext);
+        mPhoneNumberHelper = new PhoneNumberHelper(resources);
+        PhoneCallDetailsHelper phoneCallDetailsHelper = new PhoneCallDetailsHelper(
+                resources, callTypeHelper, mPhoneNumberHelper);
+        mCallLogViewsHelper =
+                new CallLogListItemHelper(
+                        phoneCallDetailsHelper, mPhoneNumberHelper, resources);
+        mCallLogGroupBuilder = new CallLogGroupBuilder(this);
+    }
+
+    /**
+     * Requery on background thread when {@link Cursor} changes.
+     */
+    @Override
+    protected void onContentChanged() {
+        mCallFetcher.fetchCalls();
+    }
+
+    void setLoading(boolean loading) {
+        mLoading = loading;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        if (mLoading) {
+            // We don't want the empty state to show when loading.
+            return false;
+        } else {
+            return super.isEmpty();
+        }
+    }
+
+    /**
+     * Starts a background thread to process contact-lookup requests, unless one
+     * has already been started.
+     */
+    private synchronized void startRequestProcessing() {
+        // For unit-testing.
+        if (mRequestProcessingDisabled) return;
+
+        // Idempotence... if a thread is already started, don't start another.
+        if (mCallerIdThread != null) return;
+
+        mCallerIdThread = new QueryThread();
+        mCallerIdThread.setPriority(Thread.MIN_PRIORITY);
+        mCallerIdThread.start();
+    }
+
+    /**
+     * Stops the background thread that processes updates and cancels any
+     * pending requests to start it.
+     */
+    public synchronized void stopRequestProcessing() {
+        // Remove any pending requests to start the processing thread.
+        mHandler.removeMessages(START_THREAD);
+        if (mCallerIdThread != null) {
+            // Stop the thread; we are finished with it.
+            mCallerIdThread.stopProcessing();
+            mCallerIdThread.interrupt();
+            mCallerIdThread = null;
+        }
+    }
+
+    /**
+     * Stop receiving onPreDraw() notifications.
+     */
+    private void unregisterPreDrawListener() {
+        if (mViewTreeObserver != null && mViewTreeObserver.isAlive()) {
+            mViewTreeObserver.removeOnPreDrawListener(this);
+        }
+        mViewTreeObserver = null;
+    }
+
+    public void invalidateCache() {
+        mContactInfoCache.expireAll();
+
+        // Restart the request-processing thread after the next draw.
+        stopRequestProcessing();
+        unregisterPreDrawListener();
+    }
+
+    /**
+     * Enqueues a request to look up the contact details for the given phone number.
+     * <p>
+     * It also provides the current contact info stored in the call log for this number.
+     * <p>
+     * If the {@code immediate} parameter is true, it will start immediately the thread that looks
+     * up the contact information (if it has not been already started). Otherwise, it will be
+     * started with a delay. See {@link #START_PROCESSING_REQUESTS_DELAY_MILLIS}.
+     */
+    @VisibleForTesting
+    void enqueueRequest(String number, String countryIso, ContactInfo callLogInfo,
+            boolean immediate) {
+        ContactInfoRequest request = new ContactInfoRequest(number, countryIso, callLogInfo);
+        synchronized (mRequests) {
+            if (!mRequests.contains(request)) {
+                mRequests.add(request);
+                mRequests.notifyAll();
+            }
+        }
+        if (immediate) startRequestProcessing();
+    }
+
+    /**
+     * Queries the appropriate content provider for the contact associated with the number.
+     * <p>
+     * Upon completion it also updates the cache in the call log, if it is different from
+     * {@code callLogInfo}.
+     * <p>
+     * The number might be either a SIP address or a phone number.
+     * <p>
+     * It returns true if it updated the content of the cache and we should therefore tell the
+     * view to update its content.
+     */
+    private boolean queryContactInfo(String number, String countryIso, ContactInfo callLogInfo) {
+        final ContactInfo info = mContactInfoHelper.lookupNumber(number, countryIso);
+
+        if (info == null) {
+            // The lookup failed, just return without requesting to update the view.
+            return false;
+        }
+
+        // Check the existing entry in the cache: only if it has changed we should update the
+        // view.
+        NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
+        ContactInfo existingInfo = mContactInfoCache.getPossiblyExpired(numberCountryIso);
+        boolean updated = (existingInfo != ContactInfo.EMPTY) && !info.equals(existingInfo);
+
+        // Store the data in the cache so that the UI thread can use to display it. Store it
+        // even if it has not changed so that it is marked as not expired.
+        mContactInfoCache.put(numberCountryIso, info);
+        // Update the call log even if the cache it is up-to-date: it is possible that the cache
+        // contains the value from a different call log entry.
+        updateCallLogContactInfoCache(number, countryIso, info, callLogInfo);
+        return updated;
+    }
+
+    /*
+     * Handles requests for contact name and number type.
+     */
+    private class QueryThread extends Thread {
+        private volatile boolean mDone = false;
+
+        public QueryThread() {
+            super("CallLogAdapter.QueryThread");
+        }
+
+        public void stopProcessing() {
+            mDone = true;
+        }
+
+        @Override
+        public void run() {
+            boolean needRedraw = false;
+            while (true) {
+                // Check if thread is finished, and if so return immediately.
+                if (mDone) return;
+
+                // Obtain next request, if any is available.
+                // Keep synchronized section small.
+                ContactInfoRequest req = null;
+                synchronized (mRequests) {
+                    if (!mRequests.isEmpty()) {
+                        req = mRequests.removeFirst();
+                    }
+                }
+
+                if (req != null) {
+                    // Process the request. If the lookup succeeds, schedule a
+                    // redraw.
+                    needRedraw |= queryContactInfo(req.number, req.countryIso, req.callLogInfo);
+                } else {
+                    // Throttle redraw rate by only sending them when there are
+                    // more requests.
+                    if (needRedraw) {
+                        needRedraw = false;
+                        mHandler.sendEmptyMessage(REDRAW);
+                    }
+
+                    // Wait until another request is available, or until this
+                    // thread is no longer needed (as indicated by being
+                    // interrupted).
+                    try {
+                        synchronized (mRequests) {
+                            mRequests.wait(1000);
+                        }
+                    } catch (InterruptedException ie) {
+                        // Ignore, and attempt to continue processing requests.
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    protected void addGroups(Cursor cursor) {
+        mCallLogGroupBuilder.addGroups(cursor);
+    }
+
+    @Override
+    protected View newStandAloneView(Context context, ViewGroup parent) {
+        LayoutInflater inflater =
+                (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
+        findAndCacheViews(view);
+        return view;
+    }
+
+    @Override
+    protected void bindStandAloneView(View view, Context context, Cursor cursor) {
+        bindView(view, cursor, 1);
+    }
+
+    @Override
+    protected View newChildView(Context context, ViewGroup parent) {
+        LayoutInflater inflater =
+                (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
+        findAndCacheViews(view);
+        return view;
+    }
+
+    @Override
+    protected void bindChildView(View view, Context context, Cursor cursor) {
+        bindView(view, cursor, 1);
+    }
+
+    @Override
+    protected View newGroupView(Context context, ViewGroup parent) {
+        LayoutInflater inflater =
+                (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
+        findAndCacheViews(view);
+        return view;
+    }
+
+    @Override
+    protected void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
+            boolean expanded) {
+        bindView(view, cursor, groupSize);
+    }
+
+    private void findAndCacheViews(View view) {
+        // Get the views to bind to.
+        CallLogListItemViews views = CallLogListItemViews.fromView(view);
+        views.primaryActionView.setOnClickListener(mPrimaryActionListener);
+        views.secondaryActionView.setOnClickListener(mSecondaryActionListener);
+        view.setTag(views);
+    }
+
+    /**
+     * Binds the views in the entry to the data in the call log.
+     *
+     * @param view the view corresponding to this entry
+     * @param c the cursor pointing to the entry in the call log
+     * @param count the number of entries in the current item, greater than 1 if it is a group
+     */
+    private void bindView(View view, Cursor c, int count) {
+        final CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+        final int section = c.getInt(CallLogQuery.SECTION);
+
+        // This might be a header: check the value of the section column in the cursor.
+        if (section == CallLogQuery.SECTION_NEW_HEADER
+                || section == CallLogQuery.SECTION_OLD_HEADER) {
+            views.primaryActionView.setVisibility(View.GONE);
+            views.bottomDivider.setVisibility(View.GONE);
+            views.listHeaderTextView.setVisibility(View.VISIBLE);
+            views.listHeaderTextView.setText(
+                    section == CallLogQuery.SECTION_NEW_HEADER
+                            ? R.string.call_log_new_header
+                            : R.string.call_log_old_header);
+            // Nothing else to set up for a header.
+            return;
+        }
+        // Default case: an item in the call log.
+        views.primaryActionView.setVisibility(View.VISIBLE);
+        views.bottomDivider.setVisibility(isLastOfSection(c) ? View.GONE : View.VISIBLE);
+        views.listHeaderTextView.setVisibility(View.GONE);
+
+        final String number = c.getString(CallLogQuery.NUMBER);
+        final long date = c.getLong(CallLogQuery.DATE);
+        final long duration = c.getLong(CallLogQuery.DURATION);
+        final int callType = c.getInt(CallLogQuery.CALL_TYPE);
+        final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO);
+
+        final ContactInfo cachedContactInfo = getContactInfoFromCallLog(c);
+
+        views.primaryActionView.setTag(
+                IntentProvider.getCallDetailIntentProvider(
+                        this, c.getPosition(), c.getLong(CallLogQuery.ID), count));
+        // Store away the voicemail information so we can play it directly.
+        if (callType == Calls.VOICEMAIL_TYPE) {
+            String voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI);
+            final long rowId = c.getLong(CallLogQuery.ID);
+            views.secondaryActionView.setTag(
+                    IntentProvider.getPlayVoicemailIntentProvider(rowId, voicemailUri));
+        } else if (!TextUtils.isEmpty(number)) {
+            // Store away the number so we can call it directly if you click on the call icon.
+            views.secondaryActionView.setTag(
+                    IntentProvider.getReturnCallIntentProvider(number));
+        } else {
+            // No action enabled.
+            views.secondaryActionView.setTag(null);
+        }
+
+        // Lookup contacts with this number
+        NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
+        ExpirableCache.CachedValue<ContactInfo> cachedInfo =
+                mContactInfoCache.getCachedValue(numberCountryIso);
+        ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue();
+        if (!mPhoneNumberHelper.canPlaceCallsTo(number)
+                || mPhoneNumberHelper.isVoicemailNumber(number)) {
+            // If this is a number that cannot be dialed, there is no point in looking up a contact
+            // for it.
+            info = ContactInfo.EMPTY;
+        } else if (cachedInfo == null) {
+            mContactInfoCache.put(numberCountryIso, ContactInfo.EMPTY);
+            // Use the cached contact info from the call log.
+            info = cachedContactInfo;
+            // The db request should happen on a non-UI thread.
+            // Request the contact details immediately since they are currently missing.
+            enqueueRequest(number, countryIso, cachedContactInfo, true);
+            // We will format the phone number when we make the background request.
+        } else {
+            if (cachedInfo.isExpired()) {
+                // The contact info is no longer up to date, we should request it. However, we
+                // do not need to request them immediately.
+                enqueueRequest(number, countryIso, cachedContactInfo, false);
+            } else  if (!callLogInfoMatches(cachedContactInfo, info)) {
+                // The call log information does not match the one we have, look it up again.
+                // We could simply update the call log directly, but that needs to be done in a
+                // background thread, so it is easier to simply request a new lookup, which will, as
+                // a side-effect, update the call log.
+                enqueueRequest(number, countryIso, cachedContactInfo, false);
+            }
+
+            if (info == ContactInfo.EMPTY) {
+                // Use the cached contact info from the call log.
+                info = cachedContactInfo;
+            }
+        }
+
+        final Uri lookupUri = info.lookupUri;
+        final String name = info.name;
+        final int ntype = info.type;
+        final String label = info.label;
+        final long photoId = info.photoId;
+        CharSequence formattedNumber = info.formattedNumber;
+        final int[] callTypes = getCallTypes(c, count);
+        final String geocode = c.getString(CallLogQuery.GEOCODED_LOCATION);
+        final PhoneCallDetails details;
+        if (TextUtils.isEmpty(name)) {
+            details = new PhoneCallDetails(number, formattedNumber, countryIso, geocode,
+                    callTypes, date, duration);
+        } else {
+            // We do not pass a photo id since we do not need the high-res picture.
+            details = new PhoneCallDetails(number, formattedNumber, countryIso, geocode,
+                    callTypes, date, duration, name, ntype, label, lookupUri, null);
+        }
+
+        final boolean isNew = c.getInt(CallLogQuery.IS_READ) == 0;
+        // New items also use the highlighted version of the text.
+        final boolean isHighlighted = isNew;
+        mCallLogViewsHelper.setPhoneCallDetails(views, details, isHighlighted);
+        setPhoto(views, photoId, lookupUri);
+
+        // Listen for the first draw
+        if (mViewTreeObserver == null) {
+            mViewTreeObserver = view.getViewTreeObserver();
+            mViewTreeObserver.addOnPreDrawListener(this);
+        }
+    }
+
+    /** Returns true if this is the last item of a section. */
+    private boolean isLastOfSection(Cursor c) {
+        if (c.isLast()) return true;
+        final int section = c.getInt(CallLogQuery.SECTION);
+        if (!c.moveToNext()) return true;
+        final int nextSection = c.getInt(CallLogQuery.SECTION);
+        c.moveToPrevious();
+        return section != nextSection;
+    }
+
+    /** Checks whether the contact info from the call log matches the one from the contacts db. */
+    private boolean callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info) {
+        // The call log only contains a subset of the fields in the contacts db.
+        // Only check those.
+        return TextUtils.equals(callLogInfo.name, info.name)
+                && callLogInfo.type == info.type
+                && TextUtils.equals(callLogInfo.label, info.label);
+    }
+
+    /** Stores the updated contact info in the call log if it is different from the current one. */
+    private void updateCallLogContactInfoCache(String number, String countryIso,
+            ContactInfo updatedInfo, ContactInfo callLogInfo) {
+        final ContentValues values = new ContentValues();
+        boolean needsUpdate = false;
+
+        if (callLogInfo != null) {
+            if (!TextUtils.equals(updatedInfo.name, callLogInfo.name)) {
+                values.put(Calls.CACHED_NAME, updatedInfo.name);
+                needsUpdate = true;
+            }
+
+            if (updatedInfo.type != callLogInfo.type) {
+                values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
+                needsUpdate = true;
+            }
+
+            if (!TextUtils.equals(updatedInfo.label, callLogInfo.label)) {
+                values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
+                needsUpdate = true;
+            }
+            if (!UriUtils.areEqual(updatedInfo.lookupUri, callLogInfo.lookupUri)) {
+                values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
+                needsUpdate = true;
+            }
+            if (!TextUtils.equals(updatedInfo.normalizedNumber, callLogInfo.normalizedNumber)) {
+                values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
+                needsUpdate = true;
+            }
+            if (!TextUtils.equals(updatedInfo.number, callLogInfo.number)) {
+                values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
+                needsUpdate = true;
+            }
+            if (updatedInfo.photoId != callLogInfo.photoId) {
+                values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
+                needsUpdate = true;
+            }
+            if (!TextUtils.equals(updatedInfo.formattedNumber, callLogInfo.formattedNumber)) {
+                values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
+                needsUpdate = true;
+            }
+        } else {
+            // No previous values, store all of them.
+            values.put(Calls.CACHED_NAME, updatedInfo.name);
+            values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
+            values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
+            values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
+            values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
+            values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
+            values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
+            values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
+            needsUpdate = true;
+        }
+
+        if (!needsUpdate) return;
+
+        if (countryIso == null) {
+            mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values,
+                    Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " IS NULL",
+                    new String[]{ number });
+        } else {
+            mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values,
+                    Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " = ?",
+                    new String[]{ number, countryIso });
+        }
+    }
+
+    /** Returns the contact information as stored in the call log. */
+    private ContactInfo getContactInfoFromCallLog(Cursor c) {
+        ContactInfo info = new ContactInfo();
+        info.lookupUri = UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_LOOKUP_URI));
+        info.name = c.getString(CallLogQuery.CACHED_NAME);
+        info.type = c.getInt(CallLogQuery.CACHED_NUMBER_TYPE);
+        info.label = c.getString(CallLogQuery.CACHED_NUMBER_LABEL);
+        String matchedNumber = c.getString(CallLogQuery.CACHED_MATCHED_NUMBER);
+        info.number = matchedNumber == null ? c.getString(CallLogQuery.NUMBER) : matchedNumber;
+        info.normalizedNumber = c.getString(CallLogQuery.CACHED_NORMALIZED_NUMBER);
+        info.photoId = c.getLong(CallLogQuery.CACHED_PHOTO_ID);
+        info.photoUri = null;  // We do not cache the photo URI.
+        info.formattedNumber = c.getString(CallLogQuery.CACHED_FORMATTED_NUMBER);
+        return info;
+    }
+
+    /**
+     * Returns the call types for the given number of items in the cursor.
+     * <p>
+     * It uses the next {@code count} rows in the cursor to extract the types.
+     * <p>
+     * It position in the cursor is unchanged by this function.
+     */
+    private int[] getCallTypes(Cursor cursor, int count) {
+        int position = cursor.getPosition();
+        int[] callTypes = new int[count];
+        for (int index = 0; index < count; ++index) {
+            callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE);
+            cursor.moveToNext();
+        }
+        cursor.moveToPosition(position);
+        return callTypes;
+    }
+
+    private void setPhoto(CallLogListItemViews views, long photoId, Uri contactUri) {
+        views.quickContactView.assignContactUri(contactUri);
+        mContactPhotoManager.loadThumbnail(views.quickContactView, photoId, true);
+    }
+
+    /**
+     * Sets whether processing of requests for contact details should be enabled.
+     * <p>
+     * This method should be called in tests to disable such processing of requests when not
+     * needed.
+     */
+    @VisibleForTesting
+    void disableRequestProcessingForTest() {
+        mRequestProcessingDisabled = true;
+    }
+
+    @VisibleForTesting
+    void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) {
+        NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
+        mContactInfoCache.put(numberCountryIso, contactInfo);
+    }
+
+    @Override
+    public void addGroup(int cursorPosition, int size, boolean expanded) {
+        super.addGroup(cursorPosition, size, expanded);
+    }
+
+    /*
+     * Get the number from the Contacts, if available, since sometimes
+     * the number provided by caller id may not be formatted properly
+     * depending on the carrier (roaming) in use at the time of the
+     * incoming call.
+     * Logic : If the caller-id number starts with a "+", use it
+     *         Else if the number in the contacts starts with a "+", use that one
+     *         Else if the number in the contacts is longer, use that one
+     */
+    public String getBetterNumberFromContacts(String number, String countryIso) {
+        String matchingNumber = null;
+        // Look in the cache first. If it's not found then query the Phones db
+        NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
+        ContactInfo ci = mContactInfoCache.getPossiblyExpired(numberCountryIso);
+        if (ci != null && ci != ContactInfo.EMPTY) {
+            matchingNumber = ci.number;
+        } else {
+            try {
+                Cursor phonesCursor = mContext.getContentResolver().query(
+                        Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number),
+                        PhoneQuery._PROJECTION, null, null, null);
+                if (phonesCursor != null) {
+                    if (phonesCursor.moveToFirst()) {
+                        matchingNumber = phonesCursor.getString(PhoneQuery.MATCHED_NUMBER);
+                    }
+                    phonesCursor.close();
+                }
+            } catch (Exception e) {
+                // Use the number from the call log
+            }
+        }
+        if (!TextUtils.isEmpty(matchingNumber) &&
+                (matchingNumber.startsWith("+")
+                        || matchingNumber.length() > number.length())) {
+            number = matchingNumber;
+        }
+        return number;
+    }
+}
diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java
new file mode 100644
index 0000000..4b31134
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallLogFragment.java
@@ -0,0 +1,549 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import android.app.Activity;
+import android.app.KeyguardManager;
+import android.app.ListFragment;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.preference.PreferenceManager;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.android.common.io.MoreCloseables;
+import com.android.contacts.ContactsUtils;
+import com.android.contacts.R;
+import com.android.contacts.util.Constants;
+import com.android.contacts.util.EmptyLoader;
+import com.android.dialer.voicemail.VoicemailStatusHelper;
+import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage;
+import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
+import com.android.internal.telephony.CallerInfo;
+import com.android.internal.telephony.ITelephony;
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.List;
+
+/**
+ * Displays a list of call log entries.
+ */
+public class CallLogFragment extends ListFragment
+        implements CallLogQueryHandler.Listener, CallLogAdapter.CallFetcher {
+    private static final String TAG = "CallLogFragment";
+
+    /**
+     * ID of the empty loader to defer other fragments.
+     */
+    private static final int EMPTY_LOADER_ID = 0;
+
+    private static final String PREF_CALL_LOG_FILTER_LAST_CALL_TYPE = "CallLogFragment_last_filter";
+
+    private CallLogAdapter mAdapter;
+    private CallLogQueryHandler mCallLogQueryHandler;
+    private boolean mScrollToTop;
+
+    /** Whether there is at least one voicemail source installed. */
+    private boolean mVoicemailSourcesAvailable = false;
+    /** Whether we are currently filtering over voicemail. */
+    private boolean mShowingVoicemailOnly = false;
+
+    private VoicemailStatusHelper mVoicemailStatusHelper;
+    private View mStatusMessageView;
+    private TextView mStatusMessageText;
+    private TextView mStatusMessageAction;
+    private TextView mFilterStatusView;
+    private KeyguardManager mKeyguardManager;
+
+    private boolean mEmptyLoaderRunning;
+    private boolean mCallLogFetched;
+    private boolean mVoicemailStatusFetched;
+
+    private final Handler mHandler = new Handler();
+
+    private class CustomContentObserver extends ContentObserver {
+        public CustomContentObserver() {
+            super(mHandler);
+        }
+        @Override
+        public void onChange(boolean selfChange) {
+            mRefreshDataRequired = true;
+        }
+    }
+
+    // See issue 6363009
+    private final ContentObserver mCallLogObserver = new CustomContentObserver();
+    private final ContentObserver mContactsObserver = new CustomContentObserver();
+    private boolean mRefreshDataRequired = true;
+
+    // Exactly same variable is in Fragment as a package private.
+    private boolean mMenuVisible = true;
+
+    // Default to all calls.
+    private int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL;
+
+    @Override
+    public void onCreate(Bundle state) {
+        super.onCreate(state);
+
+        mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(), this);
+        mKeyguardManager =
+                (KeyguardManager) getActivity().getSystemService(Context.KEYGUARD_SERVICE);
+        getActivity().getContentResolver().registerContentObserver(
+                CallLog.CONTENT_URI, true, mCallLogObserver);
+        getActivity().getContentResolver().registerContentObserver(
+                ContactsContract.Contacts.CONTENT_URI, true, mContactsObserver);
+        setHasOptionsMenu(true);
+
+        // Load the last filter used.
+        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
+        mCallTypeFilter = prefs.getInt(PREF_CALL_LOG_FILTER_LAST_CALL_TYPE,
+                CallLogQueryHandler.CALL_TYPE_ALL);
+    }
+
+    /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */
+    @Override
+    public void onCallsFetched(Cursor cursor) {
+        if (getActivity() == null || getActivity().isFinishing()) {
+            return;
+        }
+        mAdapter.setLoading(false);
+        mAdapter.changeCursor(cursor);
+        // This will update the state of the "Clear call log" menu item.
+        getActivity().invalidateOptionsMenu();
+        if (mScrollToTop) {
+            final ListView listView = getListView();
+            // The smooth-scroll animation happens over a fixed time period.
+            // As a result, if it scrolls through a large portion of the list,
+            // each frame will jump so far from the previous one that the user
+            // will not experience the illusion of downward motion.  Instead,
+            // if we're not already near the top of the list, we instantly jump
+            // near the top, and animate from there.
+            if (listView.getFirstVisiblePosition() > 5) {
+                listView.setSelection(5);
+            }
+            // Workaround for framework issue: the smooth-scroll doesn't
+            // occur if setSelection() is called immediately before.
+            mHandler.post(new Runnable() {
+               @Override
+               public void run() {
+                   if (getActivity() == null || getActivity().isFinishing()) {
+                       return;
+                   }
+                   listView.smoothScrollToPosition(0);
+               }
+            });
+
+            mScrollToTop = false;
+        }
+        mCallLogFetched = true;
+        destroyEmptyLoaderIfAllDataFetched();
+    }
+
+    /**
+     * Called by {@link CallLogQueryHandler} after a successful query to voicemail status provider.
+     */
+    @Override
+    public void onVoicemailStatusFetched(Cursor statusCursor) {
+        if (getActivity() == null || getActivity().isFinishing()) {
+            return;
+        }
+        updateVoicemailStatusMessage(statusCursor);
+
+        int activeSources = mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor);
+        setVoicemailSourcesAvailable(activeSources != 0);
+        MoreCloseables.closeQuietly(statusCursor);
+        mVoicemailStatusFetched = true;
+        destroyEmptyLoaderIfAllDataFetched();
+    }
+
+    private void destroyEmptyLoaderIfAllDataFetched() {
+        if (mCallLogFetched && mVoicemailStatusFetched && mEmptyLoaderRunning) {
+            mEmptyLoaderRunning = false;
+            getLoaderManager().destroyLoader(EMPTY_LOADER_ID);
+        }
+    }
+
+    /** Sets whether there are any voicemail sources available in the platform. */
+    private void setVoicemailSourcesAvailable(boolean voicemailSourcesAvailable) {
+        if (mVoicemailSourcesAvailable == voicemailSourcesAvailable) return;
+        mVoicemailSourcesAvailable = voicemailSourcesAvailable;
+
+        Activity activity = getActivity();
+        if (activity != null) {
+            // This is so that the options menu content is updated.
+            activity.invalidateOptionsMenu();
+        }
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
+        View view = inflater.inflate(R.layout.call_log_fragment, container, false);
+        mVoicemailStatusHelper = new VoicemailStatusHelperImpl();
+        mStatusMessageView = view.findViewById(R.id.voicemail_status);
+        mStatusMessageText = (TextView) view.findViewById(R.id.voicemail_status_message);
+        mStatusMessageAction = (TextView) view.findViewById(R.id.voicemail_status_action);
+        mFilterStatusView = (TextView) view.findViewById(R.id.filter_status);
+        return view;
+    }
+
+    @Override
+    public void onViewCreated(View view, Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        String currentCountryIso = ContactsUtils.getCurrentCountryIso(getActivity());
+        mAdapter = new CallLogAdapter(getActivity(), this,
+                new ContactInfoHelper(getActivity(), currentCountryIso));
+        setListAdapter(mAdapter);
+        getListView().setItemsCanFocus(true);
+
+        updateFilterHeader();
+    }
+
+    /**
+     * Based on the new intent, decide whether the list should be configured
+     * to scroll up to display the first item.
+     */
+    public void configureScreenFromIntent(Intent newIntent) {
+        // Typically, when switching to the call-log we want to show the user
+        // the same section of the list that they were most recently looking
+        // at.  However, under some circumstances, we want to automatically
+        // scroll to the top of the list to present the newest call items.
+        // For example, immediately after a call is finished, we want to
+        // display information about that call.
+        mScrollToTop = Calls.CONTENT_TYPE.equals(newIntent.getType());
+    }
+
+    @Override
+    public void onStart() {
+        // Start the empty loader now to defer other fragments.  We destroy it when both calllog
+        // and the voicemail status are fetched.
+        getLoaderManager().initLoader(EMPTY_LOADER_ID, null,
+                new EmptyLoader.Callback(getActivity()));
+        mEmptyLoaderRunning = true;
+        super.onStart();
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        refreshData();
+    }
+
+    private void updateVoicemailStatusMessage(Cursor statusCursor) {
+        List<StatusMessage> messages = mVoicemailStatusHelper.getStatusMessages(statusCursor);
+        if (messages.size() == 0) {
+            mStatusMessageView.setVisibility(View.GONE);
+        } else {
+            mStatusMessageView.setVisibility(View.VISIBLE);
+            // TODO: Change the code to show all messages. For now just pick the first message.
+            final StatusMessage message = messages.get(0);
+            if (message.showInCallLog()) {
+                mStatusMessageText.setText(message.callLogMessageId);
+            }
+            if (message.actionMessageId != -1) {
+                mStatusMessageAction.setText(message.actionMessageId);
+            }
+            if (message.actionUri != null) {
+                mStatusMessageAction.setVisibility(View.VISIBLE);
+                mStatusMessageAction.setOnClickListener(new View.OnClickListener() {
+                    @Override
+                    public void onClick(View v) {
+                        getActivity().startActivity(
+                                new Intent(Intent.ACTION_VIEW, message.actionUri));
+                    }
+                });
+            } else {
+                mStatusMessageAction.setVisibility(View.GONE);
+            }
+        }
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        // Kill the requests thread
+        mAdapter.stopRequestProcessing();
+
+        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
+        prefs.edit()
+                .putInt(PREF_CALL_LOG_FILTER_LAST_CALL_TYPE, mCallTypeFilter)
+                .apply();
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        updateOnExit();
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        mAdapter.stopRequestProcessing();
+        mAdapter.changeCursor(null);
+        getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver);
+        getActivity().getContentResolver().unregisterContentObserver(mContactsObserver);
+    }
+
+    @Override
+    public void fetchCalls() {
+        mCallLogQueryHandler.fetchCalls(mCallTypeFilter);
+    }
+
+    public void startCallsQuery() {
+        mAdapter.setLoading(true);
+        mCallLogQueryHandler.fetchCalls(mCallTypeFilter);
+        if (mShowingVoicemailOnly) {
+            mShowingVoicemailOnly = false;
+            getActivity().invalidateOptionsMenu();
+        }
+    }
+
+    private void startVoicemailStatusQuery() {
+        mCallLogQueryHandler.fetchVoicemailStatus();
+    }
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        super.onCreateOptionsMenu(menu, inflater);
+        inflater.inflate(R.menu.call_log_options, menu);
+    }
+
+    @Override
+    public void onPrepareOptionsMenu(Menu menu) {
+        final MenuItem itemDeleteAll = menu.findItem(R.id.delete_all);
+        // Check if all the menu items are inflated correctly. As a shortcut, we assume all
+        // menu items are ready if the first item is non-null.
+        if (itemDeleteAll != null) {
+            itemDeleteAll.setEnabled(mAdapter != null && !mAdapter.isEmpty());
+            menu.findItem(R.id.show_voicemails_only).setVisible(mVoicemailSourcesAvailable);
+        }
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.delete_all:
+                ClearCallLogDialog.show(getFragmentManager());
+                return true;
+
+            case R.id.show_outgoing_only:
+                mCallLogQueryHandler.fetchCalls(Calls.OUTGOING_TYPE);
+                mCallTypeFilter = Calls.OUTGOING_TYPE;
+                updateFilterHeader();
+                return true;
+
+            case R.id.show_incoming_only:
+                mCallLogQueryHandler.fetchCalls(Calls.INCOMING_TYPE);
+                mCallTypeFilter = Calls.INCOMING_TYPE;
+                updateFilterHeader();
+                return true;
+
+            case R.id.show_missed_only:
+                mCallLogQueryHandler.fetchCalls(Calls.MISSED_TYPE);
+                mCallTypeFilter = Calls.MISSED_TYPE;
+                updateFilterHeader();
+                return true;
+
+            case R.id.show_voicemails_only:
+                mCallLogQueryHandler.fetchCalls(Calls.VOICEMAIL_TYPE);
+                mCallTypeFilter = Calls.VOICEMAIL_TYPE;
+                updateFilterHeader();
+                mShowingVoicemailOnly = true;
+                return true;
+
+            case R.id.show_all_calls:
+                mCallLogQueryHandler.fetchCalls(CallLogQueryHandler.CALL_TYPE_ALL);
+                mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL;
+                updateFilterHeader();
+                mShowingVoicemailOnly = false;
+                return true;
+
+            default:
+                return false;
+        }
+    }
+
+    private void updateFilterHeader() {
+        switch (mCallTypeFilter) {
+            case CallLogQueryHandler.CALL_TYPE_ALL:
+                mFilterStatusView.setVisibility(View.GONE);
+                break;
+            case Calls.INCOMING_TYPE:
+                showFilterStatus(R.string.call_log_incoming_header);
+                break;
+            case Calls.OUTGOING_TYPE:
+                showFilterStatus(R.string.call_log_outgoing_header);
+                break;
+            case Calls.MISSED_TYPE:
+                showFilterStatus(R.string.call_log_missed_header);
+                break;
+            case Calls.VOICEMAIL_TYPE:
+                showFilterStatus(R.string.call_log_voicemail_header);
+                break;
+        }
+    }
+
+    private void showFilterStatus(int resId) {
+        mFilterStatusView.setText(resId);
+        mFilterStatusView.setVisibility(View.VISIBLE);
+    }
+
+    public void callSelectedEntry() {
+        int position = getListView().getSelectedItemPosition();
+        if (position < 0) {
+            // In touch mode you may often not have something selected, so
+            // just call the first entry to make sure that [send] [send] calls the
+            // most recent entry.
+            position = 0;
+        }
+        final Cursor cursor = (Cursor)mAdapter.getItem(position);
+        if (cursor != null) {
+            String number = cursor.getString(CallLogQuery.NUMBER);
+            if (TextUtils.isEmpty(number)
+                    || number.equals(CallerInfo.UNKNOWN_NUMBER)
+                    || number.equals(CallerInfo.PRIVATE_NUMBER)
+                    || number.equals(CallerInfo.PAYPHONE_NUMBER)) {
+                // This number can't be called, do nothing
+                return;
+            }
+            Intent intent;
+            // If "number" is really a SIP address, construct a sip: URI.
+            if (PhoneNumberUtils.isUriNumber(number)) {
+                intent = ContactsUtils.getCallIntent(
+                        Uri.fromParts(Constants.SCHEME_SIP, number, null));
+            } else {
+                // We're calling a regular PSTN phone number.
+                // Construct a tel: URI, but do some other possible cleanup first.
+                int callType = cursor.getInt(CallLogQuery.CALL_TYPE);
+                if (!number.startsWith("+") &&
+                       (callType == Calls.INCOMING_TYPE
+                                || callType == Calls.MISSED_TYPE)) {
+                    // If the caller-id matches a contact with a better qualified number, use it
+                    String countryIso = cursor.getString(CallLogQuery.COUNTRY_ISO);
+                    number = mAdapter.getBetterNumberFromContacts(number, countryIso);
+                }
+                intent = ContactsUtils.getCallIntent(
+                        Uri.fromParts(Constants.SCHEME_TEL, number, null));
+            }
+            intent.setFlags(
+                    Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+            startActivity(intent);
+        }
+    }
+
+    @VisibleForTesting
+    CallLogAdapter getAdapter() {
+        return mAdapter;
+    }
+
+    @Override
+    public void setMenuVisibility(boolean menuVisible) {
+        super.setMenuVisibility(menuVisible);
+        if (mMenuVisible != menuVisible) {
+            mMenuVisible = menuVisible;
+            if (!menuVisible) {
+                updateOnExit();
+            } else if (isResumed()) {
+                refreshData();
+            }
+        }
+    }
+
+    /** Requests updates to the data to be shown. */
+    private void refreshData() {
+        // Prevent unnecessary refresh.
+        if (mRefreshDataRequired) {
+            // Mark all entries in the contact info cache as out of date, so they will be looked up
+            // again once being shown.
+            mAdapter.invalidateCache();
+            startCallsQuery();
+            startVoicemailStatusQuery();
+            updateOnEntry();
+            mRefreshDataRequired = false;
+        }
+    }
+
+    /** Removes the missed call notifications. */
+    private void removeMissedCallNotifications() {
+        try {
+            ITelephony telephony =
+                    ITelephony.Stub.asInterface(ServiceManager.getService("phone"));
+            if (telephony != null) {
+                telephony.cancelMissedCallsNotification();
+            } else {
+                Log.w(TAG, "Telephony service is null, can't call " +
+                        "cancelMissedCallsNotification");
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to clear missed calls notification due to remote exception");
+        }
+    }
+
+    /** Updates call data and notification state while leaving the call log tab. */
+    private void updateOnExit() {
+        updateOnTransition(false);
+    }
+
+    /** Updates call data and notification state while entering the call log tab. */
+    private void updateOnEntry() {
+        updateOnTransition(true);
+    }
+
+    private void updateOnTransition(boolean onEntry) {
+        // We don't want to update any call data when keyguard is on because the user has likely not
+        // seen the new calls yet.
+        // This might be called before onCreate() and thus we need to check null explicitly.
+        if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()) {
+            // On either of the transitions we reset the new flag and update the notifications.
+            // While exiting we additionally consume all missed calls (by marking them as read).
+            // This will ensure that they no more appear in the "new" section when we return back.
+            mCallLogQueryHandler.markNewCallsAsOld();
+            if (!onEntry) {
+                mCallLogQueryHandler.markMissedCallsAsRead();
+            }
+            removeMissedCallNotifications();
+            updateVoicemailNotifications();
+        }
+    }
+
+    private void updateVoicemailNotifications() {
+        Intent serviceIntent = new Intent(getActivity(), CallLogNotificationsService.class);
+        serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_NOTIFICATIONS);
+        getActivity().startService(serviceIntent);
+    }
+}
diff --git a/src/com/android/dialer/calllog/CallLogGroupBuilder.java b/src/com/android/dialer/calllog/CallLogGroupBuilder.java
new file mode 100644
index 0000000..bf472bd
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallLogGroupBuilder.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import android.database.Cursor;
+import android.provider.CallLog.Calls;
+import android.telephony.PhoneNumberUtils;
+
+import com.android.common.widget.GroupingListAdapter;
+import com.google.common.annotations.VisibleForTesting;
+
+/**
+ * Groups together calls in the call log.
+ * <p>
+ * This class is meant to be used in conjunction with {@link GroupingListAdapter}.
+ */
+public class CallLogGroupBuilder {
+    public interface GroupCreator {
+        public void addGroup(int cursorPosition, int size, boolean expanded);
+    }
+
+    /** The object on which the groups are created. */
+    private final GroupCreator mGroupCreator;
+
+    public CallLogGroupBuilder(GroupCreator groupCreator) {
+        mGroupCreator = groupCreator;
+    }
+
+    /**
+     * Finds all groups of adjacent entries in the call log which should be grouped together and
+     * calls {@link GroupCreator#addGroup(int, int, boolean)} on {@link #mGroupCreator} for each of
+     * them.
+     * <p>
+     * For entries that are not grouped with others, we do not need to create a group of size one.
+     * <p>
+     * It assumes that the cursor will not change during its execution.
+     *
+     * @see GroupingListAdapter#addGroups(Cursor)
+     */
+    public void addGroups(Cursor cursor) {
+        final int count = cursor.getCount();
+        if (count == 0) {
+            return;
+        }
+
+        int currentGroupSize = 1;
+        cursor.moveToFirst();
+        // The number of the first entry in the group.
+        String firstNumber = cursor.getString(CallLogQuery.NUMBER);
+        // This is the type of the first call in the group.
+        int firstCallType = cursor.getInt(CallLogQuery.CALL_TYPE);
+        while (cursor.moveToNext()) {
+            // The number of the current row in the cursor.
+            final String currentNumber = cursor.getString(CallLogQuery.NUMBER);
+            final int callType = cursor.getInt(CallLogQuery.CALL_TYPE);
+            final boolean sameNumber = equalNumbers(firstNumber, currentNumber);
+            final boolean shouldGroup;
+
+            if (CallLogQuery.isSectionHeader(cursor)) {
+                // Cannot group headers.
+                shouldGroup = false;
+            } else if (!sameNumber) {
+                // Should only group with calls from the same number.
+                shouldGroup = false;
+            } else if (firstCallType == Calls.VOICEMAIL_TYPE) {
+                // never group voicemail.
+                shouldGroup = false;
+            } else {
+                // Incoming, outgoing, and missed calls group together.
+                shouldGroup = (callType == Calls.INCOMING_TYPE || callType == Calls.OUTGOING_TYPE ||
+                        callType == Calls.MISSED_TYPE);
+            }
+
+            if (shouldGroup) {
+                // Increment the size of the group to include the current call, but do not create
+                // the group until we find a call that does not match.
+                currentGroupSize++;
+            } else {
+                // Create a group for the previous set of calls, excluding the current one, but do
+                // not create a group for a single call.
+                if (currentGroupSize > 1) {
+                    addGroup(cursor.getPosition() - currentGroupSize, currentGroupSize);
+                }
+                // Start a new group; it will include at least the current call.
+                currentGroupSize = 1;
+                // The current entry is now the first in the group.
+                firstNumber = currentNumber;
+                firstCallType = callType;
+            }
+        }
+        // If the last set of calls at the end of the call log was itself a group, create it now.
+        if (currentGroupSize > 1) {
+            addGroup(count - currentGroupSize, currentGroupSize);
+        }
+    }
+
+    /**
+     * Creates a group of items in the cursor.
+     * <p>
+     * The group is always unexpanded.
+     *
+     * @see CallLogAdapter#addGroup(int, int, boolean)
+     */
+    private void addGroup(int cursorPosition, int size) {
+        mGroupCreator.addGroup(cursorPosition, size, false);
+    }
+
+    @VisibleForTesting
+    boolean equalNumbers(String number1, String number2) {
+        if (PhoneNumberUtils.isUriNumber(number1) || PhoneNumberUtils.isUriNumber(number2)) {
+            return compareSipAddresses(number1, number2);
+        } else {
+            return PhoneNumberUtils.compare(number1, number2);
+        }
+    }
+
+    @VisibleForTesting
+    boolean compareSipAddresses(String number1, String number2) {
+        if (number1 == null || number2 == null) return number1 == number2;
+
+        int index1 = number1.indexOf('@');
+        final String userinfo1;
+        final String rest1;
+        if (index1 != -1) {
+            userinfo1 = number1.substring(0, index1);
+            rest1 = number1.substring(index1);
+        } else {
+            userinfo1 = number1;
+            rest1 = "";
+        }
+
+        int index2 = number2.indexOf('@');
+        final String userinfo2;
+        final String rest2;
+        if (index2 != -1) {
+            userinfo2 = number2.substring(0, index2);
+            rest2 = number2.substring(index2);
+        } else {
+            userinfo2 = number2;
+            rest2 = "";
+        }
+
+        return userinfo1.equals(userinfo2) && rest1.equalsIgnoreCase(rest2);
+    }
+}
diff --git a/src/com/android/dialer/calllog/CallLogListItemHelper.java b/src/com/android/dialer/calllog/CallLogListItemHelper.java
new file mode 100644
index 0000000..7862a56
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallLogListItemHelper.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import android.content.res.Resources;
+import android.provider.CallLog.Calls;
+import android.text.TextUtils;
+import android.view.View;
+
+import com.android.dialer.PhoneCallDetails;
+import com.android.dialer.PhoneCallDetailsHelper;
+import com.android.contacts.R;
+
+/**
+ * Helper class to fill in the views of a call log entry.
+ */
+/*package*/ class CallLogListItemHelper {
+    /** Helper for populating the details of a phone call. */
+    private final PhoneCallDetailsHelper mPhoneCallDetailsHelper;
+    /** Helper for handling phone numbers. */
+    private final PhoneNumberHelper mPhoneNumberHelper;
+    /** Resources to look up strings. */
+    private final Resources mResources;
+
+    /**
+     * Creates a new helper instance.
+     *
+     * @param phoneCallDetailsHelper used to set the details of a phone call
+     * @param phoneNumberHelper used to process phone number
+     */
+    public CallLogListItemHelper(PhoneCallDetailsHelper phoneCallDetailsHelper,
+            PhoneNumberHelper phoneNumberHelper, Resources resources) {
+        mPhoneCallDetailsHelper = phoneCallDetailsHelper;
+        mPhoneNumberHelper = phoneNumberHelper;
+        mResources = resources;
+    }
+
+    /**
+     * Sets the name, label, and number for a contact.
+     *
+     * @param views the views to populate
+     * @param details the details of a phone call needed to fill in the data
+     * @param isHighlighted whether to use the highlight text for the call
+     */
+    public void setPhoneCallDetails(CallLogListItemViews views, PhoneCallDetails details,
+            boolean isHighlighted) {
+        mPhoneCallDetailsHelper.setPhoneCallDetails(views.phoneCallDetailsViews, details,
+                isHighlighted);
+        boolean canCall = mPhoneNumberHelper.canPlaceCallsTo(details.number);
+        boolean canPlay = details.callTypes[0] == Calls.VOICEMAIL_TYPE;
+
+        if (canPlay) {
+            // Playback action takes preference.
+            configurePlaySecondaryAction(views, isHighlighted);
+            views.dividerView.setVisibility(View.VISIBLE);
+        } else if (canCall) {
+            // Call is the secondary action.
+            configureCallSecondaryAction(views, details);
+            views.dividerView.setVisibility(View.VISIBLE);
+        } else {
+            // No action available.
+            views.secondaryActionView.setVisibility(View.GONE);
+            views.dividerView.setVisibility(View.GONE);
+        }
+    }
+
+    /** Sets the secondary action to correspond to the call button. */
+    private void configureCallSecondaryAction(CallLogListItemViews views,
+            PhoneCallDetails details) {
+        views.secondaryActionView.setVisibility(View.VISIBLE);
+        views.secondaryActionView.setImageResource(R.drawable.ic_ab_dialer_holo_dark);
+        views.secondaryActionView.setContentDescription(getCallActionDescription(details));
+    }
+
+    /** Returns the description used by the call action for this phone call. */
+    private CharSequence getCallActionDescription(PhoneCallDetails details) {
+        final CharSequence recipient;
+        if (!TextUtils.isEmpty(details.name)) {
+            recipient = details.name;
+        } else {
+            recipient = mPhoneNumberHelper.getDisplayNumber(
+                    details.number, details.formattedNumber);
+        }
+        return mResources.getString(R.string.description_call, recipient);
+    }
+
+    /** Sets the secondary action to correspond to the play button. */
+    private void configurePlaySecondaryAction(CallLogListItemViews views, boolean isHighlighted) {
+        views.secondaryActionView.setVisibility(View.VISIBLE);
+        views.secondaryActionView.setImageResource(
+                isHighlighted ? R.drawable.ic_play_active_holo_dark : R.drawable.ic_play_holo_dark);
+        views.secondaryActionView.setContentDescription(
+                mResources.getString(R.string.description_call_log_play_button));
+    }
+}
diff --git a/src/com/android/dialer/calllog/CallLogListItemView.java b/src/com/android/dialer/calllog/CallLogListItemView.java
new file mode 100644
index 0000000..113b02a
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallLogListItemView.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+
+/**
+ * An entry in the call log.
+ */
+public class CallLogListItemView extends LinearLayout {
+    public CallLogListItemView(Context context) {
+        super(context);
+    }
+
+    public CallLogListItemView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public CallLogListItemView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    public void requestLayout() {
+        // We will assume that once measured this will not need to resize
+        // itself, so there is no need to pass the layout request to the parent
+        // view (ListView).
+        forceLayout();
+    }
+}
diff --git a/src/com/android/dialer/calllog/CallLogListItemViews.java b/src/com/android/dialer/calllog/CallLogListItemViews.java
new file mode 100644
index 0000000..5b860ef
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallLogListItemViews.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import android.content.Context;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+
+import com.android.dialer.PhoneCallDetailsViews;
+import com.android.contacts.R;
+import com.android.contacts.test.NeededForTesting;
+
+/**
+ * Simple value object containing the various views within a call log entry.
+ */
+public final class CallLogListItemViews {
+    /** The quick contact badge for the contact. */
+    public final QuickContactBadge quickContactView;
+    /** The primary action view of the entry. */
+    public final View primaryActionView;
+    /** The secondary action button on the entry. */
+    public final ImageView secondaryActionView;
+    /** The divider between the primary and secondary actions. */
+    public final View dividerView;
+    /** The details of the phone call. */
+    public final PhoneCallDetailsViews phoneCallDetailsViews;
+    /** The text of the header of a section. */
+    public final TextView listHeaderTextView;
+    /** The divider to be shown below items. */
+    public final View bottomDivider;
+
+    private CallLogListItemViews(QuickContactBadge quickContactView, View primaryActionView,
+            ImageView secondaryActionView, View dividerView,
+            PhoneCallDetailsViews phoneCallDetailsViews,
+            TextView listHeaderTextView, View bottomDivider) {
+        this.quickContactView = quickContactView;
+        this.primaryActionView = primaryActionView;
+        this.secondaryActionView = secondaryActionView;
+        this.dividerView = dividerView;
+        this.phoneCallDetailsViews = phoneCallDetailsViews;
+        this.listHeaderTextView = listHeaderTextView;
+        this.bottomDivider = bottomDivider;
+    }
+
+    public static CallLogListItemViews fromView(View view) {
+        return new CallLogListItemViews(
+                (QuickContactBadge) view.findViewById(R.id.quick_contact_photo),
+                view.findViewById(R.id.primary_action_view),
+                (ImageView) view.findViewById(R.id.secondary_action_icon),
+                view.findViewById(R.id.divider),
+                PhoneCallDetailsViews.fromView(view),
+                (TextView) view.findViewById(R.id.call_log_header),
+                view.findViewById(R.id.call_log_divider));
+    }
+
+    @NeededForTesting
+    public static CallLogListItemViews createForTest(Context context) {
+        return new CallLogListItemViews(
+                new QuickContactBadge(context),
+                new View(context),
+                new ImageView(context),
+                new View(context),
+                PhoneCallDetailsViews.createForTest(context),
+                new TextView(context),
+                new View(context));
+    }
+}
diff --git a/src/com/android/dialer/calllog/CallLogNotificationsService.java b/src/com/android/dialer/calllog/CallLogNotificationsService.java
new file mode 100644
index 0000000..3270963
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallLogNotificationsService.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import android.app.IntentService;
+import android.content.Intent;
+import android.net.Uri;
+import android.util.Log;
+
+/**
+ * Provides operations for managing notifications.
+ * <p>
+ * It handles the following actions:
+ * <ul>
+ * <li>{@link #ACTION_MARK_NEW_VOICEMAILS_AS_OLD}: marks all the new voicemails in the call log as
+ * old; this is called when a notification is dismissed.</li>
+ * <li>{@link #ACTION_UPDATE_NOTIFICATIONS}: updates the content of the new items notification; it
+ * may include an optional extra {@link #EXTRA_NEW_VOICEMAIL_URI}, containing the URI of the new
+ * voicemail that has triggered this update (if any).</li>
+ * </ul>
+ */
+public class CallLogNotificationsService extends IntentService {
+    private static final String TAG = "CallLogNotificationsService";
+
+    /** Action to mark all the new voicemails as old. */
+    public static final String ACTION_MARK_NEW_VOICEMAILS_AS_OLD =
+            "com.android.dialer.calllog.ACTION_MARK_NEW_VOICEMAILS_AS_OLD";
+
+    /**
+     * Action to update the notifications.
+     * <p>
+     * May include an optional extra {@link #EXTRA_NEW_VOICEMAIL_URI}.
+     */
+    public static final String ACTION_UPDATE_NOTIFICATIONS =
+            "com.android.dialer.calllog.UPDATE_NOTIFICATIONS";
+
+    /**
+     * Extra to included with {@link #ACTION_UPDATE_NOTIFICATIONS} to identify the new voicemail
+     * that triggered an update.
+     * <p>
+     * It must be a {@link Uri}.
+     */
+    public static final String EXTRA_NEW_VOICEMAIL_URI = "NEW_VOICEMAIL_URI";
+
+    private CallLogQueryHandler mCallLogQueryHandler;
+
+    public CallLogNotificationsService() {
+        super("CallLogNotificationsService");
+    }
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mCallLogQueryHandler = new CallLogQueryHandler(getContentResolver(), null /*listener*/);
+    }
+
+    @Override
+    protected void onHandleIntent(Intent intent) {
+        if (ACTION_MARK_NEW_VOICEMAILS_AS_OLD.equals(intent.getAction())) {
+            mCallLogQueryHandler.markNewVoicemailsAsOld();
+        } else if (ACTION_UPDATE_NOTIFICATIONS.equals(intent.getAction())) {
+            Uri voicemailUri = (Uri) intent.getParcelableExtra(EXTRA_NEW_VOICEMAIL_URI);
+            DefaultVoicemailNotifier.getInstance(this).updateNotification(voicemailUri);
+        } else {
+            Log.d(TAG, "onHandleIntent: could not handle: " + intent);
+        }
+    }
+}
diff --git a/src/com/android/dialer/calllog/CallLogQuery.java b/src/com/android/dialer/calllog/CallLogQuery.java
new file mode 100644
index 0000000..5f7b27b
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallLogQuery.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import android.database.Cursor;
+import android.provider.CallLog.Calls;
+
+/**
+ * The query for the call log table.
+ */
+public final class CallLogQuery {
+    // If you alter this, you must also alter the method that inserts a fake row to the headers
+    // in the CallLogQueryHandler class called createHeaderCursorFor().
+    public static final String[] _PROJECTION = new String[] {
+            Calls._ID,                       // 0
+            Calls.NUMBER,                    // 1
+            Calls.DATE,                      // 2
+            Calls.DURATION,                  // 3
+            Calls.TYPE,                      // 4
+            Calls.COUNTRY_ISO,               // 5
+            Calls.VOICEMAIL_URI,             // 6
+            Calls.GEOCODED_LOCATION,         // 7
+            Calls.CACHED_NAME,               // 8
+            Calls.CACHED_NUMBER_TYPE,        // 9
+            Calls.CACHED_NUMBER_LABEL,       // 10
+            Calls.CACHED_LOOKUP_URI,         // 11
+            Calls.CACHED_MATCHED_NUMBER,     // 12
+            Calls.CACHED_NORMALIZED_NUMBER,  // 13
+            Calls.CACHED_PHOTO_ID,           // 14
+            Calls.CACHED_FORMATTED_NUMBER,   // 15
+            Calls.IS_READ,                   // 16
+    };
+
+    public static final int ID = 0;
+    public static final int NUMBER = 1;
+    public static final int DATE = 2;
+    public static final int DURATION = 3;
+    public static final int CALL_TYPE = 4;
+    public static final int COUNTRY_ISO = 5;
+    public static final int VOICEMAIL_URI = 6;
+    public static final int GEOCODED_LOCATION = 7;
+    public static final int CACHED_NAME = 8;
+    public static final int CACHED_NUMBER_TYPE = 9;
+    public static final int CACHED_NUMBER_LABEL = 10;
+    public static final int CACHED_LOOKUP_URI = 11;
+    public static final int CACHED_MATCHED_NUMBER = 12;
+    public static final int CACHED_NORMALIZED_NUMBER = 13;
+    public static final int CACHED_PHOTO_ID = 14;
+    public static final int CACHED_FORMATTED_NUMBER = 15;
+    public static final int IS_READ = 16;
+    /** The index of the synthetic "section" column in the extended projection. */
+    public static final int SECTION = 17;
+
+    /**
+     * The name of the synthetic "section" column.
+     * <p>
+     * This column identifies whether a row is a header or an actual item, and whether it is
+     * part of the new or old calls.
+     */
+    public static final String SECTION_NAME = "section";
+    /** The value of the "section" column for the header of the new section. */
+    public static final int SECTION_NEW_HEADER = 0;
+    /** The value of the "section" column for the items of the new section. */
+    public static final int SECTION_NEW_ITEM = 1;
+    /** The value of the "section" column for the header of the old section. */
+    public static final int SECTION_OLD_HEADER = 2;
+    /** The value of the "section" column for the items of the old section. */
+    public static final int SECTION_OLD_ITEM = 3;
+
+    /** The call log projection including the section name. */
+    public static final String[] EXTENDED_PROJECTION;
+    static {
+        EXTENDED_PROJECTION = new String[_PROJECTION.length + 1];
+        System.arraycopy(_PROJECTION, 0, EXTENDED_PROJECTION, 0, _PROJECTION.length);
+        EXTENDED_PROJECTION[_PROJECTION.length] = SECTION_NAME;
+    }
+
+    public static boolean isSectionHeader(Cursor cursor) {
+        int section = cursor.getInt(CallLogQuery.SECTION);
+        return section == CallLogQuery.SECTION_NEW_HEADER
+                || section == CallLogQuery.SECTION_OLD_HEADER;
+    }
+
+    public static boolean isNewSection(Cursor cursor) {
+        int section = cursor.getInt(CallLogQuery.SECTION);
+        return section == CallLogQuery.SECTION_NEW_ITEM
+                || section == CallLogQuery.SECTION_NEW_HEADER;
+    }
+}
diff --git a/src/com/android/dialer/calllog/CallLogQueryHandler.java b/src/com/android/dialer/calllog/CallLogQueryHandler.java
new file mode 100644
index 0000000..2e67e5a
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallLogQueryHandler.java
@@ -0,0 +1,364 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.database.MergeCursor;
+import android.database.sqlite.SQLiteDatabaseCorruptException;
+import android.database.sqlite.SQLiteDiskIOException;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteFullException;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.provider.CallLog.Calls;
+import android.provider.VoicemailContract.Status;
+import android.util.Log;
+
+import com.android.common.io.MoreCloseables;
+import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
+import com.google.common.collect.Lists;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/** Handles asynchronous queries to the call log. */
+/*package*/ class CallLogQueryHandler extends AsyncQueryHandler {
+    private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+    private static final String TAG = "CallLogQueryHandler";
+    private static final int NUM_LOGS_TO_DISPLAY = 1000;
+
+    /** The token for the query to fetch the new entries from the call log. */
+    private static final int QUERY_NEW_CALLS_TOKEN = 53;
+    /** The token for the query to fetch the old entries from the call log. */
+    private static final int QUERY_OLD_CALLS_TOKEN = 54;
+    /** The token for the query to mark all missed calls as old after seeing the call log. */
+    private static final int UPDATE_MARK_AS_OLD_TOKEN = 55;
+    /** The token for the query to mark all new voicemails as old. */
+    private static final int UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN = 56;
+    /** The token for the query to mark all missed calls as read after seeing the call log. */
+    private static final int UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN = 57;
+    /** The token for the query to fetch voicemail status messages. */
+    private static final int QUERY_VOICEMAIL_STATUS_TOKEN = 58;
+
+    /**
+     * Call type similar to Calls.INCOMING_TYPE used to specify all types instead of one particular
+     * type.
+     */
+    public static final int CALL_TYPE_ALL = -1;
+
+    /**
+     * The time window from the current time within which an unread entry will be added to the new
+     * section.
+     */
+    private static final long NEW_SECTION_TIME_WINDOW = TimeUnit.DAYS.toMillis(7);
+
+    private final WeakReference<Listener> mListener;
+
+    /** The cursor containing the new calls, or null if they have not yet been fetched. */
+    @GuardedBy("this") private Cursor mNewCallsCursor;
+    /** The cursor containing the old calls, or null if they have not yet been fetched. */
+    @GuardedBy("this") private Cursor mOldCallsCursor;
+    /**
+     * The identifier of the latest calls request.
+     * <p>
+     * A request for the list of calls requires two queries and hence the two cursor
+     * {@link #mNewCallsCursor} and {@link #mOldCallsCursor} above, corresponding to
+     * {@link #QUERY_NEW_CALLS_TOKEN} and {@link #QUERY_OLD_CALLS_TOKEN}.
+     * <p>
+     * When a new request is about to be started, existing cursors are closed. However, it is
+     * possible that one of the queries completes after the new request has started. This means that
+     * we might merge two cursors that do not correspond to the same request. Moreover, this may
+     * lead to a resource leak if the same query completes and we override the cursor without
+     * closing it first.
+     * <p>
+     * To make sure we only join two cursors from the same request, we use this variable to store
+     * the request id of the latest request and make sure we only process cursors corresponding to
+     * the this request.
+     */
+    @GuardedBy("this") private int mCallsRequestId;
+
+    /**
+     * Simple handler that wraps background calls to catch
+     * {@link SQLiteException}, such as when the disk is full.
+     */
+    protected class CatchingWorkerHandler extends AsyncQueryHandler.WorkerHandler {
+        public CatchingWorkerHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            try {
+                // Perform same query while catching any exceptions
+                super.handleMessage(msg);
+            } catch (SQLiteDiskIOException e) {
+                Log.w(TAG, "Exception on background worker thread", e);
+            } catch (SQLiteFullException e) {
+                Log.w(TAG, "Exception on background worker thread", e);
+            } catch (SQLiteDatabaseCorruptException e) {
+                Log.w(TAG, "Exception on background worker thread", e);
+            }
+        }
+    }
+
+    @Override
+    protected Handler createHandler(Looper looper) {
+        // Provide our special handler that catches exceptions
+        return new CatchingWorkerHandler(looper);
+    }
+
+    public CallLogQueryHandler(ContentResolver contentResolver, Listener listener) {
+        super(contentResolver);
+        mListener = new WeakReference<Listener>(listener);
+    }
+
+    /** Creates a cursor that contains a single row and maps the section to the given value. */
+    private Cursor createHeaderCursorFor(int section) {
+        MatrixCursor matrixCursor =
+                new MatrixCursor(CallLogQuery.EXTENDED_PROJECTION);
+        // The values in this row correspond to default values for _PROJECTION from CallLogQuery
+        // plus the section value.
+        matrixCursor.addRow(new Object[]{
+                0L, "", 0L, 0L, 0, "", "", "", null, 0, null, null, null, null, 0L, null, 0,
+                section
+        });
+        return matrixCursor;
+    }
+
+    /** Returns a cursor for the old calls header. */
+    private Cursor createOldCallsHeaderCursor() {
+        return createHeaderCursorFor(CallLogQuery.SECTION_OLD_HEADER);
+    }
+
+    /** Returns a cursor for the new calls header. */
+    private Cursor createNewCallsHeaderCursor() {
+        return createHeaderCursorFor(CallLogQuery.SECTION_NEW_HEADER);
+    }
+
+    /**
+     * Fetches the list of calls from the call log for a given type.
+     * <p>
+     * It will asynchronously update the content of the list view when the fetch completes.
+     */
+    public void fetchCalls(int callType) {
+        cancelFetch();
+        int requestId = newCallsRequest();
+        fetchCalls(QUERY_NEW_CALLS_TOKEN, requestId, true /*isNew*/, callType);
+        fetchCalls(QUERY_OLD_CALLS_TOKEN, requestId, false /*isNew*/, callType);
+    }
+
+    public void fetchVoicemailStatus() {
+        startQuery(QUERY_VOICEMAIL_STATUS_TOKEN, null, Status.CONTENT_URI,
+                VoicemailStatusHelperImpl.PROJECTION, null, null, null);
+    }
+
+    /** Fetches the list of calls in the call log, either the new one or the old ones. */
+    private void fetchCalls(int token, int requestId, boolean isNew, int callType) {
+        // We need to check for NULL explicitly otherwise entries with where READ is NULL
+        // may not match either the query or its negation.
+        // We consider the calls that are not yet consumed (i.e. IS_READ = 0) as "new".
+        String selection = String.format("%s IS NOT NULL AND %s = 0 AND %s > ?",
+                Calls.IS_READ, Calls.IS_READ, Calls.DATE);
+        List<String> selectionArgs = Lists.newArrayList(
+                Long.toString(System.currentTimeMillis() - NEW_SECTION_TIME_WINDOW));
+        if (!isNew) {
+            // Negate the query.
+            selection = String.format("NOT (%s)", selection);
+        }
+        if (callType > CALL_TYPE_ALL) {
+            // Add a clause to fetch only items of type voicemail.
+            selection = String.format("(%s) AND (%s = ?)", selection, Calls.TYPE);
+            selectionArgs.add(Integer.toString(callType));
+        }
+        Uri uri = Calls.CONTENT_URI_WITH_VOICEMAIL.buildUpon()
+                .appendQueryParameter(Calls.LIMIT_PARAM_KEY, Integer.toString(NUM_LOGS_TO_DISPLAY))
+                .build();
+        startQuery(token, requestId, uri,
+                CallLogQuery._PROJECTION, selection, selectionArgs.toArray(EMPTY_STRING_ARRAY),
+                Calls.DEFAULT_SORT_ORDER);
+    }
+
+    /** Cancel any pending fetch request. */
+    private void cancelFetch() {
+        cancelOperation(QUERY_NEW_CALLS_TOKEN);
+        cancelOperation(QUERY_OLD_CALLS_TOKEN);
+    }
+
+    /** Updates all new calls to mark them as old. */
+    public void markNewCallsAsOld() {
+        // Mark all "new" calls as not new anymore.
+        StringBuilder where = new StringBuilder();
+        where.append(Calls.NEW);
+        where.append(" = 1");
+
+        ContentValues values = new ContentValues(1);
+        values.put(Calls.NEW, "0");
+
+        startUpdate(UPDATE_MARK_AS_OLD_TOKEN, null, Calls.CONTENT_URI_WITH_VOICEMAIL,
+                values, where.toString(), null);
+    }
+
+    /** Updates all new voicemails to mark them as old. */
+    public void markNewVoicemailsAsOld() {
+        // Mark all "new" voicemails as not new anymore.
+        StringBuilder where = new StringBuilder();
+        where.append(Calls.NEW);
+        where.append(" = 1 AND ");
+        where.append(Calls.TYPE);
+        where.append(" = ?");
+
+        ContentValues values = new ContentValues(1);
+        values.put(Calls.NEW, "0");
+
+        startUpdate(UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN, null, Calls.CONTENT_URI_WITH_VOICEMAIL,
+                values, where.toString(), new String[]{ Integer.toString(Calls.VOICEMAIL_TYPE) });
+    }
+
+    /** Updates all missed calls to mark them as read. */
+    public void markMissedCallsAsRead() {
+        // Mark all "new" calls as not new anymore.
+        StringBuilder where = new StringBuilder();
+        where.append(Calls.IS_READ).append(" = 0");
+        where.append(" AND ");
+        where.append(Calls.TYPE).append(" = ").append(Calls.MISSED_TYPE);
+
+        ContentValues values = new ContentValues(1);
+        values.put(Calls.IS_READ, "1");
+
+        startUpdate(UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN, null, Calls.CONTENT_URI, values,
+                where.toString(), null);
+    }
+
+    /**
+     * Start a new request and return its id. The request id will be used as the cookie for the
+     * background request.
+     * <p>
+     * Closes any open cursor that has not yet been sent to the requester.
+     */
+    private synchronized int newCallsRequest() {
+        MoreCloseables.closeQuietly(mNewCallsCursor);
+        MoreCloseables.closeQuietly(mOldCallsCursor);
+        mNewCallsCursor = null;
+        mOldCallsCursor = null;
+        return ++mCallsRequestId;
+    }
+
+    @Override
+    protected synchronized void onQueryComplete(int token, Object cookie, Cursor cursor) {
+        if (token == QUERY_NEW_CALLS_TOKEN) {
+            int requestId = ((Integer) cookie).intValue();
+            if (requestId != mCallsRequestId) {
+                // Ignore this query since it does not correspond to the latest request.
+                return;
+            }
+
+            // Store the returned cursor.
+            MoreCloseables.closeQuietly(mNewCallsCursor);
+            mNewCallsCursor = new ExtendedCursor(
+                    cursor, CallLogQuery.SECTION_NAME, CallLogQuery.SECTION_NEW_ITEM);
+        } else if (token == QUERY_OLD_CALLS_TOKEN) {
+            int requestId = ((Integer) cookie).intValue();
+            if (requestId != mCallsRequestId) {
+                // Ignore this query since it does not correspond to the latest request.
+                return;
+            }
+
+            // Store the returned cursor.
+            MoreCloseables.closeQuietly(mOldCallsCursor);
+            mOldCallsCursor = new ExtendedCursor(
+                    cursor, CallLogQuery.SECTION_NAME, CallLogQuery.SECTION_OLD_ITEM);
+        } else if (token == QUERY_VOICEMAIL_STATUS_TOKEN) {
+            updateVoicemailStatus(cursor);
+            return;
+        } else {
+            Log.w(TAG, "Unknown query completed: ignoring: " + token);
+            return;
+        }
+
+        if (mNewCallsCursor != null && mOldCallsCursor != null) {
+            updateAdapterData(createMergedCursor());
+        }
+    }
+
+    /** Creates the merged cursor representing the data to show in the call log. */
+    @GuardedBy("this")
+    private Cursor createMergedCursor() {
+        try {
+            final boolean hasNewCalls = mNewCallsCursor.getCount() != 0;
+            final boolean hasOldCalls = mOldCallsCursor.getCount() != 0;
+
+            if (!hasNewCalls) {
+                // Return only the old calls, without the header.
+                MoreCloseables.closeQuietly(mNewCallsCursor);
+                return mOldCallsCursor;
+            }
+
+            if (!hasOldCalls) {
+                // Return only the new calls.
+                MoreCloseables.closeQuietly(mOldCallsCursor);
+                return new MergeCursor(
+                        new Cursor[]{ createNewCallsHeaderCursor(), mNewCallsCursor });
+            }
+
+            return new MergeCursor(new Cursor[]{
+                    createNewCallsHeaderCursor(), mNewCallsCursor,
+                    createOldCallsHeaderCursor(), mOldCallsCursor});
+        } finally {
+            // Any cursor still open is now owned, directly or indirectly, by the caller.
+            mNewCallsCursor = null;
+            mOldCallsCursor = null;
+        }
+    }
+
+    /**
+     * Updates the adapter in the call log fragment to show the new cursor data.
+     */
+    private void updateAdapterData(Cursor combinedCursor) {
+        final Listener listener = mListener.get();
+        if (listener != null) {
+            listener.onCallsFetched(combinedCursor);
+        }
+    }
+
+    private void updateVoicemailStatus(Cursor statusCursor) {
+        final Listener listener = mListener.get();
+        if (listener != null) {
+            listener.onVoicemailStatusFetched(statusCursor);
+        }
+    }
+
+    /** Listener to completion of various queries. */
+    public interface Listener {
+        /** Called when {@link CallLogQueryHandler#fetchVoicemailStatus()} completes. */
+        void onVoicemailStatusFetched(Cursor statusCursor);
+
+        /**
+         * Called when {@link CallLogQueryHandler#fetchCalls(int)}complete.
+         */
+        void onCallsFetched(Cursor combinedCursor);
+    }
+}
diff --git a/src/com/android/dialer/calllog/CallLogReceiver.java b/src/com/android/dialer/calllog/CallLogReceiver.java
new file mode 100644
index 0000000..97d2951
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallLogReceiver.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.VoicemailContract;
+import android.util.Log;
+
+/**
+ * Receiver for call log events.
+ * <p>
+ * It is currently used to handle {@link VoicemailContract#ACTION_NEW_VOICEMAIL} and
+ * {@link Intent#ACTION_BOOT_COMPLETED}.
+ */
+public class CallLogReceiver extends BroadcastReceiver {
+    private static final String TAG = "CallLogReceiver";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (VoicemailContract.ACTION_NEW_VOICEMAIL.equals(intent.getAction())) {
+            Intent serviceIntent = new Intent(context, CallLogNotificationsService.class);
+            serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_NOTIFICATIONS);
+            serviceIntent.putExtra(
+                    CallLogNotificationsService.EXTRA_NEW_VOICEMAIL_URI, intent.getData());
+            context.startService(serviceIntent);
+        } else if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
+            Intent serviceIntent = new Intent(context, CallLogNotificationsService.class);
+            serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_NOTIFICATIONS);
+            context.startService(serviceIntent);
+        } else {
+            Log.w(TAG, "onReceive: could not handle: " + intent);
+        }
+    }
+}
diff --git a/src/com/android/dialer/calllog/CallTypeHelper.java b/src/com/android/dialer/calllog/CallTypeHelper.java
new file mode 100644
index 0000000..255258e
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallTypeHelper.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import android.content.res.Resources;
+import android.provider.CallLog.Calls;
+
+import com.android.contacts.R;
+
+/**
+ * Helper class to perform operations related to call types.
+ */
+public class CallTypeHelper {
+    /** Name used to identify incoming calls. */
+    private final CharSequence mIncomingName;
+    /** Name used to identify outgoing calls. */
+    private final CharSequence mOutgoingName;
+    /** Name used to identify missed calls. */
+    private final CharSequence mMissedName;
+    /** Name used to identify voicemail calls. */
+    private final CharSequence mVoicemailName;
+    /** Color used to identify new missed calls. */
+    private final int mNewMissedColor;
+    /** Color used to identify new voicemail calls. */
+    private final int mNewVoicemailColor;
+
+    public CallTypeHelper(Resources resources) {
+        // Cache these values so that we do not need to look them up each time.
+        mIncomingName = resources.getString(R.string.type_incoming);
+        mOutgoingName = resources.getString(R.string.type_outgoing);
+        mMissedName = resources.getString(R.string.type_missed);
+        mVoicemailName = resources.getString(R.string.type_voicemail);
+        mNewMissedColor = resources.getColor(R.color.call_log_missed_call_highlight_color);
+        mNewVoicemailColor = resources.getColor(R.color.call_log_voicemail_highlight_color);
+    }
+
+    /** Returns the text used to represent the given call type. */
+    public CharSequence getCallTypeText(int callType) {
+        switch (callType) {
+            case Calls.INCOMING_TYPE:
+                return mIncomingName;
+
+            case Calls.OUTGOING_TYPE:
+                return mOutgoingName;
+
+            case Calls.MISSED_TYPE:
+                return mMissedName;
+
+            case Calls.VOICEMAIL_TYPE:
+                return mVoicemailName;
+
+            default:
+                throw new IllegalArgumentException("invalid call type: " + callType);
+        }
+    }
+
+    /** Returns the color used to highlight the given call type, null if not highlight is needed. */
+    public Integer getHighlightedColor(int callType) {
+        switch (callType) {
+            case Calls.INCOMING_TYPE:
+                // New incoming calls are not highlighted.
+                return null;
+
+            case Calls.OUTGOING_TYPE:
+                // New outgoing calls are not highlighted.
+                return null;
+
+            case Calls.MISSED_TYPE:
+                return mNewMissedColor;
+
+            case Calls.VOICEMAIL_TYPE:
+                return mNewVoicemailColor;
+
+            default:
+                throw new IllegalArgumentException("invalid call type: " + callType);
+        }
+    }
+}
diff --git a/src/com/android/dialer/calllog/CallTypeIconsView.java b/src/com/android/dialer/calllog/CallTypeIconsView.java
new file mode 100644
index 0000000..e26d5a1
--- /dev/null
+++ b/src/com/android/dialer/calllog/CallTypeIconsView.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.provider.CallLog.Calls;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.contacts.R;
+import com.android.contacts.test.NeededForTesting;
+import com.google.common.collect.Lists;
+
+import java.util.List;
+
+/**
+ * View that draws one or more symbols for different types of calls (missed calls, outgoing etc).
+ * The symbols are set up horizontally. As this view doesn't create subviews, it is better suited
+ * for ListView-recycling that a regular LinearLayout using ImageViews.
+ */
+public class CallTypeIconsView extends View {
+    private List<Integer> mCallTypes = Lists.newArrayListWithCapacity(3);
+    private Resources mResources;
+    private int mWidth;
+    private int mHeight;
+
+    public CallTypeIconsView(Context context) {
+        this(context, null);
+    }
+
+    public CallTypeIconsView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mResources = new Resources(context);
+    }
+
+    public void clear() {
+        mCallTypes.clear();
+        mWidth = 0;
+        mHeight = 0;
+        invalidate();
+    }
+
+    public void add(int callType) {
+        mCallTypes.add(callType);
+
+        final Drawable drawable = getCallTypeDrawable(callType);
+        mWidth += drawable.getIntrinsicWidth() + mResources.iconMargin;
+        mHeight = Math.max(mHeight, drawable.getIntrinsicHeight());
+        invalidate();
+    }
+
+    @NeededForTesting
+    public int getCount() {
+        return mCallTypes.size();
+    }
+
+    @NeededForTesting
+    public int getCallType(int index) {
+        return mCallTypes.get(index);
+    }
+
+    private Drawable getCallTypeDrawable(int callType) {
+        switch (callType) {
+            case Calls.INCOMING_TYPE:
+                return mResources.incoming;
+            case Calls.OUTGOING_TYPE:
+                return mResources.outgoing;
+            case Calls.MISSED_TYPE:
+                return mResources.missed;
+            case Calls.VOICEMAIL_TYPE:
+                return mResources.voicemail;
+            default:
+                throw new IllegalArgumentException("invalid call type: " + callType);
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        setMeasuredDimension(mWidth, mHeight);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        int left = 0;
+        for (Integer callType : mCallTypes) {
+            final Drawable drawable = getCallTypeDrawable(callType);
+            final int right = left + drawable.getIntrinsicWidth();
+            drawable.setBounds(left, 0, right, drawable.getIntrinsicHeight());
+            drawable.draw(canvas);
+            left = right + mResources.iconMargin;
+        }
+    }
+
+    private static class Resources {
+        public final Drawable incoming;
+        public final Drawable outgoing;
+        public final Drawable missed;
+        public final Drawable voicemail;
+        public final int iconMargin;
+
+        public Resources(Context context) {
+            final android.content.res.Resources r = context.getResources();
+            incoming = r.getDrawable(R.drawable.ic_call_incoming_holo_dark);
+            outgoing = r.getDrawable(R.drawable.ic_call_outgoing_holo_dark);
+            missed = r.getDrawable(R.drawable.ic_call_missed_holo_dark);
+            voicemail = r.getDrawable(R.drawable.ic_call_voicemail_holo_dark);
+            iconMargin = r.getDimensionPixelSize(R.dimen.call_log_icon_margin);
+        }
+    }
+}
diff --git a/src/com/android/dialer/calllog/ClearCallLogDialog.java b/src/com/android/dialer/calllog/ClearCallLogDialog.java
new file mode 100644
index 0000000..e91c08f
--- /dev/null
+++ b/src/com/android/dialer/calllog/ClearCallLogDialog.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.app.ProgressDialog;
+import android.content.ContentResolver;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.CallLog.Calls;
+
+import com.android.contacts.R;
+
+/**
+ * Dialog that clears the call log after confirming with the user
+ */
+public class ClearCallLogDialog extends DialogFragment {
+    /** Preferred way to show this dialog */
+    public static void show(FragmentManager fragmentManager) {
+        ClearCallLogDialog dialog = new ClearCallLogDialog();
+        dialog.show(fragmentManager, "deleteCallLog");
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        final ContentResolver resolver = getActivity().getContentResolver();
+        final OnClickListener okListener = new OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                final ProgressDialog progressDialog = ProgressDialog.show(getActivity(),
+                        getString(R.string.clearCallLogProgress_title),
+                        "", true, false);
+                final AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
+                    @Override
+                    protected Void doInBackground(Void... params) {
+                        resolver.delete(Calls.CONTENT_URI, null, null);
+                        return null;
+                    }
+                    @Override
+                    protected void onPostExecute(Void result) {
+                        progressDialog.dismiss();
+                    }
+                };
+                // TODO: Once we have the API, we should configure this ProgressDialog
+                // to only show up after a certain time (e.g. 150ms)
+                progressDialog.show();
+                task.execute();
+            }
+        };
+        return new AlertDialog.Builder(getActivity())
+            .setTitle(R.string.clearCallLogConfirmation_title)
+            .setIconAttribute(android.R.attr.alertDialogIcon)
+            .setMessage(R.string.clearCallLogConfirmation)
+            .setNegativeButton(android.R.string.cancel, null)
+            .setPositiveButton(android.R.string.ok, okListener)
+            .setCancelable(true)
+            .create();
+    }
+}
diff --git a/src/com/android/dialer/calllog/ContactInfo.java b/src/com/android/dialer/calllog/ContactInfo.java
new file mode 100644
index 0000000..b48adef
--- /dev/null
+++ b/src/com/android/dialer/calllog/ContactInfo.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import android.net.Uri;
+import android.text.TextUtils;
+
+import com.android.contacts.util.UriUtils;
+
+/**
+ * Information for a contact as needed by the Call Log.
+ */
+public final class ContactInfo {
+    public Uri lookupUri;
+    public String name;
+    public int type;
+    public String label;
+    public String number;
+    public String formattedNumber;
+    public String normalizedNumber;
+    /** The photo for the contact, if available. */
+    public long photoId;
+    /** The high-res photo for the contact, if available. */
+    public Uri photoUri;
+
+    public static ContactInfo EMPTY = new ContactInfo();
+
+    @Override
+    public int hashCode() {
+        // Uses only name and contactUri to determine hashcode.
+        // This should be sufficient to have a reasonable distribution of hash codes.
+        // Moreover, there should be no two people with the same lookupUri.
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((lookupUri == null) ? 0 : lookupUri.hashCode());
+        result = prime * result + ((name == null) ? 0 : name.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+        if (obj == null) return false;
+        if (getClass() != obj.getClass()) return false;
+        ContactInfo other = (ContactInfo) obj;
+        if (!UriUtils.areEqual(lookupUri, other.lookupUri)) return false;
+        if (!TextUtils.equals(name, other.name)) return false;
+        if (type != other.type) return false;
+        if (!TextUtils.equals(label, other.label)) return false;
+        if (!TextUtils.equals(number, other.number)) return false;
+        if (!TextUtils.equals(formattedNumber, other.formattedNumber)) return false;
+        if (!TextUtils.equals(normalizedNumber, other.normalizedNumber)) return false;
+        if (photoId != other.photoId) return false;
+        if (!UriUtils.areEqual(photoUri, other.photoUri)) return false;
+        return true;
+    }
+}
diff --git a/src/com/android/dialer/calllog/ContactInfoHelper.java b/src/com/android/dialer/calllog/ContactInfoHelper.java
new file mode 100644
index 0000000..b6f0662
--- /dev/null
+++ b/src/com/android/dialer/calllog/ContactInfoHelper.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.PhoneLookup;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+
+import com.android.contacts.util.UriUtils;
+
+/**
+ * Utility class to look up the contact information for a given number.
+ */
+public class ContactInfoHelper {
+    private final Context mContext;
+    private final String mCurrentCountryIso;
+
+    public ContactInfoHelper(Context context, String currentCountryIso) {
+        mContext = context;
+        mCurrentCountryIso = currentCountryIso;
+    }
+
+    /**
+     * Returns the contact information for the given number.
+     * <p>
+     * If the number does not match any contact, returns a contact info containing only the number
+     * and the formatted number.
+     * <p>
+     * If an error occurs during the lookup, it returns null.
+     *
+     * @param number the number to look up
+     * @param countryIso the country associated with this number
+     */
+    public ContactInfo lookupNumber(String number, String countryIso) {
+        final ContactInfo info;
+
+        // Determine the contact info.
+        if (PhoneNumberUtils.isUriNumber(number)) {
+            // This "number" is really a SIP address.
+            ContactInfo sipInfo = queryContactInfoForSipAddress(number);
+            if (sipInfo == null || sipInfo == ContactInfo.EMPTY) {
+                // Check whether the "username" part of the SIP address is
+                // actually the phone number of a contact.
+                String username = PhoneNumberUtils.getUsernameFromUriNumber(number);
+                if (PhoneNumberUtils.isGlobalPhoneNumber(username)) {
+                    sipInfo = queryContactInfoForPhoneNumber(username, countryIso);
+                }
+            }
+            info = sipInfo;
+        } else {
+            // Look for a contact that has the given phone number.
+            ContactInfo phoneInfo = queryContactInfoForPhoneNumber(number, countryIso);
+
+            if (phoneInfo == null || phoneInfo == ContactInfo.EMPTY) {
+                // Check whether the phone number has been saved as an "Internet call" number.
+                phoneInfo = queryContactInfoForSipAddress(number);
+            }
+            info = phoneInfo;
+        }
+
+        final ContactInfo updatedInfo;
+        if (info == null) {
+            // The lookup failed.
+            updatedInfo = null;
+        } else {
+            // If we did not find a matching contact, generate an empty contact info for the number.
+            if (info == ContactInfo.EMPTY) {
+                // Did not find a matching contact.
+                updatedInfo = new ContactInfo();
+                updatedInfo.number = number;
+                updatedInfo.formattedNumber = formatPhoneNumber(number, null, countryIso);
+            } else {
+                updatedInfo = info;
+            }
+        }
+        return updatedInfo;
+    }
+
+    /**
+     * Looks up a contact using the given URI.
+     * <p>
+     * It returns null if an error occurs, {@link ContactInfo#EMPTY} if no matching contact is
+     * found, or the {@link ContactInfo} for the given contact.
+     * <p>
+     * The {@link ContactInfo#formattedNumber} field is always set to {@code null} in the returned
+     * value.
+     */
+    private ContactInfo lookupContactFromUri(Uri uri) {
+        final ContactInfo info;
+        Cursor phonesCursor =
+                mContext.getContentResolver().query(
+                        uri, PhoneQuery._PROJECTION, null, null, null);
+
+        if (phonesCursor != null) {
+            try {
+                if (phonesCursor.moveToFirst()) {
+                    info = new ContactInfo();
+                    long contactId = phonesCursor.getLong(PhoneQuery.PERSON_ID);
+                    String lookupKey = phonesCursor.getString(PhoneQuery.LOOKUP_KEY);
+                    info.lookupUri = Contacts.getLookupUri(contactId, lookupKey);
+                    info.name = phonesCursor.getString(PhoneQuery.NAME);
+                    info.type = phonesCursor.getInt(PhoneQuery.PHONE_TYPE);
+                    info.label = phonesCursor.getString(PhoneQuery.LABEL);
+                    info.number = phonesCursor.getString(PhoneQuery.MATCHED_NUMBER);
+                    info.normalizedNumber = phonesCursor.getString(PhoneQuery.NORMALIZED_NUMBER);
+                    info.photoId = phonesCursor.getLong(PhoneQuery.PHOTO_ID);
+                    info.photoUri =
+                            UriUtils.parseUriOrNull(phonesCursor.getString(PhoneQuery.PHOTO_URI));
+                    info.formattedNumber = null;
+                } else {
+                    info = ContactInfo.EMPTY;
+                }
+            } finally {
+                phonesCursor.close();
+            }
+        } else {
+            // Failed to fetch the data, ignore this request.
+            info = null;
+        }
+        return info;
+    }
+
+    /**
+     * Determines the contact information for the given SIP address.
+     * <p>
+     * It returns the contact info if found.
+     * <p>
+     * If no contact corresponds to the given SIP address, returns {@link ContactInfo#EMPTY}.
+     * <p>
+     * If the lookup fails for some other reason, it returns null.
+     */
+    private ContactInfo queryContactInfoForSipAddress(String sipAddress) {
+        final ContactInfo info;
+
+        // "contactNumber" is a SIP address, so use the PhoneLookup table with the SIP parameter.
+        Uri.Builder uriBuilder = PhoneLookup.CONTENT_FILTER_URI.buildUpon();
+        uriBuilder.appendPath(Uri.encode(sipAddress));
+        uriBuilder.appendQueryParameter(PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS, "1");
+        return lookupContactFromUri(uriBuilder.build());
+    }
+
+    /**
+     * Determines the contact information for the given phone number.
+     * <p>
+     * It returns the contact info if found.
+     * <p>
+     * If no contact corresponds to the given phone number, returns {@link ContactInfo#EMPTY}.
+     * <p>
+     * If the lookup fails for some other reason, it returns null.
+     */
+    private ContactInfo queryContactInfoForPhoneNumber(String number, String countryIso) {
+        String contactNumber = number;
+        if (!TextUtils.isEmpty(countryIso)) {
+            // Normalize the number: this is needed because the PhoneLookup query below does not
+            // accept a country code as an input.
+            String numberE164 = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+            if (!TextUtils.isEmpty(numberE164)) {
+                // Only use it if the number could be formatted to E164.
+                contactNumber = numberE164;
+            }
+        }
+
+        // The "contactNumber" is a regular phone number, so use the PhoneLookup table.
+        Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(contactNumber));
+        ContactInfo info = lookupContactFromUri(uri);
+        if (info != null && info != ContactInfo.EMPTY) {
+            info.formattedNumber = formatPhoneNumber(number, null, countryIso);
+        }
+        return info;
+    }
+
+    /**
+     * Format the given phone number
+     *
+     * @param number the number to be formatted.
+     * @param normalizedNumber the normalized number of the given number.
+     * @param countryIso the ISO 3166-1 two letters country code, the country's
+     *        convention will be used to format the number if the normalized
+     *        phone is null.
+     *
+     * @return the formatted number, or the given number if it was formatted.
+     */
+    private String formatPhoneNumber(String number, String normalizedNumber,
+            String countryIso) {
+        if (TextUtils.isEmpty(number)) {
+            return "";
+        }
+        // If "number" is really a SIP address, don't try to do any formatting at all.
+        if (PhoneNumberUtils.isUriNumber(number)) {
+            return number;
+        }
+        if (TextUtils.isEmpty(countryIso)) {
+            countryIso = mCurrentCountryIso;
+        }
+        return PhoneNumberUtils.formatNumber(number, normalizedNumber, countryIso);
+    }
+}
diff --git a/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java b/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java
new file mode 100644
index 0000000..0f6fe3b
--- /dev/null
+++ b/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.PhoneLookup;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.common.io.MoreCloseables;
+import com.android.dialer.CallDetailActivity;
+import com.android.contacts.R;
+import com.google.common.collect.Maps;
+
+import java.util.Map;
+
+/**
+ * Implementation of {@link VoicemailNotifier} that shows a notification in the
+ * status bar.
+ */
+public class DefaultVoicemailNotifier implements VoicemailNotifier {
+    public static final String TAG = "DefaultVoicemailNotifier";
+
+    /** The tag used to identify notifications from this class. */
+    private static final String NOTIFICATION_TAG = "DefaultVoicemailNotifier";
+    /** The identifier of the notification of new voicemails. */
+    private static final int NOTIFICATION_ID = 1;
+
+    /** The singleton instance of {@link DefaultVoicemailNotifier}. */
+    private static DefaultVoicemailNotifier sInstance;
+
+    private final Context mContext;
+    private final NotificationManager mNotificationManager;
+    private final NewCallsQuery mNewCallsQuery;
+    private final NameLookupQuery mNameLookupQuery;
+    private final PhoneNumberHelper mPhoneNumberHelper;
+
+    /** Returns the singleton instance of the {@link DefaultVoicemailNotifier}. */
+    public static synchronized DefaultVoicemailNotifier getInstance(Context context) {
+        if (sInstance == null) {
+            NotificationManager notificationManager =
+                    (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+            ContentResolver contentResolver = context.getContentResolver();
+            sInstance = new DefaultVoicemailNotifier(context, notificationManager,
+                    createNewCallsQuery(contentResolver),
+                    createNameLookupQuery(contentResolver),
+                    createPhoneNumberHelper(context));
+        }
+        return sInstance;
+    }
+
+    private DefaultVoicemailNotifier(Context context,
+            NotificationManager notificationManager, NewCallsQuery newCallsQuery,
+            NameLookupQuery nameLookupQuery, PhoneNumberHelper phoneNumberHelper) {
+        mContext = context;
+        mNotificationManager = notificationManager;
+        mNewCallsQuery = newCallsQuery;
+        mNameLookupQuery = nameLookupQuery;
+        mPhoneNumberHelper = phoneNumberHelper;
+    }
+
+    /** Updates the notification and notifies of the call with the given URI. */
+    @Override
+    public void updateNotification(Uri newCallUri) {
+        // Lookup the list of new voicemails to include in the notification.
+        // TODO: Move this into a service, to avoid holding the receiver up.
+        final NewCall[] newCalls = mNewCallsQuery.query();
+
+        if (newCalls == null) {
+            // Query failed, just return.
+            return;
+        }
+
+        if (newCalls.length == 0) {
+            // No voicemails to notify about: clear the notification.
+            clearNotification();
+            return;
+        }
+
+        Resources resources = mContext.getResources();
+
+        // This represents a list of names to include in the notification.
+        String callers = null;
+
+        // Maps each number into a name: if a number is in the map, it has already left a more
+        // recent voicemail.
+        final Map<String, String> names = Maps.newHashMap();
+
+        // Determine the call corresponding to the new voicemail we have to notify about.
+        NewCall callToNotify = null;
+
+        // Iterate over the new voicemails to determine all the information above.
+        for (NewCall newCall : newCalls) {
+            // Check if we already know the name associated with this number.
+            String name = names.get(newCall.number);
+            if (name == null) {
+                // Look it up in the database.
+                name = mNameLookupQuery.query(newCall.number);
+                // If we cannot lookup the contact, use the number instead.
+                if (name == null) {
+                    name = mPhoneNumberHelper.getDisplayNumber(newCall.number, "").toString();
+                    if (TextUtils.isEmpty(name)) {
+                        name = newCall.number;
+                    }
+                }
+                names.put(newCall.number, name);
+                // This is a new caller. Add it to the back of the list of callers.
+                if (TextUtils.isEmpty(callers)) {
+                    callers = name;
+                } else {
+                    callers = resources.getString(
+                            R.string.notification_voicemail_callers_list, callers, name);
+                }
+            }
+            // Check if this is the new call we need to notify about.
+            if (newCallUri != null && newCallUri.equals(newCall.voicemailUri)) {
+                callToNotify = newCall;
+            }
+        }
+
+        if (newCallUri != null && callToNotify == null) {
+            Log.e(TAG, "The new call could not be found in the call log: " + newCallUri);
+        }
+
+        // Determine the title of the notification and the icon for it.
+        final String title = resources.getQuantityString(
+                R.plurals.notification_voicemail_title, newCalls.length, newCalls.length);
+        // TODO: Use the photo of contact if all calls are from the same person.
+        final int icon = android.R.drawable.stat_notify_voicemail;
+
+        Notification.Builder notificationBuilder = new Notification.Builder(mContext)
+                .setSmallIcon(icon)
+                .setContentTitle(title)
+                .setContentText(callers)
+                .setDefaults(callToNotify != null ? Notification.DEFAULT_ALL : 0)
+                .setDeleteIntent(createMarkNewVoicemailsAsOldIntent())
+                .setAutoCancel(true);
+
+        // Determine the intent to fire when the notification is clicked on.
+        final Intent contentIntent;
+        if (newCalls.length == 1) {
+            // Open the voicemail directly.
+            contentIntent = new Intent(mContext, CallDetailActivity.class);
+            contentIntent.setData(newCalls[0].callsUri);
+            contentIntent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI,
+                    newCalls[0].voicemailUri);
+            Intent playIntent = new Intent(mContext, CallDetailActivity.class);
+            playIntent.setData(newCalls[0].callsUri);
+            playIntent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI,
+                    newCalls[0].voicemailUri);
+            playIntent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK, true);
+            playIntent.putExtra(CallDetailActivity.EXTRA_FROM_NOTIFICATION, true);
+            notificationBuilder.addAction(R.drawable.ic_play_holo_dark,
+                    resources.getString(R.string.notification_action_voicemail_play),
+                    PendingIntent.getActivity(mContext, 0, playIntent, 0));
+        } else {
+            // Open the call log.
+            contentIntent = new Intent(Intent.ACTION_VIEW, Calls.CONTENT_URI);
+        }
+        notificationBuilder.setContentIntent(
+                PendingIntent.getActivity(mContext, 0, contentIntent, 0));
+
+        // The text to show in the ticker, describing the new event.
+        if (callToNotify != null) {
+            notificationBuilder.setTicker(resources.getString(
+                    R.string.notification_new_voicemail_ticker, names.get(callToNotify.number)));
+        }
+
+        mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_ID, notificationBuilder.build());
+    }
+
+    /** Creates a pending intent that marks all new voicemails as old. */
+    private PendingIntent createMarkNewVoicemailsAsOldIntent() {
+        Intent intent = new Intent(mContext, CallLogNotificationsService.class);
+        intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD);
+        return PendingIntent.getService(mContext, 0, intent, 0);
+    }
+
+    @Override
+    public void clearNotification() {
+        mNotificationManager.cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
+    }
+
+    /** Information about a new voicemail. */
+    private static final class NewCall {
+        public final Uri callsUri;
+        public final Uri voicemailUri;
+        public final String number;
+
+        public NewCall(Uri callsUri, Uri voicemailUri, String number) {
+            this.callsUri = callsUri;
+            this.voicemailUri = voicemailUri;
+            this.number = number;
+        }
+    }
+
+    /** Allows determining the new calls for which a notification should be generated. */
+    public interface NewCallsQuery {
+        /**
+         * Returns the new calls for which a notification should be generated.
+         */
+        public NewCall[] query();
+    }
+
+    /** Create a new instance of {@link NewCallsQuery}. */
+    public static NewCallsQuery createNewCallsQuery(ContentResolver contentResolver) {
+        return new DefaultNewCallsQuery(contentResolver);
+    }
+
+    /**
+     * Default implementation of {@link NewCallsQuery} that looks up the list of new calls to
+     * notify about in the call log.
+     */
+    private static final class DefaultNewCallsQuery implements NewCallsQuery {
+        private static final String[] PROJECTION = {
+            Calls._ID, Calls.NUMBER, Calls.VOICEMAIL_URI
+        };
+        private static final int ID_COLUMN_INDEX = 0;
+        private static final int NUMBER_COLUMN_INDEX = 1;
+        private static final int VOICEMAIL_URI_COLUMN_INDEX = 2;
+
+        private final ContentResolver mContentResolver;
+
+        private DefaultNewCallsQuery(ContentResolver contentResolver) {
+            mContentResolver = contentResolver;
+        }
+
+        @Override
+        public NewCall[] query() {
+            final String selection = String.format("%s = 1 AND %s = ?", Calls.NEW, Calls.TYPE);
+            final String[] selectionArgs = new String[]{ Integer.toString(Calls.VOICEMAIL_TYPE) };
+            Cursor cursor = null;
+            try {
+                cursor = mContentResolver.query(Calls.CONTENT_URI_WITH_VOICEMAIL, PROJECTION,
+                        selection, selectionArgs, Calls.DEFAULT_SORT_ORDER);
+                if (cursor == null) {
+                    return null;
+                }
+                NewCall[] newCalls = new NewCall[cursor.getCount()];
+                while (cursor.moveToNext()) {
+                    newCalls[cursor.getPosition()] = createNewCallsFromCursor(cursor);
+                }
+                return newCalls;
+            } finally {
+                MoreCloseables.closeQuietly(cursor);
+            }
+        }
+
+        /** Returns an instance of {@link NewCall} created by using the values of the cursor. */
+        private NewCall createNewCallsFromCursor(Cursor cursor) {
+            String voicemailUriString = cursor.getString(VOICEMAIL_URI_COLUMN_INDEX);
+            Uri callsUri = ContentUris.withAppendedId(
+                    Calls.CONTENT_URI_WITH_VOICEMAIL, cursor.getLong(ID_COLUMN_INDEX));
+            Uri voicemailUri = voicemailUriString == null ? null : Uri.parse(voicemailUriString);
+            return new NewCall(callsUri, voicemailUri, cursor.getString(NUMBER_COLUMN_INDEX));
+        }
+    }
+
+    /** Allows determining the name associated with a given phone number. */
+    public interface NameLookupQuery {
+        /**
+         * Returns the name associated with the given number in the contacts database, or null if
+         * the number does not correspond to any of the contacts.
+         * <p>
+         * If there are multiple contacts with the same phone number, it will return the name of one
+         * of the matching contacts.
+         */
+        public String query(String number);
+    }
+
+    /** Create a new instance of {@link NameLookupQuery}. */
+    public static NameLookupQuery createNameLookupQuery(ContentResolver contentResolver) {
+        return new DefaultNameLookupQuery(contentResolver);
+    }
+
+    /**
+     * Default implementation of {@link NameLookupQuery} that looks up the name of a contact in the
+     * contacts database.
+     */
+    private static final class DefaultNameLookupQuery implements NameLookupQuery {
+        private static final String[] PROJECTION = { PhoneLookup.DISPLAY_NAME };
+        private static final int DISPLAY_NAME_COLUMN_INDEX = 0;
+
+        private final ContentResolver mContentResolver;
+
+        private DefaultNameLookupQuery(ContentResolver contentResolver) {
+            mContentResolver = contentResolver;
+        }
+
+        @Override
+        public String query(String number) {
+            Cursor cursor = null;
+            try {
+                cursor = mContentResolver.query(
+                        Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)),
+                        PROJECTION, null, null, null);
+                if (cursor == null || !cursor.moveToFirst()) return null;
+                return cursor.getString(DISPLAY_NAME_COLUMN_INDEX);
+            } finally {
+                if (cursor != null) {
+                    cursor.close();
+                }
+            }
+        }
+    }
+
+    /**
+     * Create a new PhoneNumberHelper.
+     * <p>
+     * This will cause some Disk I/O, at least the first time it is created, so it should not be
+     * called from the main thread.
+     */
+    public static PhoneNumberHelper createPhoneNumberHelper(Context context) {
+        return new PhoneNumberHelper(context.getResources());
+    }
+}
diff --git a/src/com/android/dialer/calllog/ExtendedCursor.java b/src/com/android/dialer/calllog/ExtendedCursor.java
new file mode 100644
index 0000000..3e55aab
--- /dev/null
+++ b/src/com/android/dialer/calllog/ExtendedCursor.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import android.database.AbstractCursor;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+
+import com.android.common.io.MoreCloseables;
+
+/**
+ * Wraps a cursor to add an additional column with the same value for all rows.
+ * <p>
+ * The number of rows in the cursor and the set of columns is determined by the cursor being
+ * wrapped.
+ */
+public class ExtendedCursor extends AbstractCursor {
+    /** The cursor to wrap. */
+    private final Cursor mCursor;
+    /** The name of the additional column. */
+    private final String mColumnName;
+    /** The value to be assigned to the additional column. */
+    private final Object mValue;
+
+    /**
+     * Creates a new cursor which extends the given cursor by adding a column with a constant value.
+     *
+     * @param cursor the cursor to extend
+     * @param columnName the name of the additional column
+     * @param value the value to be assigned to the additional column
+     */
+    public ExtendedCursor(Cursor cursor, String columnName, Object value) {
+        mCursor = cursor;
+        mColumnName = columnName;
+        mValue = value;
+    }
+
+    @Override
+    public int getCount() {
+        return mCursor.getCount();
+    }
+
+    @Override
+    public String[] getColumnNames() {
+        String[] columnNames = mCursor.getColumnNames();
+        int length = columnNames.length;
+        String[] extendedColumnNames = new String[length + 1];
+        System.arraycopy(columnNames, 0, extendedColumnNames, 0, length);
+        extendedColumnNames[length] = mColumnName;
+        return extendedColumnNames;
+    }
+
+    @Override
+    public String getString(int column) {
+        if (column == mCursor.getColumnCount()) {
+            return (String) mValue;
+        }
+        return mCursor.getString(column);
+    }
+
+    @Override
+    public short getShort(int column) {
+        if (column == mCursor.getColumnCount()) {
+            return (Short) mValue;
+        }
+        return mCursor.getShort(column);
+    }
+
+    @Override
+    public int getInt(int column) {
+        if (column == mCursor.getColumnCount()) {
+            return (Integer) mValue;
+        }
+        return mCursor.getInt(column);
+    }
+
+    @Override
+    public long getLong(int column) {
+        if (column == mCursor.getColumnCount()) {
+            return (Long) mValue;
+        }
+        return mCursor.getLong(column);
+    }
+
+    @Override
+    public float getFloat(int column) {
+        if (column == mCursor.getColumnCount()) {
+            return (Float) mValue;
+        }
+        return mCursor.getFloat(column);
+    }
+
+    @Override
+    public double getDouble(int column) {
+        if (column == mCursor.getColumnCount()) {
+            return (Double) mValue;
+        }
+        return mCursor.getDouble(column);
+    }
+
+    @Override
+    public boolean isNull(int column) {
+        if (column == mCursor.getColumnCount()) {
+            return mValue == null;
+        }
+        return mCursor.isNull(column);
+    }
+
+    @Override
+    public boolean onMove(int oldPosition, int newPosition) {
+        return mCursor.moveToPosition(newPosition);
+    }
+
+    @Override
+    public void close() {
+        MoreCloseables.closeQuietly(mCursor);
+        super.close();
+    }
+
+    @Override
+    public void registerContentObserver(ContentObserver observer) {
+        mCursor.registerContentObserver(observer);
+    }
+
+    @Override
+    public void unregisterContentObserver(ContentObserver observer) {
+        mCursor.unregisterContentObserver(observer);
+    }
+
+    @Override
+    public void registerDataSetObserver(DataSetObserver observer) {
+        mCursor.registerDataSetObserver(observer);
+    }
+
+    @Override
+    public void unregisterDataSetObserver(DataSetObserver observer) {
+        mCursor.unregisterDataSetObserver(observer);
+    }
+}
diff --git a/src/com/android/dialer/calllog/IntentProvider.java b/src/com/android/dialer/calllog/IntentProvider.java
new file mode 100644
index 0000000..f43dc51
--- /dev/null
+++ b/src/com/android/dialer/calllog/IntentProvider.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.CallLog.Calls;
+
+import com.android.dialer.CallDetailActivity;
+import com.android.contacts.ContactsUtils;
+
+/**
+ * Used to create an intent to attach to an action in the call log.
+ * <p>
+ * The intent is constructed lazily with the given information.
+ */
+public abstract class IntentProvider {
+    public abstract Intent getIntent(Context context);
+
+    public static IntentProvider getReturnCallIntentProvider(final String number) {
+        return new IntentProvider() {
+            @Override
+            public Intent getIntent(Context context) {
+                return ContactsUtils.getCallIntent(number);
+            }
+        };
+    }
+
+    public static IntentProvider getPlayVoicemailIntentProvider(final long rowId,
+            final String voicemailUri) {
+        return new IntentProvider() {
+            @Override
+            public Intent getIntent(Context context) {
+                Intent intent = new Intent(context, CallDetailActivity.class);
+                intent.setData(ContentUris.withAppendedId(
+                        Calls.CONTENT_URI_WITH_VOICEMAIL, rowId));
+                if (voicemailUri != null) {
+                    intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI,
+                            Uri.parse(voicemailUri));
+                }
+                intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK, true);
+                return intent;
+            }
+        };
+    }
+
+    public static IntentProvider getCallDetailIntentProvider(
+            final CallLogAdapter adapter, final int position, final long id, final int groupSize) {
+        return new IntentProvider() {
+            @Override
+            public Intent getIntent(Context context) {
+                Cursor cursor = adapter.getCursor();
+                cursor.moveToPosition(position);
+                if (CallLogQuery.isSectionHeader(cursor)) {
+                    // Do nothing when a header is clicked.
+                    return null;
+                }
+                Intent intent = new Intent(context, CallDetailActivity.class);
+                // Check if the first item is a voicemail.
+                String voicemailUri = cursor.getString(CallLogQuery.VOICEMAIL_URI);
+                if (voicemailUri != null) {
+                    intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI,
+                            Uri.parse(voicemailUri));
+                }
+                intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK, false);
+
+                if (groupSize > 1) {
+                    // We want to restore the position in the cursor at the end.
+                    long[] ids = new long[groupSize];
+                    // Copy the ids of the rows in the group.
+                    for (int index = 0; index < groupSize; ++index) {
+                        ids[index] = cursor.getLong(CallLogQuery.ID);
+                        cursor.moveToNext();
+                    }
+                    intent.putExtra(CallDetailActivity.EXTRA_CALL_LOG_IDS, ids);
+                } else {
+                    // If there is a single item, use the direct URI for it.
+                    intent.setData(ContentUris.withAppendedId(
+                            Calls.CONTENT_URI_WITH_VOICEMAIL, id));
+                }
+                return intent;
+            }
+        };
+    }
+}
diff --git a/src/com/android/dialer/calllog/PhoneNumberHelper.java b/src/com/android/dialer/calllog/PhoneNumberHelper.java
new file mode 100644
index 0000000..70505ee
--- /dev/null
+++ b/src/com/android/dialer/calllog/PhoneNumberHelper.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import android.content.res.Resources;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+
+import com.android.contacts.R;
+import com.android.internal.telephony.CallerInfo;
+
+/**
+ * Helper for formatting and managing phone numbers.
+ */
+public class PhoneNumberHelper {
+    private final Resources mResources;
+
+    public PhoneNumberHelper(Resources resources) {
+        mResources = resources;
+    }
+
+    /** Returns true if it is possible to place a call to the given number. */
+    public boolean canPlaceCallsTo(CharSequence number) {
+        return !(TextUtils.isEmpty(number)
+                || number.equals(CallerInfo.UNKNOWN_NUMBER)
+                || number.equals(CallerInfo.PRIVATE_NUMBER)
+                || number.equals(CallerInfo.PAYPHONE_NUMBER));
+    }
+
+    /** Returns true if it is possible to send an SMS to the given number. */
+    public boolean canSendSmsTo(CharSequence number) {
+        return canPlaceCallsTo(number) && !isVoicemailNumber(number) && !isSipNumber(number);
+    }
+
+    /**
+     * Returns the string to display for the given phone number.
+     *
+     * @param number the number to display
+     * @param formattedNumber the formatted number if available, may be null
+     */
+    public CharSequence getDisplayNumber(CharSequence number, CharSequence formattedNumber) {
+        if (TextUtils.isEmpty(number)) {
+            return "";
+        }
+        if (number.equals(CallerInfo.UNKNOWN_NUMBER)) {
+            return mResources.getString(R.string.unknown);
+        }
+        if (number.equals(CallerInfo.PRIVATE_NUMBER)) {
+            return mResources.getString(R.string.private_num);
+        }
+        if (number.equals(CallerInfo.PAYPHONE_NUMBER)) {
+            return mResources.getString(R.string.payphone);
+        }
+        if (isVoicemailNumber(number)) {
+            return mResources.getString(R.string.voicemail);
+        }
+        if (TextUtils.isEmpty(formattedNumber)) {
+            return number;
+        } else {
+            return formattedNumber;
+        }
+    }
+
+    /**
+     * Returns true if the given number is the number of the configured voicemail.
+     * To be able to mock-out this, it is not a static method.
+     */
+    public boolean isVoicemailNumber(CharSequence number) {
+        return PhoneNumberUtils.isVoiceMailNumber(number.toString());
+    }
+
+    /**
+     * Returns true if the given number is a SIP address.
+     * To be able to mock-out this, it is not a static method.
+     */
+    public boolean isSipNumber(CharSequence number) {
+        return PhoneNumberUtils.isUriNumber(number.toString());
+    }
+}
diff --git a/src/com/android/dialer/calllog/PhoneQuery.java b/src/com/android/dialer/calllog/PhoneQuery.java
new file mode 100644
index 0000000..7190522
--- /dev/null
+++ b/src/com/android/dialer/calllog/PhoneQuery.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import android.provider.ContactsContract.PhoneLookup;
+
+/**
+ * The query to look up the {@link ContactInfo} for a given number in the Call Log.
+ */
+final class PhoneQuery {
+    public static final String[] _PROJECTION = new String[] {
+            PhoneLookup._ID,
+            PhoneLookup.DISPLAY_NAME,
+            PhoneLookup.TYPE,
+            PhoneLookup.LABEL,
+            PhoneLookup.NUMBER,
+            PhoneLookup.NORMALIZED_NUMBER,
+            PhoneLookup.PHOTO_ID,
+            PhoneLookup.LOOKUP_KEY,
+            PhoneLookup.PHOTO_URI};
+
+    public static final int PERSON_ID = 0;
+    public static final int NAME = 1;
+    public static final int PHONE_TYPE = 2;
+    public static final int LABEL = 3;
+    public static final int MATCHED_NUMBER = 4;
+    public static final int NORMALIZED_NUMBER = 5;
+    public static final int PHOTO_ID = 6;
+    public static final int LOOKUP_KEY = 7;
+    public static final int PHOTO_URI = 8;
+}
diff --git a/src/com/android/dialer/calllog/VoicemailNotifier.java b/src/com/android/dialer/calllog/VoicemailNotifier.java
new file mode 100644
index 0000000..d433cf7
--- /dev/null
+++ b/src/com/android/dialer/calllog/VoicemailNotifier.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import android.net.Uri;
+
+/**
+ * Handles notifications for voicemails.
+ */
+public interface VoicemailNotifier {
+    /**
+     * Updates the notification and clears it if there are no new voicemails.
+     * <p>
+     * If the given URI corresponds to a new voicemail, also notifies about it.
+     * <p>
+     * It is not safe to call this method from the main thread.
+     *
+     * @param newCallUri URI of the new call, may be null
+     */
+    public void updateNotification(Uri newCallUri);
+
+    /** Clears the new voicemail notification. */
+    public void clearNotification();
+}
diff --git a/src/com/android/dialer/dialpad/DialpadFragment.java b/src/com/android/dialer/dialpad/DialpadFragment.java
new file mode 100644
index 0000000..a4f3599
--- /dev/null
+++ b/src/com/android/dialer/dialpad/DialpadFragment.java
@@ -0,0 +1,1629 @@
+/*
+ * Copyright (C) 2011 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.dialer.dialpad;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.AudioManager;
+import android.media.ToneGenerator;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemProperties;
+import android.provider.Contacts.Intents.Insert;
+import android.provider.Contacts.People;
+import android.provider.Contacts.Phones;
+import android.provider.Contacts.PhonesColumns;
+import android.provider.Settings;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyManager;
+import android.text.Editable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.text.method.DialerKeyListener;
+import android.text.style.RelativeSizeSpan;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.PopupMenu;
+import android.widget.TextView;
+
+import com.android.contacts.ContactsUtils;
+import com.android.contacts.R;
+import com.android.contacts.SpecialCharSequenceMgr;
+import com.android.dialer.DialtactsActivity;
+import com.android.contacts.util.Constants;
+import com.android.contacts.util.PhoneNumberFormatter;
+import com.android.contacts.util.StopWatch;
+import com.android.internal.telephony.ITelephony;
+import com.android.phone.CallLogAsync;
+import com.android.phone.HapticFeedback;
+
+/**
+ * Fragment that displays a twelve-key phone dialpad.
+ */
+public class DialpadFragment extends Fragment
+        implements View.OnClickListener,
+        View.OnLongClickListener, View.OnKeyListener,
+        AdapterView.OnItemClickListener, TextWatcher,
+        PopupMenu.OnMenuItemClickListener,
+        DialpadImageButton.OnPressedListener {
+    private static final String TAG = DialpadFragment.class.getSimpleName();
+
+    private static final boolean DEBUG = DialtactsActivity.DEBUG;
+
+    private static final String EMPTY_NUMBER = "";
+
+    /** The length of DTMF tones in milliseconds */
+    private static final int TONE_LENGTH_MS = 150;
+    private static final int TONE_LENGTH_INFINITE = -1;
+
+    /** The DTMF tone volume relative to other sounds in the stream */
+    private static final int TONE_RELATIVE_VOLUME = 80;
+
+    /** Stream type used to play the DTMF tones off call, and mapped to the volume control keys */
+    private static final int DIAL_TONE_STREAM_TYPE = AudioManager.STREAM_DTMF;
+
+    /**
+     * View (usually FrameLayout) containing mDigits field. This can be null, in which mDigits
+     * isn't enclosed by the container.
+     */
+    private View mDigitsContainer;
+    private EditText mDigits;
+
+    /** Remembers if we need to clear digits field when the screen is completely gone. */
+    private boolean mClearDigitsOnStop;
+
+    private View mDelete;
+    private ToneGenerator mToneGenerator;
+    private final Object mToneGeneratorLock = new Object();
+    private View mDialpad;
+    /**
+     * Remembers the number of dialpad buttons which are pressed at this moment.
+     * If it becomes 0, meaning no buttons are pressed, we'll call
+     * {@link ToneGenerator#stopTone()}; the method shouldn't be called unless the last key is
+     * released.
+     */
+    private int mDialpadPressCount;
+
+    private View mDialButtonContainer;
+    private View mDialButton;
+    private ListView mDialpadChooser;
+    private DialpadChooserAdapter mDialpadChooserAdapter;
+
+    /**
+     * Regular expression prohibiting manual phone call. Can be empty, which means "no rule".
+     */
+    private String mProhibitedPhoneNumberRegexp;
+
+
+    // Last number dialed, retrieved asynchronously from the call DB
+    // in onCreate. This number is displayed when the user hits the
+    // send key and cleared in onPause.
+    private final CallLogAsync mCallLog = new CallLogAsync();
+    private String mLastNumberDialed = EMPTY_NUMBER;
+
+    // determines if we want to playback local DTMF tones.
+    private boolean mDTMFToneEnabled;
+
+    // Vibration (haptic feedback) for dialer key presses.
+    private final HapticFeedback mHaptic = new HapticFeedback();
+
+    /** Identifier for the "Add Call" intent extra. */
+    private static final String ADD_CALL_MODE_KEY = "add_call_mode";
+
+    /**
+     * Identifier for intent extra for sending an empty Flash message for
+     * CDMA networks. This message is used by the network to simulate a
+     * press/depress of the "hookswitch" of a landline phone. Aka "empty flash".
+     *
+     * TODO: Using an intent extra to tell the phone to send this flash is a
+     * temporary measure. To be replaced with an ITelephony call in the future.
+     * TODO: Keep in sync with the string defined in OutgoingCallBroadcaster.java
+     * in Phone app until this is replaced with the ITelephony API.
+     */
+    private static final String EXTRA_SEND_EMPTY_FLASH
+            = "com.android.phone.extra.SEND_EMPTY_FLASH";
+
+    private String mCurrentCountryIso;
+
+    private final PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
+        /**
+         * Listen for phone state changes so that we can take down the
+         * "dialpad chooser" if the phone becomes idle while the
+         * chooser UI is visible.
+         */
+        @Override
+        public void onCallStateChanged(int state, String incomingNumber) {
+            // Log.i(TAG, "PhoneStateListener.onCallStateChanged: "
+            //       + state + ", '" + incomingNumber + "'");
+            if ((state == TelephonyManager.CALL_STATE_IDLE) && dialpadChooserVisible()) {
+                // Log.i(TAG, "Call ended with dialpad chooser visible!  Taking it down...");
+                // Note there's a race condition in the UI here: the
+                // dialpad chooser could conceivably disappear (on its
+                // own) at the exact moment the user was trying to select
+                // one of the choices, which would be confusing.  (But at
+                // least that's better than leaving the dialpad chooser
+                // onscreen, but useless...)
+                showDialpadChooser(false);
+            }
+        }
+    };
+
+    private boolean mWasEmptyBeforeTextChange;
+
+    /**
+     * This field is set to true while processing an incoming DIAL intent, in order to make sure
+     * that SpecialCharSequenceMgr actions can be triggered by user input but *not* by a
+     * tel: URI passed by some other app.  It will be set to false when all digits are cleared.
+     */
+    private boolean mDigitsFilledByIntent;
+
+    private static final String PREF_DIGITS_FILLED_BY_INTENT = "pref_digits_filled_by_intent";
+
+    @Override
+    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+        mWasEmptyBeforeTextChange = TextUtils.isEmpty(s);
+    }
+
+    @Override
+    public void onTextChanged(CharSequence input, int start, int before, int changeCount) {
+        if (mWasEmptyBeforeTextChange != TextUtils.isEmpty(input)) {
+            final Activity activity = getActivity();
+            if (activity != null) {
+                activity.invalidateOptionsMenu();
+            }
+        }
+
+        // DTMF Tones do not need to be played here any longer -
+        // the DTMF dialer handles that functionality now.
+    }
+
+    @Override
+    public void afterTextChanged(Editable input) {
+        // When DTMF dialpad buttons are being pressed, we delay SpecialCharSequencMgr sequence,
+        // since some of SpecialCharSequenceMgr's behavior is too abrupt for the "touch-down"
+        // behavior.
+        if (!mDigitsFilledByIntent &&
+                SpecialCharSequenceMgr.handleChars(getActivity(), input.toString(), mDigits)) {
+            // A special sequence was entered, clear the digits
+            mDigits.getText().clear();
+        }
+
+        if (isDigitsEmpty()) {
+            mDigitsFilledByIntent = false;
+            mDigits.setCursorVisible(false);
+        }
+
+        updateDialAndDeleteButtonEnabledState();
+    }
+
+    @Override
+    public void onCreate(Bundle state) {
+        super.onCreate(state);
+
+        mCurrentCountryIso = ContactsUtils.getCurrentCountryIso(getActivity());
+
+        try {
+            mHaptic.init(getActivity(),
+                         getResources().getBoolean(R.bool.config_enable_dialer_key_vibration));
+        } catch (Resources.NotFoundException nfe) {
+             Log.e(TAG, "Vibrate control bool missing.", nfe);
+        }
+
+        setHasOptionsMenu(true);
+
+        mProhibitedPhoneNumberRegexp = getResources().getString(
+                R.string.config_prohibited_phone_number_regexp);
+
+        if (state != null) {
+            mDigitsFilledByIntent = state.getBoolean(PREF_DIGITS_FILLED_BY_INTENT);
+        }
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
+        View fragmentView = inflater.inflate(R.layout.dialpad_fragment, container, false);
+
+        // Load up the resources for the text field.
+        Resources r = getResources();
+
+        mDigitsContainer = fragmentView.findViewById(R.id.digits_container);
+        mDigits = (EditText) fragmentView.findViewById(R.id.digits);
+        mDigits.setKeyListener(DialerKeyListener.getInstance());
+        mDigits.setOnClickListener(this);
+        mDigits.setOnKeyListener(this);
+        mDigits.setOnLongClickListener(this);
+        mDigits.addTextChangedListener(this);
+
+        PhoneNumberFormatter.setPhoneNumberFormattingTextWatcher(getActivity(), mDigits);
+
+        // Check for the presence of the keypad
+        View oneButton = fragmentView.findViewById(R.id.one);
+        if (oneButton != null) {
+            setupKeypad(fragmentView);
+        }
+
+        DisplayMetrics dm = getResources().getDisplayMetrics();
+        int minCellSize = (int) (56 * dm.density); // 56dip == minimum size of menu buttons
+        int cellCount = dm.widthPixels / minCellSize;
+        int fakeMenuItemWidth = dm.widthPixels / cellCount;
+        mDialButtonContainer = fragmentView.findViewById(R.id.dialButtonContainer);
+        // If in portrait, add padding to the dial button since we need space for the
+        // search and menu/overflow buttons.
+        if (mDialButtonContainer != null && !ContactsUtils.isLandscape(this.getActivity())) {
+            mDialButtonContainer.setPadding(
+                    fakeMenuItemWidth, mDialButtonContainer.getPaddingTop(),
+                    fakeMenuItemWidth, mDialButtonContainer.getPaddingBottom());
+        }
+        mDialButton = fragmentView.findViewById(R.id.dialButton);
+        if (r.getBoolean(R.bool.config_show_onscreen_dial_button)) {
+            mDialButton.setOnClickListener(this);
+            mDialButton.setOnLongClickListener(this);
+        } else {
+            mDialButton.setVisibility(View.GONE); // It's VISIBLE by default
+            mDialButton = null;
+        }
+
+        mDelete = fragmentView.findViewById(R.id.deleteButton);
+        if (mDelete != null) {
+            mDelete.setOnClickListener(this);
+            mDelete.setOnLongClickListener(this);
+        }
+
+        mDialpad = fragmentView.findViewById(R.id.dialpad);  // This is null in landscape mode.
+
+        // In landscape we put the keyboard in phone mode.
+        if (null == mDialpad) {
+            mDigits.setInputType(android.text.InputType.TYPE_CLASS_PHONE);
+        } else {
+            mDigits.setCursorVisible(false);
+        }
+
+        // Set up the "dialpad chooser" UI; see showDialpadChooser().
+        mDialpadChooser = (ListView) fragmentView.findViewById(R.id.dialpadChooser);
+        mDialpadChooser.setOnItemClickListener(this);
+
+        configureScreenFromIntent(getActivity().getIntent());
+
+        return fragmentView;
+    }
+
+    private boolean isLayoutReady() {
+        return mDigits != null;
+    }
+
+    public EditText getDigitsWidget() {
+        return mDigits;
+    }
+
+    /**
+     * @return true when {@link #mDigits} is actually filled by the Intent.
+     */
+    private boolean fillDigitsIfNecessary(Intent intent) {
+        final String action = intent.getAction();
+        if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)) {
+            Uri uri = intent.getData();
+            if (uri != null) {
+                if (Constants.SCHEME_TEL.equals(uri.getScheme())) {
+                    // Put the requested number into the input area
+                    String data = uri.getSchemeSpecificPart();
+                    // Remember it is filled via Intent.
+                    mDigitsFilledByIntent = true;
+                    setFormattedDigits(data, null);
+                    return true;
+                } else {
+                    String type = intent.getType();
+                    if (People.CONTENT_ITEM_TYPE.equals(type)
+                            || Phones.CONTENT_ITEM_TYPE.equals(type)) {
+                        // Query the phone number
+                        Cursor c = getActivity().getContentResolver().query(intent.getData(),
+                                new String[] {PhonesColumns.NUMBER, PhonesColumns.NUMBER_KEY},
+                                null, null, null);
+                        if (c != null) {
+                            try {
+                                if (c.moveToFirst()) {
+                                    // Remember it is filled via Intent.
+                                    mDigitsFilledByIntent = true;
+                                    // Put the number into the input area
+                                    setFormattedDigits(c.getString(0), c.getString(1));
+                                    return true;
+                                }
+                            } finally {
+                                c.close();
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * @see #showDialpadChooser(boolean)
+     */
+    private static boolean needToShowDialpadChooser(Intent intent, boolean isAddCallMode) {
+        final String action = intent.getAction();
+
+        boolean needToShowDialpadChooser = false;
+
+        if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)) {
+            Uri uri = intent.getData();
+            if (uri == null) {
+                // ACTION_DIAL or ACTION_VIEW with no data.
+                // This behaves basically like ACTION_MAIN: If there's
+                // already an active call, bring up an intermediate UI to
+                // make the user confirm what they really want to do.
+                // Be sure *not* to show the dialpad chooser if this is an
+                // explicit "Add call" action, though.
+                if (!isAddCallMode && phoneIsInUse()) {
+                    needToShowDialpadChooser = true;
+                }
+            }
+        } else if (Intent.ACTION_MAIN.equals(action)) {
+            // The MAIN action means we're bringing up a blank dialer
+            // (e.g. by selecting the Home shortcut, or tabbing over from
+            // Contacts or Call log.)
+            //
+            // At this point, IF there's already an active call, there's a
+            // good chance that the user got here accidentally (but really
+            // wanted the in-call dialpad instead).  So we bring up an
+            // intermediate UI to make the user confirm what they really
+            // want to do.
+            if (phoneIsInUse()) {
+                // Log.i(TAG, "resolveIntent(): phone is in use; showing dialpad chooser!");
+                needToShowDialpadChooser = true;
+            }
+        }
+
+        return needToShowDialpadChooser;
+    }
+
+    private static boolean isAddCallMode(Intent intent) {
+        final String action = intent.getAction();
+        if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)) {
+            // see if we are "adding a call" from the InCallScreen; false by default.
+            return intent.getBooleanExtra(ADD_CALL_MODE_KEY, false);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Checks the given Intent and changes dialpad's UI state. For example, if the Intent requires
+     * the screen to enter "Add Call" mode, this method will show correct UI for the mode.
+     */
+    public void configureScreenFromIntent(Intent intent) {
+        if (!isLayoutReady()) {
+            // This happens typically when parent's Activity#onNewIntent() is called while
+            // Fragment#onCreateView() isn't called yet, and thus we cannot configure Views at
+            // this point. onViewCreate() should call this method after preparing layouts, so
+            // just ignore this call now.
+            Log.i(TAG,
+                    "Screen configuration is requested before onCreateView() is called. Ignored");
+            return;
+        }
+
+        boolean needToShowDialpadChooser = false;
+
+        final boolean isAddCallMode = isAddCallMode(intent);
+        if (!isAddCallMode) {
+            final boolean digitsFilled = fillDigitsIfNecessary(intent);
+            if (!digitsFilled) {
+                needToShowDialpadChooser = needToShowDialpadChooser(intent, isAddCallMode);
+            }
+        }
+        showDialpadChooser(needToShowDialpadChooser);
+    }
+
+    /**
+     * Sets formatted digits to digits field.
+     */
+    private void setFormattedDigits(String data, String normalizedNumber) {
+        // strip the non-dialable numbers out of the data string.
+        String dialString = PhoneNumberUtils.extractNetworkPortion(data);
+        dialString =
+                PhoneNumberUtils.formatNumber(dialString, normalizedNumber, mCurrentCountryIso);
+        if (!TextUtils.isEmpty(dialString)) {
+            Editable digits = mDigits.getText();
+            digits.replace(0, digits.length(), dialString);
+            // for some reason this isn't getting called in the digits.replace call above..
+            // but in any case, this will make sure the background drawable looks right
+            afterTextChanged(digits);
+        }
+    }
+
+    private void setupKeypad(View fragmentView) {
+        int[] buttonIds = new int[] { R.id.one, R.id.two, R.id.three, R.id.four, R.id.five,
+                R.id.six, R.id.seven, R.id.eight, R.id.nine, R.id.zero, R.id.star, R.id.pound};
+        for (int id : buttonIds) {
+            ((DialpadImageButton) fragmentView.findViewById(id)).setOnPressedListener(this);
+        }
+
+        // Long-pressing one button will initiate Voicemail.
+        fragmentView.findViewById(R.id.one).setOnLongClickListener(this);
+
+        // Long-pressing zero button will enter '+' instead.
+        fragmentView.findViewById(R.id.zero).setOnLongClickListener(this);
+
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+
+        final StopWatch stopWatch = StopWatch.start("Dialpad.onResume");
+
+        // Query the last dialed number. Do it first because hitting
+        // the DB is 'slow'. This call is asynchronous.
+        queryLastOutgoingCall();
+
+        stopWatch.lap("qloc");
+
+        // retrieve the DTMF tone play back setting.
+        mDTMFToneEnabled = Settings.System.getInt(getActivity().getContentResolver(),
+                Settings.System.DTMF_TONE_WHEN_DIALING, 1) == 1;
+
+        stopWatch.lap("dtwd");
+
+        // Retrieve the haptic feedback setting.
+        mHaptic.checkSystemSetting();
+
+        stopWatch.lap("hptc");
+
+        // if the mToneGenerator creation fails, just continue without it.  It is
+        // a local audio signal, and is not as important as the dtmf tone itself.
+        synchronized (mToneGeneratorLock) {
+            if (mToneGenerator == null) {
+                try {
+                    mToneGenerator = new ToneGenerator(DIAL_TONE_STREAM_TYPE, TONE_RELATIVE_VOLUME);
+                } catch (RuntimeException e) {
+                    Log.w(TAG, "Exception caught while creating local tone generator: " + e);
+                    mToneGenerator = null;
+                }
+            }
+        }
+        stopWatch.lap("tg");
+        // Prevent unnecessary confusion. Reset the press count anyway.
+        mDialpadPressCount = 0;
+
+        Activity parent = getActivity();
+        if (parent instanceof DialtactsActivity) {
+            // See if we were invoked with a DIAL intent. If we were, fill in the appropriate
+            // digits in the dialer field.
+            fillDigitsIfNecessary(parent.getIntent());
+        }
+
+        stopWatch.lap("fdin");
+
+        // While we're in the foreground, listen for phone state changes,
+        // purely so that we can take down the "dialpad chooser" if the
+        // phone becomes idle while the chooser UI is visible.
+        TelephonyManager telephonyManager =
+                (TelephonyManager) getActivity().getSystemService(Context.TELEPHONY_SERVICE);
+        telephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
+
+        stopWatch.lap("tm");
+
+        // Potentially show hint text in the mDigits field when the user
+        // hasn't typed any digits yet.  (If there's already an active call,
+        // this hint text will remind the user that he's about to add a new
+        // call.)
+        //
+        // TODO: consider adding better UI for the case where *both* lines
+        // are currently in use.  (Right now we let the user try to add
+        // another call, but that call is guaranteed to fail.  Perhaps the
+        // entire dialer UI should be disabled instead.)
+        if (phoneIsInUse()) {
+            final SpannableString hint = new SpannableString(
+                    getActivity().getString(R.string.dialerDialpadHintText));
+            hint.setSpan(new RelativeSizeSpan(0.8f), 0, hint.length(), 0);
+            mDigits.setHint(hint);
+        } else {
+            // Common case; no hint necessary.
+            mDigits.setHint(null);
+
+            // Also, a sanity-check: the "dialpad chooser" UI should NEVER
+            // be visible if the phone is idle!
+            showDialpadChooser(false);
+        }
+
+        stopWatch.lap("hnt");
+
+        updateDialAndDeleteButtonEnabledState();
+
+        stopWatch.lap("bes");
+
+        stopWatch.stopAndLog(TAG, 50);
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+
+        // Stop listening for phone state changes.
+        TelephonyManager telephonyManager =
+                (TelephonyManager) getActivity().getSystemService(Context.TELEPHONY_SERVICE);
+        telephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
+
+        // Make sure we don't leave this activity with a tone still playing.
+        stopTone();
+        // Just in case reset the counter too.
+        mDialpadPressCount = 0;
+
+        synchronized (mToneGeneratorLock) {
+            if (mToneGenerator != null) {
+                mToneGenerator.release();
+                mToneGenerator = null;
+            }
+        }
+        // TODO: I wonder if we should not check if the AsyncTask that
+        // lookup the last dialed number has completed.
+        mLastNumberDialed = EMPTY_NUMBER;  // Since we are going to query again, free stale number.
+
+        SpecialCharSequenceMgr.cleanup();
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        if (mClearDigitsOnStop) {
+            mClearDigitsOnStop = false;
+            mDigits.getText().clear();
+        }
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putBoolean(PREF_DIGITS_FILLED_BY_INTENT, mDigitsFilledByIntent);
+    }
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        super.onCreateOptionsMenu(menu, inflater);
+        // Landscape dialer uses the real actionbar menu, whereas portrait uses a fake one
+        // that is created using constructPopupMenu()
+        if (ContactsUtils.isLandscape(this.getActivity()) ||
+                ViewConfiguration.get(getActivity()).hasPermanentMenuKey() &&
+                isLayoutReady() && mDialpadChooser != null) {
+            inflater.inflate(R.menu.dialpad_options, menu);
+        }
+    }
+
+    @Override
+    public void onPrepareOptionsMenu(Menu menu) {
+        // Hardware menu key should be available and Views should already be ready.
+        if (ContactsUtils.isLandscape(this.getActivity()) ||
+                ViewConfiguration.get(getActivity()).hasPermanentMenuKey() &&
+                isLayoutReady() && mDialpadChooser != null) {
+            setupMenuItems(menu);
+        }
+    }
+
+    private void setupMenuItems(Menu menu) {
+        final MenuItem callSettingsMenuItem = menu.findItem(R.id.menu_call_settings_dialpad);
+        final MenuItem addToContactMenuItem = menu.findItem(R.id.menu_add_contacts);
+        final MenuItem twoSecPauseMenuItem = menu.findItem(R.id.menu_2s_pause);
+        final MenuItem waitMenuItem = menu.findItem(R.id.menu_add_wait);
+
+        // Check if all the menu items are inflated correctly. As a shortcut, we assume all menu
+        // items are ready if the first item is non-null.
+        if (callSettingsMenuItem == null) {
+            return;
+        }
+
+        final Activity activity = getActivity();
+        if (activity != null && ViewConfiguration.get(activity).hasPermanentMenuKey()) {
+            // Call settings should be available via its parent Activity.
+            callSettingsMenuItem.setVisible(false);
+        } else {
+            callSettingsMenuItem.setVisible(true);
+            callSettingsMenuItem.setIntent(DialtactsActivity.getCallSettingsIntent());
+        }
+
+        // We show "add to contacts", "2sec pause", and "add wait" menus only when the user is
+        // seeing usual dialpads and has typed at least one digit.
+        // We never show a menu if the "choose dialpad" UI is up.
+        if (dialpadChooserVisible() || isDigitsEmpty()) {
+            addToContactMenuItem.setVisible(false);
+            twoSecPauseMenuItem.setVisible(false);
+            waitMenuItem.setVisible(false);
+        } else {
+            final CharSequence digits = mDigits.getText();
+
+            // Put the current digits string into an intent
+            addToContactMenuItem.setIntent(getAddToContactIntent(digits));
+            addToContactMenuItem.setVisible(true);
+
+            // Check out whether to show Pause & Wait option menu items
+            int selectionStart;
+            int selectionEnd;
+            String strDigits = digits.toString();
+
+            selectionStart = mDigits.getSelectionStart();
+            selectionEnd = mDigits.getSelectionEnd();
+
+            if (selectionStart != -1) {
+                if (selectionStart > selectionEnd) {
+                    // swap it as we want start to be less then end
+                    int tmp = selectionStart;
+                    selectionStart = selectionEnd;
+                    selectionEnd = tmp;
+                }
+
+                if (selectionStart != 0) {
+                    // Pause can be visible if cursor is not in the begining
+                    twoSecPauseMenuItem.setVisible(true);
+
+                    // For Wait to be visible set of condition to meet
+                    waitMenuItem.setVisible(showWait(selectionStart, selectionEnd, strDigits));
+                } else {
+                    // cursor in the beginning both pause and wait to be invisible
+                    twoSecPauseMenuItem.setVisible(false);
+                    waitMenuItem.setVisible(false);
+                }
+            } else {
+                twoSecPauseMenuItem.setVisible(true);
+
+                // cursor is not selected so assume new digit is added to the end
+                int strLength = strDigits.length();
+                waitMenuItem.setVisible(showWait(strLength, strLength, strDigits));
+            }
+        }
+    }
+
+    private static Intent getAddToContactIntent(CharSequence digits) {
+        final Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
+        intent.putExtra(Insert.PHONE, digits);
+        intent.setType(People.CONTENT_ITEM_TYPE);
+        return intent;
+    }
+
+    private void keyPressed(int keyCode) {
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_1:
+                playTone(ToneGenerator.TONE_DTMF_1, TONE_LENGTH_INFINITE);
+                break;
+            case KeyEvent.KEYCODE_2:
+                playTone(ToneGenerator.TONE_DTMF_2, TONE_LENGTH_INFINITE);
+                break;
+            case KeyEvent.KEYCODE_3:
+                playTone(ToneGenerator.TONE_DTMF_3, TONE_LENGTH_INFINITE);
+                break;
+            case KeyEvent.KEYCODE_4:
+                playTone(ToneGenerator.TONE_DTMF_4, TONE_LENGTH_INFINITE);
+                break;
+            case KeyEvent.KEYCODE_5:
+                playTone(ToneGenerator.TONE_DTMF_5, TONE_LENGTH_INFINITE);
+                break;
+            case KeyEvent.KEYCODE_6:
+                playTone(ToneGenerator.TONE_DTMF_6, TONE_LENGTH_INFINITE);
+                break;
+            case KeyEvent.KEYCODE_7:
+                playTone(ToneGenerator.TONE_DTMF_7, TONE_LENGTH_INFINITE);
+                break;
+            case KeyEvent.KEYCODE_8:
+                playTone(ToneGenerator.TONE_DTMF_8, TONE_LENGTH_INFINITE);
+                break;
+            case KeyEvent.KEYCODE_9:
+                playTone(ToneGenerator.TONE_DTMF_9, TONE_LENGTH_INFINITE);
+                break;
+            case KeyEvent.KEYCODE_0:
+                playTone(ToneGenerator.TONE_DTMF_0, TONE_LENGTH_INFINITE);
+                break;
+            case KeyEvent.KEYCODE_POUND:
+                playTone(ToneGenerator.TONE_DTMF_P, TONE_LENGTH_INFINITE);
+                break;
+            case KeyEvent.KEYCODE_STAR:
+                playTone(ToneGenerator.TONE_DTMF_S, TONE_LENGTH_INFINITE);
+                break;
+            default:
+                break;
+        }
+
+        mHaptic.vibrate();
+        KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
+        mDigits.onKeyDown(keyCode, event);
+
+        // If the cursor is at the end of the text we hide it.
+        final int length = mDigits.length();
+        if (length == mDigits.getSelectionStart() && length == mDigits.getSelectionEnd()) {
+            mDigits.setCursorVisible(false);
+        }
+    }
+
+    @Override
+    public boolean onKey(View view, int keyCode, KeyEvent event) {
+        switch (view.getId()) {
+            case R.id.digits:
+                if (keyCode == KeyEvent.KEYCODE_ENTER) {
+                    dialButtonPressed();
+                    return true;
+                }
+                break;
+        }
+        return false;
+    }
+
+    /**
+     * When a key is pressed, we start playing DTMF tone, do vibration, and enter the digit
+     * immediately. When a key is released, we stop the tone. Note that the "key press" event will
+     * be delivered by the system with certain amount of delay, it won't be synced with user's
+     * actual "touch-down" behavior.
+     */
+    @Override
+    public void onPressed(View view, boolean pressed) {
+        if (DEBUG) Log.d(TAG, "onPressed(). view: " + view + ", pressed: " + pressed);
+        if (pressed) {
+            switch (view.getId()) {
+                case R.id.one: {
+                    keyPressed(KeyEvent.KEYCODE_1);
+                    break;
+                }
+                case R.id.two: {
+                    keyPressed(KeyEvent.KEYCODE_2);
+                    break;
+                }
+                case R.id.three: {
+                    keyPressed(KeyEvent.KEYCODE_3);
+                    break;
+                }
+                case R.id.four: {
+                    keyPressed(KeyEvent.KEYCODE_4);
+                    break;
+                }
+                case R.id.five: {
+                    keyPressed(KeyEvent.KEYCODE_5);
+                    break;
+                }
+                case R.id.six: {
+                    keyPressed(KeyEvent.KEYCODE_6);
+                    break;
+                }
+                case R.id.seven: {
+                    keyPressed(KeyEvent.KEYCODE_7);
+                    break;
+                }
+                case R.id.eight: {
+                    keyPressed(KeyEvent.KEYCODE_8);
+                    break;
+                }
+                case R.id.nine: {
+                    keyPressed(KeyEvent.KEYCODE_9);
+                    break;
+                }
+                case R.id.zero: {
+                    keyPressed(KeyEvent.KEYCODE_0);
+                    break;
+                }
+                case R.id.pound: {
+                    keyPressed(KeyEvent.KEYCODE_POUND);
+                    break;
+                }
+                case R.id.star: {
+                    keyPressed(KeyEvent.KEYCODE_STAR);
+                    break;
+                }
+                default: {
+                    Log.wtf(TAG, "Unexpected onTouch(ACTION_DOWN) event from: " + view);
+                    break;
+                }
+            }
+            mDialpadPressCount++;
+        } else {
+            view.jumpDrawablesToCurrentState();
+            mDialpadPressCount--;
+            if (mDialpadPressCount < 0) {
+                // e.g.
+                // - when the user action is detected as horizontal swipe, at which only
+                //   "up" event is thrown.
+                // - when the user long-press '0' button, at which dialpad will decrease this count
+                //   while it still gets press-up event here.
+                if (DEBUG) Log.d(TAG, "mKeyPressCount become negative.");
+                stopTone();
+                mDialpadPressCount = 0;
+            } else if (mDialpadPressCount == 0) {
+                stopTone();
+            }
+        }
+    }
+
+    @Override
+    public void onClick(View view) {
+        switch (view.getId()) {
+            case R.id.deleteButton: {
+                keyPressed(KeyEvent.KEYCODE_DEL);
+                return;
+            }
+            case R.id.dialButton: {
+                mHaptic.vibrate();  // Vibrate here too, just like we do for the regular keys
+                dialButtonPressed();
+                return;
+            }
+            case R.id.digits: {
+                if (!isDigitsEmpty()) {
+                    mDigits.setCursorVisible(true);
+                }
+                return;
+            }
+            default: {
+                Log.wtf(TAG, "Unexpected onClick() event from: " + view);
+                return;
+            }
+        }
+    }
+
+    public PopupMenu constructPopupMenu(View anchorView) {
+        final Context context = getActivity();
+        if (context == null) {
+            return null;
+        }
+        final PopupMenu popupMenu = new PopupMenu(context, anchorView);
+        final Menu menu = popupMenu.getMenu();
+        popupMenu.inflate(R.menu.dialpad_options);
+        popupMenu.setOnMenuItemClickListener(this);
+        setupMenuItems(menu);
+        return popupMenu;
+    }
+
+    @Override
+    public boolean onLongClick(View view) {
+        final Editable digits = mDigits.getText();
+        final int id = view.getId();
+        switch (id) {
+            case R.id.deleteButton: {
+                digits.clear();
+                // TODO: The framework forgets to clear the pressed
+                // status of disabled button. Until this is fixed,
+                // clear manually the pressed status. b/2133127
+                mDelete.setPressed(false);
+                return true;
+            }
+            case R.id.one: {
+                // '1' may be already entered since we rely on onTouch() event for numeric buttons.
+                // Just for safety we also check if the digits field is empty or not.
+                if (isDigitsEmpty() || TextUtils.equals(mDigits.getText(), "1")) {
+                    // We'll try to initiate voicemail and thus we want to remove irrelevant string.
+                    removePreviousDigitIfPossible();
+
+                    if (isVoicemailAvailable()) {
+                        callVoicemail();
+                    } else if (getActivity() != null) {
+                        // Voicemail is unavailable maybe because Airplane mode is turned on.
+                        // Check the current status and show the most appropriate error message.
+                        final boolean isAirplaneModeOn =
+                                Settings.System.getInt(getActivity().getContentResolver(),
+                                Settings.System.AIRPLANE_MODE_ON, 0) != 0;
+                        if (isAirplaneModeOn) {
+                            DialogFragment dialogFragment = ErrorDialogFragment.newInstance(
+                                    R.string.dialog_voicemail_airplane_mode_message);
+                            dialogFragment.show(getFragmentManager(),
+                                    "voicemail_request_during_airplane_mode");
+                        } else {
+                            DialogFragment dialogFragment = ErrorDialogFragment.newInstance(
+                                    R.string.dialog_voicemail_not_ready_message);
+                            dialogFragment.show(getFragmentManager(), "voicemail_not_ready");
+                        }
+                    }
+                    return true;
+                }
+                return false;
+            }
+            case R.id.zero: {
+                // Remove tentative input ('0') done by onTouch().
+                removePreviousDigitIfPossible();
+                keyPressed(KeyEvent.KEYCODE_PLUS);
+
+                // Stop tone immediately and decrease the press count, so that possible subsequent
+                // dial button presses won't honor the 0 click any more.
+                // Note: this *will* make mDialpadPressCount negative when the 0 key is released,
+                // which should be handled appropriately.
+                stopTone();
+                if (mDialpadPressCount > 0) mDialpadPressCount--;
+
+                return true;
+            }
+            case R.id.digits: {
+                // Right now EditText does not show the "paste" option when cursor is not visible.
+                // To show that, make the cursor visible, and return false, letting the EditText
+                // show the option by itself.
+                mDigits.setCursorVisible(true);
+                return false;
+            }
+            case R.id.dialButton: {
+                if (isDigitsEmpty()) {
+                    handleDialButtonClickWithEmptyDigits();
+                    // This event should be consumed so that onClick() won't do the exactly same
+                    // thing.
+                    return true;
+                } else {
+                    return false;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Remove the digit just before the current position. This can be used if we want to replace
+     * the previous digit or cancel previously entered character.
+     */
+    private void removePreviousDigitIfPossible() {
+        final Editable editable = mDigits.getText();
+        final int currentPosition = mDigits.getSelectionStart();
+        if (currentPosition > 0) {
+            mDigits.setSelection(currentPosition);
+            mDigits.getText().delete(currentPosition - 1, currentPosition);
+        }
+    }
+
+    public void callVoicemail() {
+        startActivity(ContactsUtils.getVoicemailIntent());
+        mClearDigitsOnStop = true;
+        getActivity().finish();
+    }
+
+    public static class ErrorDialogFragment extends DialogFragment {
+        private int mTitleResId;
+        private int mMessageResId;
+
+        private static final String ARG_TITLE_RES_ID = "argTitleResId";
+        private static final String ARG_MESSAGE_RES_ID = "argMessageResId";
+
+        public static ErrorDialogFragment newInstance(int messageResId) {
+            return newInstance(0, messageResId);
+        }
+
+        public static ErrorDialogFragment newInstance(int titleResId, int messageResId) {
+            final ErrorDialogFragment fragment = new ErrorDialogFragment();
+            final Bundle args = new Bundle();
+            args.putInt(ARG_TITLE_RES_ID, titleResId);
+            args.putInt(ARG_MESSAGE_RES_ID, messageResId);
+            fragment.setArguments(args);
+            return fragment;
+        }
+
+        @Override
+        public void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            mTitleResId = getArguments().getInt(ARG_TITLE_RES_ID);
+            mMessageResId = getArguments().getInt(ARG_MESSAGE_RES_ID);
+        }
+
+        @Override
+        public Dialog onCreateDialog(Bundle savedInstanceState) {
+            AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+            if (mTitleResId != 0) {
+                builder.setTitle(mTitleResId);
+            }
+            if (mMessageResId != 0) {
+                builder.setMessage(mMessageResId);
+            }
+            builder.setPositiveButton(android.R.string.ok,
+                    new DialogInterface.OnClickListener() {
+                            @Override
+                            public void onClick(DialogInterface dialog, int which) {
+                                dismiss();
+                            }
+                    });
+            return builder.create();
+        }
+    }
+
+    /**
+     * In most cases, when the dial button is pressed, there is a
+     * number in digits area. Pack it in the intent, start the
+     * outgoing call broadcast as a separate task and finish this
+     * activity.
+     *
+     * When there is no digit and the phone is CDMA and off hook,
+     * we're sending a blank flash for CDMA. CDMA networks use Flash
+     * messages when special processing needs to be done, mainly for
+     * 3-way or call waiting scenarios. Presumably, here we're in a
+     * special 3-way scenario where the network needs a blank flash
+     * before being able to add the new participant.  (This is not the
+     * case with all 3-way calls, just certain CDMA infrastructures.)
+     *
+     * Otherwise, there is no digit, display the last dialed
+     * number. Don't finish since the user may want to edit it. The
+     * user needs to press the dial button again, to dial it (general
+     * case described above).
+     */
+    public void dialButtonPressed() {
+        if (isDigitsEmpty()) { // No number entered.
+            handleDialButtonClickWithEmptyDigits();
+        } else {
+            final String number = mDigits.getText().toString();
+
+            // "persist.radio.otaspdial" is a temporary hack needed for one carrier's automated
+            // test equipment.
+            // TODO: clean it up.
+            if (number != null
+                    && !TextUtils.isEmpty(mProhibitedPhoneNumberRegexp)
+                    && number.matches(mProhibitedPhoneNumberRegexp)
+                    && (SystemProperties.getInt("persist.radio.otaspdial", 0) != 1)) {
+                Log.i(TAG, "The phone number is prohibited explicitly by a rule.");
+                if (getActivity() != null) {
+                    DialogFragment dialogFragment = ErrorDialogFragment.newInstance(
+                            R.string.dialog_phone_call_prohibited_message);
+                    dialogFragment.show(getFragmentManager(), "phone_prohibited_dialog");
+                }
+
+                // Clear the digits just in case.
+                mDigits.getText().clear();
+            } else {
+                final Intent intent = ContactsUtils.getCallIntent(number,
+                        (getActivity() instanceof DialtactsActivity ?
+                                ((DialtactsActivity)getActivity()).getCallOrigin() : null));
+                startActivity(intent);
+                mClearDigitsOnStop = true;
+                getActivity().finish();
+            }
+        }
+    }
+
+    private void handleDialButtonClickWithEmptyDigits() {
+        if (phoneIsCdma() && phoneIsOffhook()) {
+            // This is really CDMA specific. On GSM is it possible
+            // to be off hook and wanted to add a 3rd party using
+            // the redial feature.
+            startActivity(newFlashIntent());
+        } else {
+            if (!TextUtils.isEmpty(mLastNumberDialed)) {
+                // Recall the last number dialed.
+                mDigits.setText(mLastNumberDialed);
+
+                // ...and move the cursor to the end of the digits string,
+                // so you'll be able to delete digits using the Delete
+                // button (just as if you had typed the number manually.)
+                //
+                // Note we use mDigits.getText().length() here, not
+                // mLastNumberDialed.length(), since the EditText widget now
+                // contains a *formatted* version of mLastNumberDialed (due to
+                // mTextWatcher) and its length may have changed.
+                mDigits.setSelection(mDigits.getText().length());
+            } else {
+                // There's no "last number dialed" or the
+                // background query is still running. There's
+                // nothing useful for the Dial button to do in
+                // this case.  Note: with a soft dial button, this
+                // can never happens since the dial button is
+                // disabled under these conditons.
+                playTone(ToneGenerator.TONE_PROP_NACK);
+            }
+        }
+    }
+
+    /**
+     * Plays the specified tone for TONE_LENGTH_MS milliseconds.
+     */
+    private void playTone(int tone) {
+        playTone(tone, TONE_LENGTH_MS);
+    }
+
+    /**
+     * Play the specified tone for the specified milliseconds
+     *
+     * The tone is played locally, using the audio stream for phone calls.
+     * Tones are played only if the "Audible touch tones" user preference
+     * is checked, and are NOT played if the device is in silent mode.
+     *
+     * The tone length can be -1, meaning "keep playing the tone." If the caller does so, it should
+     * call stopTone() afterward.
+     *
+     * @param tone a tone code from {@link ToneGenerator}
+     * @param durationMs tone length.
+     */
+    private void playTone(int tone, int durationMs) {
+        // if local tone playback is disabled, just return.
+        if (!mDTMFToneEnabled) {
+            return;
+        }
+
+        // Also do nothing if the phone is in silent mode.
+        // We need to re-check the ringer mode for *every* playTone()
+        // call, rather than keeping a local flag that's updated in
+        // onResume(), since it's possible to toggle silent mode without
+        // leaving the current activity (via the ENDCALL-longpress menu.)
+        AudioManager audioManager =
+                (AudioManager) getActivity().getSystemService(Context.AUDIO_SERVICE);
+        int ringerMode = audioManager.getRingerMode();
+        if ((ringerMode == AudioManager.RINGER_MODE_SILENT)
+            || (ringerMode == AudioManager.RINGER_MODE_VIBRATE)) {
+            return;
+        }
+
+        synchronized (mToneGeneratorLock) {
+            if (mToneGenerator == null) {
+                Log.w(TAG, "playTone: mToneGenerator == null, tone: " + tone);
+                return;
+            }
+
+            // Start the new tone (will stop any playing tone)
+            mToneGenerator.startTone(tone, durationMs);
+        }
+    }
+
+    /**
+     * Stop the tone if it is played.
+     */
+    private void stopTone() {
+        // if local tone playback is disabled, just return.
+        if (!mDTMFToneEnabled) {
+            return;
+        }
+        synchronized (mToneGeneratorLock) {
+            if (mToneGenerator == null) {
+                Log.w(TAG, "stopTone: mToneGenerator == null");
+                return;
+            }
+            mToneGenerator.stopTone();
+        }
+    }
+
+    /**
+     * Brings up the "dialpad chooser" UI in place of the usual Dialer
+     * elements (the textfield/button and the dialpad underneath).
+     *
+     * We show this UI if the user brings up the Dialer while a call is
+     * already in progress, since there's a good chance we got here
+     * accidentally (and the user really wanted the in-call dialpad instead).
+     * So in this situation we display an intermediate UI that lets the user
+     * explicitly choose between the in-call dialpad ("Use touch tone
+     * keypad") and the regular Dialer ("Add call").  (Or, the option "Return
+     * to call in progress" just goes back to the in-call UI with no dialpad
+     * at all.)
+     *
+     * @param enabled If true, show the "dialpad chooser" instead
+     *                of the regular Dialer UI
+     */
+    private void showDialpadChooser(boolean enabled) {
+        // Check if onCreateView() is already called by checking one of View objects.
+        if (!isLayoutReady()) {
+            return;
+        }
+
+        if (enabled) {
+            // Log.i(TAG, "Showing dialpad chooser!");
+            if (mDigitsContainer != null) {
+                mDigitsContainer.setVisibility(View.GONE);
+            } else {
+                // mDigits is not enclosed by the container. Make the digits field itself gone.
+                mDigits.setVisibility(View.GONE);
+            }
+            if (mDialpad != null) mDialpad.setVisibility(View.GONE);
+            if (mDialButtonContainer != null) mDialButtonContainer.setVisibility(View.GONE);
+
+            mDialpadChooser.setVisibility(View.VISIBLE);
+
+            // Instantiate the DialpadChooserAdapter and hook it up to the
+            // ListView.  We do this only once.
+            if (mDialpadChooserAdapter == null) {
+                mDialpadChooserAdapter = new DialpadChooserAdapter(getActivity());
+            }
+            mDialpadChooser.setAdapter(mDialpadChooserAdapter);
+        } else {
+            // Log.i(TAG, "Displaying normal Dialer UI.");
+            if (mDigitsContainer != null) {
+                mDigitsContainer.setVisibility(View.VISIBLE);
+            } else {
+                mDigits.setVisibility(View.VISIBLE);
+            }
+            if (mDialpad != null) mDialpad.setVisibility(View.VISIBLE);
+            if (mDialButtonContainer != null) mDialButtonContainer.setVisibility(View.VISIBLE);
+            mDialpadChooser.setVisibility(View.GONE);
+        }
+    }
+
+    /**
+     * @return true if we're currently showing the "dialpad chooser" UI.
+     */
+    private boolean dialpadChooserVisible() {
+        return mDialpadChooser.getVisibility() == View.VISIBLE;
+    }
+
+    /**
+     * Simple list adapter, binding to an icon + text label
+     * for each item in the "dialpad chooser" list.
+     */
+    private static class DialpadChooserAdapter extends BaseAdapter {
+        private LayoutInflater mInflater;
+
+        // Simple struct for a single "choice" item.
+        static class ChoiceItem {
+            String text;
+            Bitmap icon;
+            int id;
+
+            public ChoiceItem(String s, Bitmap b, int i) {
+                text = s;
+                icon = b;
+                id = i;
+            }
+        }
+
+        // IDs for the possible "choices":
+        static final int DIALPAD_CHOICE_USE_DTMF_DIALPAD = 101;
+        static final int DIALPAD_CHOICE_RETURN_TO_CALL = 102;
+        static final int DIALPAD_CHOICE_ADD_NEW_CALL = 103;
+
+        private static final int NUM_ITEMS = 3;
+        private ChoiceItem mChoiceItems[] = new ChoiceItem[NUM_ITEMS];
+
+        public DialpadChooserAdapter(Context context) {
+            // Cache the LayoutInflate to avoid asking for a new one each time.
+            mInflater = LayoutInflater.from(context);
+
+            // Initialize the possible choices.
+            // TODO: could this be specified entirely in XML?
+
+            // - "Use touch tone keypad"
+            mChoiceItems[0] = new ChoiceItem(
+                    context.getString(R.string.dialer_useDtmfDialpad),
+                    BitmapFactory.decodeResource(context.getResources(),
+                                                 R.drawable.ic_dialer_fork_tt_keypad),
+                    DIALPAD_CHOICE_USE_DTMF_DIALPAD);
+
+            // - "Return to call in progress"
+            mChoiceItems[1] = new ChoiceItem(
+                    context.getString(R.string.dialer_returnToInCallScreen),
+                    BitmapFactory.decodeResource(context.getResources(),
+                                                 R.drawable.ic_dialer_fork_current_call),
+                    DIALPAD_CHOICE_RETURN_TO_CALL);
+
+            // - "Add call"
+            mChoiceItems[2] = new ChoiceItem(
+                    context.getString(R.string.dialer_addAnotherCall),
+                    BitmapFactory.decodeResource(context.getResources(),
+                                                 R.drawable.ic_dialer_fork_add_call),
+                    DIALPAD_CHOICE_ADD_NEW_CALL);
+        }
+
+        @Override
+        public int getCount() {
+            return NUM_ITEMS;
+        }
+
+        /**
+         * Return the ChoiceItem for a given position.
+         */
+        @Override
+        public Object getItem(int position) {
+            return mChoiceItems[position];
+        }
+
+        /**
+         * Return a unique ID for each possible choice.
+         */
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+
+        /**
+         * Make a view for each row.
+         */
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            // When convertView is non-null, we can reuse it (there's no need
+            // to reinflate it.)
+            if (convertView == null) {
+                convertView = mInflater.inflate(R.layout.dialpad_chooser_list_item, null);
+            }
+
+            TextView text = (TextView) convertView.findViewById(R.id.text);
+            text.setText(mChoiceItems[position].text);
+
+            ImageView icon = (ImageView) convertView.findViewById(R.id.icon);
+            icon.setImageBitmap(mChoiceItems[position].icon);
+
+            return convertView;
+        }
+    }
+
+    /**
+     * Handle clicks from the dialpad chooser.
+     */
+    @Override
+    public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
+        DialpadChooserAdapter.ChoiceItem item =
+                (DialpadChooserAdapter.ChoiceItem) parent.getItemAtPosition(position);
+        int itemId = item.id;
+        switch (itemId) {
+            case DialpadChooserAdapter.DIALPAD_CHOICE_USE_DTMF_DIALPAD:
+                // Log.i(TAG, "DIALPAD_CHOICE_USE_DTMF_DIALPAD");
+                // Fire off an intent to go back to the in-call UI
+                // with the dialpad visible.
+                returnToInCallScreen(true);
+                break;
+
+            case DialpadChooserAdapter.DIALPAD_CHOICE_RETURN_TO_CALL:
+                // Log.i(TAG, "DIALPAD_CHOICE_RETURN_TO_CALL");
+                // Fire off an intent to go back to the in-call UI
+                // (with the dialpad hidden).
+                returnToInCallScreen(false);
+                break;
+
+            case DialpadChooserAdapter.DIALPAD_CHOICE_ADD_NEW_CALL:
+                // Log.i(TAG, "DIALPAD_CHOICE_ADD_NEW_CALL");
+                // Ok, guess the user really did want to be here (in the
+                // regular Dialer) after all.  Bring back the normal Dialer UI.
+                showDialpadChooser(false);
+                break;
+
+            default:
+                Log.w(TAG, "onItemClick: unexpected itemId: " + itemId);
+                break;
+        }
+    }
+
+    /**
+     * Returns to the in-call UI (where there's presumably a call in
+     * progress) in response to the user selecting "use touch tone keypad"
+     * or "return to call" from the dialpad chooser.
+     */
+    private void returnToInCallScreen(boolean showDialpad) {
+        try {
+            ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone"));
+            if (phone != null) phone.showCallScreenWithDialpad(showDialpad);
+        } catch (RemoteException e) {
+            Log.w(TAG, "phone.showCallScreenWithDialpad() failed", e);
+        }
+
+        // Finally, finish() ourselves so that we don't stay on the
+        // activity stack.
+        // Note that we do this whether or not the showCallScreenWithDialpad()
+        // call above had any effect or not!  (That call is a no-op if the
+        // phone is idle, which can happen if the current call ends while
+        // the dialpad chooser is up.  In this case we can't show the
+        // InCallScreen, and there's no point staying here in the Dialer,
+        // so we just take the user back where he came from...)
+        getActivity().finish();
+    }
+
+    /**
+     * @return true if the phone is "in use", meaning that at least one line
+     *              is active (ie. off hook or ringing or dialing).
+     */
+    public static boolean phoneIsInUse() {
+        boolean phoneInUse = false;
+        try {
+            ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone"));
+            if (phone != null) phoneInUse = !phone.isIdle();
+        } catch (RemoteException e) {
+            Log.w(TAG, "phone.isIdle() failed", e);
+        }
+        return phoneInUse;
+    }
+
+    /**
+     * @return true if the phone is a CDMA phone type
+     */
+    private boolean phoneIsCdma() {
+        boolean isCdma = false;
+        try {
+            ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone"));
+            if (phone != null) {
+                isCdma = (phone.getActivePhoneType() == TelephonyManager.PHONE_TYPE_CDMA);
+            }
+        } catch (RemoteException e) {
+            Log.w(TAG, "phone.getActivePhoneType() failed", e);
+        }
+        return isCdma;
+    }
+
+    /**
+     * @return true if the phone state is OFFHOOK
+     */
+    private boolean phoneIsOffhook() {
+        boolean phoneOffhook = false;
+        try {
+            ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone"));
+            if (phone != null) phoneOffhook = phone.isOffhook();
+        } catch (RemoteException e) {
+            Log.w(TAG, "phone.isOffhook() failed", e);
+        }
+        return phoneOffhook;
+    }
+
+    /**
+     * Returns true whenever any one of the options from the menu is selected.
+     * Code changes to support dialpad options
+     */
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.menu_2s_pause:
+                updateDialString(",");
+                return true;
+            case R.id.menu_add_wait:
+                updateDialString(";");
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    @Override
+    public boolean onMenuItemClick(MenuItem item) {
+        return onOptionsItemSelected(item);
+    }
+
+    /**
+     * Updates the dial string (mDigits) after inserting a Pause character (,)
+     * or Wait character (;).
+     */
+    private void updateDialString(String newDigits) {
+        int selectionStart;
+        int selectionEnd;
+
+        // SpannableStringBuilder editable_text = new SpannableStringBuilder(mDigits.getText());
+        int anchor = mDigits.getSelectionStart();
+        int point = mDigits.getSelectionEnd();
+
+        selectionStart = Math.min(anchor, point);
+        selectionEnd = Math.max(anchor, point);
+
+        Editable digits = mDigits.getText();
+        if (selectionStart != -1) {
+            if (selectionStart == selectionEnd) {
+                // then there is no selection. So insert the pause at this
+                // position and update the mDigits.
+                digits.replace(selectionStart, selectionStart, newDigits);
+            } else {
+                digits.replace(selectionStart, selectionEnd, newDigits);
+                // Unselect: back to a regular cursor, just pass the character inserted.
+                mDigits.setSelection(selectionStart + 1);
+            }
+        } else {
+            int len = mDigits.length();
+            digits.replace(len, len, newDigits);
+        }
+    }
+
+    /**
+     * Update the enabledness of the "Dial" and "Backspace" buttons if applicable.
+     */
+    private void updateDialAndDeleteButtonEnabledState() {
+        final boolean digitsNotEmpty = !isDigitsEmpty();
+
+        if (mDialButton != null) {
+            // On CDMA phones, if we're already on a call, we *always*
+            // enable the Dial button (since you can press it without
+            // entering any digits to send an empty flash.)
+            if (phoneIsCdma() && phoneIsOffhook()) {
+                mDialButton.setEnabled(true);
+            } else {
+                // Common case: GSM, or CDMA but not on a call.
+                // Enable the Dial button if some digits have
+                // been entered, or if there is a last dialed number
+                // that could be redialed.
+                mDialButton.setEnabled(digitsNotEmpty ||
+                        !TextUtils.isEmpty(mLastNumberDialed));
+            }
+        }
+        mDelete.setEnabled(digitsNotEmpty);
+    }
+
+    /**
+     * Check if voicemail is enabled/accessible.
+     *
+     * @return true if voicemail is enabled and accessibly. Note that this can be false
+     * "temporarily" after the app boot.
+     * @see TelephonyManager#getVoiceMailNumber()
+     */
+    private boolean isVoicemailAvailable() {
+        try {
+            return (TelephonyManager.getDefault().getVoiceMailNumber() != null);
+        } catch (SecurityException se) {
+            // Possibly no READ_PHONE_STATE privilege.
+            Log.w(TAG, "SecurityException is thrown. Maybe privilege isn't sufficient.");
+        }
+        return false;
+    }
+
+    /**
+     * This function return true if Wait menu item can be shown
+     * otherwise returns false. Assumes the passed string is non-empty
+     * and the 0th index check is not required.
+     */
+    private static boolean showWait(int start, int end, String digits) {
+        if (start == end) {
+            // visible false in this case
+            if (start > digits.length()) return false;
+
+            // preceding char is ';', so visible should be false
+            if (digits.charAt(start - 1) == ';') return false;
+
+            // next char is ';', so visible should be false
+            if ((digits.length() > start) && (digits.charAt(start) == ';')) return false;
+        } else {
+            // visible false in this case
+            if (start > digits.length() || end > digits.length()) return false;
+
+            // In this case we need to just check for ';' preceding to start
+            // or next to end
+            if (digits.charAt(start - 1) == ';') return false;
+        }
+        return true;
+    }
+
+    /**
+     * @return true if the widget with the phone number digits is empty.
+     */
+    private boolean isDigitsEmpty() {
+        return mDigits.length() == 0;
+    }
+
+    /**
+     * Starts the asyn query to get the last dialed/outgoing
+     * number. When the background query finishes, mLastNumberDialed
+     * is set to the last dialed number or an empty string if none
+     * exists yet.
+     */
+    private void queryLastOutgoingCall() {
+        mLastNumberDialed = EMPTY_NUMBER;
+        CallLogAsync.GetLastOutgoingCallArgs lastCallArgs =
+                new CallLogAsync.GetLastOutgoingCallArgs(
+                    getActivity(),
+                    new CallLogAsync.OnLastOutgoingCallComplete() {
+                        @Override
+                        public void lastOutgoingCall(String number) {
+                            // TODO: Filter out emergency numbers if
+                            // the carrier does not want redial for
+                            // these.
+                            mLastNumberDialed = number;
+                            updateDialAndDeleteButtonEnabledState();
+                        }
+                    });
+        mCallLog.getLastOutgoingCall(lastCallArgs);
+    }
+
+    private Intent newFlashIntent() {
+        final Intent intent = ContactsUtils.getCallIntent(EMPTY_NUMBER);
+        intent.putExtra(EXTRA_SEND_EMPTY_FLASH, true);
+        return intent;
+    }
+}
diff --git a/src/com/android/dialer/dialpad/DialpadImageButton.java b/src/com/android/dialer/dialpad/DialpadImageButton.java
new file mode 100644
index 0000000..d5f825b
--- /dev/null
+++ b/src/com/android/dialer/dialpad/DialpadImageButton.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2012 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.dialer.dialpad;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ImageButton;
+
+/**
+ * Custom {@link ImageButton} for dialpad buttons.
+ *
+ * During horizontal swipe, we want to exit "fading out" animation offered by its background
+ * just after starting the swipe.This class overrides {@link #onTouchEvent(MotionEvent)} to achieve
+ * the behavior.
+ */
+public class DialpadImageButton extends ImageButton {
+    public interface OnPressedListener {
+        public void onPressed(View view, boolean pressed);
+    }
+
+    private OnPressedListener mOnPressedListener;
+
+    public void setOnPressedListener(OnPressedListener onPressedListener) {
+        mOnPressedListener = onPressedListener;
+    }
+
+    public DialpadImageButton(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public DialpadImageButton(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    public void setPressed(boolean pressed) {
+        super.setPressed(pressed);
+        if (mOnPressedListener != null) {
+            mOnPressedListener.onPressed(this, pressed);
+        }
+    }
+}
diff --git a/src/com/android/dialer/dialpad/DigitsEditText.java b/src/com/android/dialer/dialpad/DigitsEditText.java
new file mode 100644
index 0000000..6ad4df9
--- /dev/null
+++ b/src/com/android/dialer/dialpad/DigitsEditText.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2011 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.dialer.dialpad;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.text.InputType;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+
+/**
+ * EditText which suppresses IME show up.
+ */
+public class DigitsEditText extends EditText {
+    public DigitsEditText(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        setInputType(getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
+        setShowSoftInputOnFocus(false);
+    }
+
+    @Override
+    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+        super.onFocusChanged(focused, direction, previouslyFocusedRect);
+        final InputMethodManager imm = ((InputMethodManager) getContext()
+                .getSystemService(Context.INPUT_METHOD_SERVICE));
+        if (imm != null && imm.isActive(this)) {
+            imm.hideSoftInputFromWindow(getApplicationWindowToken(), 0);
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        final boolean ret = super.onTouchEvent(event);
+        // Must be done after super.onTouchEvent()
+        final InputMethodManager imm = ((InputMethodManager) getContext()
+                .getSystemService(Context.INPUT_METHOD_SERVICE));
+        if (imm != null && imm.isActive(this)) {
+            imm.hideSoftInputFromWindow(getApplicationWindowToken(), 0);
+        }
+        return ret;
+    }
+
+    @Override
+    public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {
+        if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED) {
+            // Since we're replacing the text every time we add or remove a
+            // character, only read the difference. (issue 5337550)
+            final int added = event.getAddedCount();
+            final int removed = event.getRemovedCount();
+            final int length = event.getBeforeText().length();
+            if (added > removed) {
+                event.setRemovedCount(0);
+                event.setAddedCount(1);
+                event.setFromIndex(length);
+            } else if (removed > added) {
+                event.setRemovedCount(1);
+                event.setAddedCount(0);
+                event.setFromIndex(length - 1);
+            } else {
+                return;
+            }
+        } else if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED) {
+            // The parent EditText class lets tts read "edit box" when this View has a focus, which
+            // confuses users on app launch (issue 5275935).
+            return;
+        }
+        super.sendAccessibilityEventUnchecked(event);
+    }
+}
diff --git a/src/com/android/dialer/util/ExpirableCache.java b/src/com/android/dialer/util/ExpirableCache.java
new file mode 100644
index 0000000..2b4e439
--- /dev/null
+++ b/src/com/android/dialer/util/ExpirableCache.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2011 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.dialer.util;
+
+import android.util.LruCache;
+
+import com.android.contacts.test.NeededForTesting;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.annotation.concurrent.Immutable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * An LRU cache in which all items can be marked as expired at a given time and it is possible to
+ * query whether a particular cached value is expired or not.
+ * <p>
+ * A typical use case for this is caching of values which are expensive to compute but which are
+ * still useful when out of date.
+ * <p>
+ * Consider a cache for contact information:
+ * <pre>{@code
+ *     private ExpirableCache<String, Contact> mContactCache;}</pre>
+ * which stores the contact information for a given phone number.
+ * <p>
+ * When we need to store contact information for a given phone number, we can look up the info in
+ * the cache:
+ * <pre>{@code
+ *     CachedValue<Contact> cachedContact = mContactCache.getCachedValue(phoneNumber);
+ * }</pre>
+ * We might also want to fetch the contact information again if the item is expired.
+ * <pre>
+ *     if (cachedContact.isExpired()) {
+ *         fetchContactForNumber(phoneNumber,
+ *                 new FetchListener() {
+ *                     &#64;Override
+ *                     public void onFetched(Contact contact) {
+ *                         mContactCache.put(phoneNumber, contact);
+ *                     }
+ *                 });
+ *     }</pre>
+ * and insert it back into the cache when the fetch completes.
+ * <p>
+ * At a certain point we want to expire the content of the cache because we know the content may
+ * no longer be up-to-date, for instance, when resuming the activity this is shown into:
+ * <pre>
+ *     &#64;Override
+ *     protected onResume() {
+ *         // We were paused for some time, the cached value might no longer be up to date.
+ *         mContactCache.expireAll();
+ *         super.onResume();
+ *     }
+ * </pre>
+ * The values will be still available from the cache, but they will be expired.
+ * <p>
+ * If interested only in the value itself, not whether it is expired or not, one should use the
+ * {@link #getPossiblyExpired(Object)} method. If interested only in non-expired values, one should
+ * use the {@link #get(Object)} method instead.
+ * <p>
+ * This class wraps around an {@link LruCache} instance: it follows the {@link LruCache} behavior
+ * for evicting items when the cache is full. It is possible to supply your own subclass of LruCache
+ * by using the {@link #create(LruCache)} method, which can define a custom expiration policy.
+ * Since the underlying cache maps keys to cached values it can determine which items are expired
+ * and which are not, allowing for an implementation that evicts expired items before non expired
+ * ones.
+ * <p>
+ * This class is thread-safe.
+ *
+ * @param <K> the type of the keys
+ * @param <V> the type of the values
+ */
+@ThreadSafe
+public class ExpirableCache<K, V> {
+    /**
+     * A cached value stored inside the cache.
+     * <p>
+     * It provides access to the value stored in the cache but also allows to check whether the
+     * value is expired.
+     *
+     * @param <V> the type of value stored in the cache
+     */
+    public interface CachedValue<V> {
+        /** Returns the value stored in the cache for a given key. */
+        public V getValue();
+
+        /**
+         * Checks whether the value, while still being present in the cache, is expired.
+         *
+         * @return true if the value is expired
+         */
+        public boolean isExpired();
+    }
+
+    /**
+     * Cached values storing the generation at which they were added.
+     */
+    @Immutable
+    private static class GenerationalCachedValue<V> implements ExpirableCache.CachedValue<V> {
+        /** The value stored in the cache. */
+        public final V mValue;
+        /** The generation at which the value was added to the cache. */
+        private final int mGeneration;
+        /** The atomic integer storing the current generation of the cache it belongs to. */
+        private final AtomicInteger mCacheGeneration;
+
+        /**
+         * @param cacheGeneration the atomic integer storing the generation of the cache in which
+         *        this value will be stored
+         */
+        public GenerationalCachedValue(V value, AtomicInteger cacheGeneration) {
+            mValue = value;
+            mCacheGeneration = cacheGeneration;
+            // Snapshot the current generation.
+            mGeneration = mCacheGeneration.get();
+        }
+
+        @Override
+        public V getValue() {
+            return mValue;
+        }
+
+        @Override
+        public boolean isExpired() {
+            return mGeneration != mCacheGeneration.get();
+        }
+    }
+
+    /** The underlying cache used to stored the cached values. */
+    private LruCache<K, CachedValue<V>> mCache;
+
+    /**
+     * The current generation of items added to the cache.
+     * <p>
+     * Items in the cache can belong to a previous generation, but in that case they would be
+     * expired.
+     *
+     * @see ExpirableCache.CachedValue#isExpired()
+     */
+    private final AtomicInteger mGeneration;
+
+    private ExpirableCache(LruCache<K, CachedValue<V>> cache) {
+        mCache = cache;
+        mGeneration = new AtomicInteger(0);
+    }
+
+    /**
+     * Returns the cached value for the given key, or null if no value exists.
+     * <p>
+     * The cached value gives access both to the value associated with the key and whether it is
+     * expired or not.
+     * <p>
+     * If not interested in whether the value is expired, use {@link #getPossiblyExpired(Object)}
+     * instead.
+     * <p>
+     * If only wants values that are not expired, use {@link #get(Object)} instead.
+     *
+     * @param key the key to look up
+     */
+    public CachedValue<V> getCachedValue(K key) {
+        return mCache.get(key);
+    }
+
+    /**
+     * Returns the value for the given key, or null if no value exists.
+     * <p>
+     * When using this method, it is not possible to determine whether the value is expired or not.
+     * Use {@link #getCachedValue(Object)} to achieve that instead. However, if using
+     * {@link #getCachedValue(Object)} to determine if an item is expired, one should use the item
+     * within the {@link CachedValue} and not call {@link #getPossiblyExpired(Object)} to get the
+     * value afterwards, since that is not guaranteed to return the same value or that the newly
+     * returned value is in the same state.
+     *
+     * @param key the key to look up
+     */
+    public V getPossiblyExpired(K key) {
+        CachedValue<V> cachedValue = getCachedValue(key);
+        return cachedValue == null ? null : cachedValue.getValue();
+    }
+
+    /**
+     * Returns the value for the given key only if it is not expired, or null if no value exists or
+     * is expired.
+     * <p>
+     * This method will return null if either there is no value associated with this key or if the
+     * associated value is expired.
+     *
+     * @param key the key to look up
+     */
+    @NeededForTesting
+    public V get(K key) {
+        CachedValue<V> cachedValue = getCachedValue(key);
+        return cachedValue == null || cachedValue.isExpired() ? null : cachedValue.getValue();
+    }
+
+    /**
+     * Puts an item in the cache.
+     * <p>
+     * Newly added item will not be expired until {@link #expireAll()} is next called.
+     *
+     * @param key the key to look up
+     * @param value the value to associate with the key
+     */
+    public void put(K key, V value) {
+        mCache.put(key, newCachedValue(value));
+    }
+
+    /**
+     * Mark all items currently in the cache as expired.
+     * <p>
+     * Newly added items after this call will be marked as not expired.
+     * <p>
+     * Expiring the items in the cache does not imply they will be evicted.
+     */
+    public void expireAll() {
+        mGeneration.incrementAndGet();
+    }
+
+    /**
+     * Creates a new {@link CachedValue} instance to be stored in this cache.
+     * <p>
+     * Implementation of {@link LruCache#create(K)} can use this method to create a new entry.
+     */
+    public CachedValue<V> newCachedValue(V value) {
+        return new GenerationalCachedValue<V>(value, mGeneration);
+    }
+
+    /**
+     * Creates a new {@link ExpirableCache} that wraps the given {@link LruCache}.
+     * <p>
+     * The created cache takes ownership of the cache passed in as an argument.
+     *
+     * @param <K> the type of the keys
+     * @param <V> the type of the values
+     * @param cache the cache to store the value in
+     * @return the newly created expirable cache
+     * @throws IllegalArgumentException if the cache is not empty
+     */
+    public static <K, V> ExpirableCache<K, V> create(LruCache<K, CachedValue<V>> cache) {
+        return new ExpirableCache<K, V>(cache);
+    }
+
+    /**
+     * Creates a new {@link ExpirableCache} with the given maximum size.
+     *
+     * @param <K> the type of the keys
+     * @param <V> the type of the values
+     * @return the newly created expirable cache
+     */
+    public static <K, V> ExpirableCache<K, V> create(int maxSize) {
+        return create(new LruCache<K, CachedValue<V>>(maxSize));
+    }
+}
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java b/src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java
new file mode 100644
index 0000000..473d40b
--- /dev/null
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java
@@ -0,0 +1,474 @@
+/*
+ * Copyright (C) 2011 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.dialer.voicemail;
+
+import static com.android.dialer.CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK;
+import static com.android.dialer.CallDetailActivity.EXTRA_VOICEMAIL_URI;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.PowerManager;
+import android.provider.VoicemailContract;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import com.android.common.io.MoreCloseables;
+import com.android.contacts.ProximitySensorAware;
+import com.android.contacts.R;
+import com.android.contacts.util.AsyncTaskExecutors;
+import com.android.ex.variablespeed.MediaPlayerProxy;
+import com.android.ex.variablespeed.VariableSpeed;
+import com.google.common.base.Preconditions;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.NotThreadSafe;
+
+/**
+ * Displays and plays back a single voicemail.
+ * <p>
+ * When the Activity containing this Fragment is created, voicemail playback
+ * will begin immediately. The Activity is expected to be started via an intent
+ * containing a suitable voicemail uri to playback.
+ * <p>
+ * This class is not thread-safe, it is thread-confined. All calls to all public
+ * methods on this class are expected to come from the main ui thread.
+ */
+@NotThreadSafe
+public class VoicemailPlaybackFragment extends Fragment {
+    private static final String TAG = "VoicemailPlayback";
+    private static final int NUMBER_OF_THREADS_IN_POOL = 2;
+    private static final String[] HAS_CONTENT_PROJECTION = new String[] {
+        VoicemailContract.Voicemails.HAS_CONTENT,
+    };
+
+    private VoicemailPlaybackPresenter mPresenter;
+    private ScheduledExecutorService mScheduledExecutorService;
+    private View mPlaybackLayout;
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        mPlaybackLayout = inflater.inflate(R.layout.playback_layout, null);
+        return mPlaybackLayout;
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        mScheduledExecutorService = createScheduledExecutorService();
+        Bundle arguments = getArguments();
+        Preconditions.checkNotNull(arguments, "fragment must be started with arguments");
+        Uri voicemailUri = arguments.getParcelable(EXTRA_VOICEMAIL_URI);
+        Preconditions.checkNotNull(voicemailUri, "fragment must contain EXTRA_VOICEMAIL_URI");
+        boolean startPlayback = arguments.getBoolean(EXTRA_VOICEMAIL_START_PLAYBACK, false);
+        PowerManager powerManager =
+                (PowerManager) getActivity().getSystemService(Context.POWER_SERVICE);
+        PowerManager.WakeLock wakeLock =
+                powerManager.newWakeLock(
+                        PowerManager.SCREEN_DIM_WAKE_LOCK, getClass().getSimpleName());
+        mPresenter = new VoicemailPlaybackPresenter(createPlaybackViewImpl(),
+                createMediaPlayer(mScheduledExecutorService), voicemailUri,
+                mScheduledExecutorService, startPlayback,
+                AsyncTaskExecutors.createAsyncTaskExecutor(), wakeLock);
+        mPresenter.onCreate(savedInstanceState);
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        mPresenter.onSaveInstanceState(outState);
+        super.onSaveInstanceState(outState);
+    }
+
+    @Override
+    public void onDestroy() {
+        mPresenter.onDestroy();
+        mScheduledExecutorService.shutdown();
+        super.onDestroy();
+    }
+
+    @Override
+    public void onPause() {
+        mPresenter.onPause();
+        super.onPause();
+    }
+
+    private PlaybackViewImpl createPlaybackViewImpl() {
+        return new PlaybackViewImpl(new ActivityReference(), getActivity().getApplicationContext(),
+                mPlaybackLayout);
+    }
+
+    private MediaPlayerProxy createMediaPlayer(ExecutorService executorService) {
+        return VariableSpeed.createVariableSpeed(executorService);
+    }
+
+    private ScheduledExecutorService createScheduledExecutorService() {
+        return Executors.newScheduledThreadPool(NUMBER_OF_THREADS_IN_POOL);
+    }
+
+    /**
+     * Formats a number of milliseconds as something that looks like {@code 00:05}.
+     * <p>
+     * We always use four digits, two for minutes two for seconds.  In the very unlikely event
+     * that the voicemail duration exceeds 99 minutes, the display is capped at 99 minutes.
+     */
+    private static String formatAsMinutesAndSeconds(int millis) {
+        int seconds = millis / 1000;
+        int minutes = seconds / 60;
+        seconds -= minutes * 60;
+        if (minutes > 99) {
+            minutes = 99;
+        }
+        return String.format("%02d:%02d", minutes, seconds);
+    }
+
+    /**
+     * An object that can provide us with an Activity.
+     * <p>
+     * Fragments suffer the drawback that the Activity they belong to may sometimes be null. This
+     * can happen if the Fragment is detached, for example. In that situation a call to
+     * {@link Fragment#getString(int)} will throw and {@link IllegalStateException}. Also, calling
+     * {@link Fragment#getActivity()} is dangerous - it may sometimes return null. And thus blindly
+     * calling a method on the result of getActivity() is dangerous too.
+     * <p>
+     * To work around this, I have made the {@link PlaybackViewImpl} class static, so that it does
+     * not have access to any Fragment methods directly. Instead it uses an application Context for
+     * things like accessing strings, accessing system services. It only uses the Activity when it
+     * absolutely needs it - and does so through this class. This makes it easy to see where we have
+     * to check for null properly.
+     */
+    private final class ActivityReference {
+        /** Gets this Fragment's Activity: <b>may be null</b>. */
+        public final Activity get() {
+            return getActivity();
+        }
+    }
+
+    /**  Methods required by the PlaybackView for the VoicemailPlaybackPresenter. */
+    private static final class PlaybackViewImpl implements VoicemailPlaybackPresenter.PlaybackView {
+        private final ActivityReference mActivityReference;
+        private final Context mApplicationContext;
+        private final SeekBar mPlaybackSeek;
+        private final ImageButton mStartStopButton;
+        private final ImageButton mPlaybackSpeakerphone;
+        private final ImageButton mRateDecreaseButton;
+        private final ImageButton mRateIncreaseButton;
+        private final TextViewWithMessagesController mTextController;
+
+        public PlaybackViewImpl(ActivityReference activityReference, Context applicationContext,
+                View playbackLayout) {
+            Preconditions.checkNotNull(activityReference);
+            Preconditions.checkNotNull(applicationContext);
+            Preconditions.checkNotNull(playbackLayout);
+            mActivityReference = activityReference;
+            mApplicationContext = applicationContext;
+            mPlaybackSeek = (SeekBar) playbackLayout.findViewById(R.id.playback_seek);
+            mStartStopButton = (ImageButton) playbackLayout.findViewById(
+                    R.id.playback_start_stop);
+            mPlaybackSpeakerphone = (ImageButton) playbackLayout.findViewById(
+                    R.id.playback_speakerphone);
+            mRateDecreaseButton = (ImageButton) playbackLayout.findViewById(
+                    R.id.rate_decrease_button);
+            mRateIncreaseButton = (ImageButton) playbackLayout.findViewById(
+                    R.id.rate_increase_button);
+            mTextController = new TextViewWithMessagesController(
+                    (TextView) playbackLayout.findViewById(R.id.playback_position_text),
+                    (TextView) playbackLayout.findViewById(R.id.playback_speed_text));
+        }
+
+        @Override
+        public void finish() {
+            Activity activity = mActivityReference.get();
+            if (activity != null) {
+                activity.finish();
+            }
+        }
+
+        @Override
+        public void runOnUiThread(Runnable runnable) {
+            Activity activity = mActivityReference.get();
+            if (activity != null) {
+                activity.runOnUiThread(runnable);
+            }
+        }
+
+        @Override
+        public Context getDataSourceContext() {
+            return mApplicationContext;
+        }
+
+        @Override
+        public void setRateDecreaseButtonListener(View.OnClickListener listener) {
+            mRateDecreaseButton.setOnClickListener(listener);
+        }
+
+        @Override
+        public void setRateIncreaseButtonListener(View.OnClickListener listener) {
+            mRateIncreaseButton.setOnClickListener(listener);
+        }
+
+        @Override
+        public void setStartStopListener(View.OnClickListener listener) {
+            mStartStopButton.setOnClickListener(listener);
+        }
+
+        @Override
+        public void setSpeakerphoneListener(View.OnClickListener listener) {
+            mPlaybackSpeakerphone.setOnClickListener(listener);
+        }
+
+        @Override
+        public void setRateDisplay(float rate, int stringResourceId) {
+            mTextController.setTemporaryText(
+                    mApplicationContext.getString(stringResourceId), 1, TimeUnit.SECONDS);
+        }
+
+        @Override
+        public void setPositionSeekListener(SeekBar.OnSeekBarChangeListener listener) {
+            mPlaybackSeek.setOnSeekBarChangeListener(listener);
+        }
+
+        @Override
+        public void playbackStarted() {
+            mStartStopButton.setImageResource(R.drawable.ic_hold_pause_holo_dark);
+        }
+
+        @Override
+        public void playbackStopped() {
+            mStartStopButton.setImageResource(R.drawable.ic_play);
+        }
+
+        @Override
+        public void enableProximitySensor() {
+            // Only change the state if the activity is still around.
+            Activity activity = mActivityReference.get();
+            if (activity != null && activity instanceof ProximitySensorAware) {
+                ((ProximitySensorAware) activity).enableProximitySensor();
+            }
+        }
+
+        @Override
+        public void disableProximitySensor() {
+            // Only change the state if the activity is still around.
+            Activity activity = mActivityReference.get();
+            if (activity != null && activity instanceof ProximitySensorAware) {
+                ((ProximitySensorAware) activity).disableProximitySensor(true);
+            }
+        }
+
+        @Override
+        public void registerContentObserver(Uri uri, ContentObserver observer) {
+            mApplicationContext.getContentResolver().registerContentObserver(uri, false, observer);
+        }
+
+        @Override
+        public void unregisterContentObserver(ContentObserver observer) {
+            mApplicationContext.getContentResolver().unregisterContentObserver(observer);
+        }
+
+        @Override
+        public void setClipPosition(int clipPositionInMillis, int clipLengthInMillis) {
+            int seekBarPosition = Math.max(0, clipPositionInMillis);
+            int seekBarMax = Math.max(seekBarPosition, clipLengthInMillis);
+            if (mPlaybackSeek.getMax() != seekBarMax) {
+                mPlaybackSeek.setMax(seekBarMax);
+            }
+            mPlaybackSeek.setProgress(seekBarPosition);
+            mTextController.setPermanentText(
+                    formatAsMinutesAndSeconds(seekBarMax - seekBarPosition));
+        }
+
+        private String getString(int resId) {
+            return mApplicationContext.getString(resId);
+        }
+
+        @Override
+        public void setIsBuffering() {
+            disableUiElements();
+            mTextController.setPermanentText(getString(R.string.voicemail_buffering));
+        }
+
+        @Override
+        public void setIsFetchingContent() {
+            disableUiElements();
+            mTextController.setPermanentText(getString(R.string.voicemail_fetching_content));
+        }
+
+        @Override
+        public void setFetchContentTimeout() {
+            disableUiElements();
+            mTextController.setPermanentText(getString(R.string.voicemail_fetching_timout));
+        }
+
+        @Override
+        public int getDesiredClipPosition() {
+            return mPlaybackSeek.getProgress();
+        }
+
+        @Override
+        public void disableUiElements() {
+            mRateIncreaseButton.setEnabled(false);
+            mRateDecreaseButton.setEnabled(false);
+            mStartStopButton.setEnabled(false);
+            mPlaybackSpeakerphone.setEnabled(false);
+            mPlaybackSeek.setProgress(0);
+            mPlaybackSeek.setEnabled(false);
+        }
+
+        @Override
+        public void playbackError(Exception e) {
+            disableUiElements();
+            mTextController.setPermanentText(getString(R.string.voicemail_playback_error));
+            Log.e(TAG, "Could not play voicemail", e);
+        }
+
+        @Override
+        public void enableUiElements() {
+            mRateIncreaseButton.setEnabled(true);
+            mRateDecreaseButton.setEnabled(true);
+            mStartStopButton.setEnabled(true);
+            mPlaybackSpeakerphone.setEnabled(true);
+            mPlaybackSeek.setEnabled(true);
+        }
+
+        @Override
+        public void sendFetchVoicemailRequest(Uri voicemailUri) {
+            Intent intent = new Intent(VoicemailContract.ACTION_FETCH_VOICEMAIL, voicemailUri);
+            mApplicationContext.sendBroadcast(intent);
+        }
+
+        @Override
+        public boolean queryHasContent(Uri voicemailUri) {
+            ContentResolver contentResolver = mApplicationContext.getContentResolver();
+            Cursor cursor = contentResolver.query(
+                    voicemailUri, HAS_CONTENT_PROJECTION, null, null, null);
+            try {
+                if (cursor != null && cursor.moveToNext()) {
+                    return cursor.getInt(cursor.getColumnIndexOrThrow(
+                            VoicemailContract.Voicemails.HAS_CONTENT)) == 1;
+                }
+            } finally {
+                MoreCloseables.closeQuietly(cursor);
+            }
+            return false;
+        }
+
+        private AudioManager getAudioManager() {
+            return (AudioManager) mApplicationContext.getSystemService(Context.AUDIO_SERVICE);
+        }
+
+        @Override
+        public boolean isSpeakerPhoneOn() {
+            return getAudioManager().isSpeakerphoneOn();
+        }
+
+        @Override
+        public void setSpeakerPhoneOn(boolean on) {
+            getAudioManager().setSpeakerphoneOn(on);
+            if (on) {
+                mPlaybackSpeakerphone.setImageResource(R.drawable.ic_speakerphone_on);
+            } else {
+                mPlaybackSpeakerphone.setImageResource(R.drawable.ic_speakerphone_off);
+            }
+        }
+
+        @Override
+        public void setVolumeControlStream(int streamType) {
+            Activity activity = mActivityReference.get();
+            if (activity != null) {
+                activity.setVolumeControlStream(streamType);
+            }
+        }
+    }
+
+    /**
+     * Controls a TextView with dynamically changing text.
+     * <p>
+     * There are two methods here of interest,
+     * {@link TextViewWithMessagesController#setPermanentText(String)} and
+     * {@link TextViewWithMessagesController#setTemporaryText(String, long, TimeUnit)}.  The
+     * former is used to set the text on the text view immediately, and is used in our case for
+     * the countdown of duration remaining during voicemail playback.  The second is used to
+     * temporarily replace this countdown with a message, in our case faster voicemail speed or
+     * slower voicemail speed, before returning to the countdown display.
+     * <p>
+     * All the methods on this class must be called from the ui thread.
+     */
+    private static final class TextViewWithMessagesController {
+        private static final float VISIBLE = 1;
+        private static final float INVISIBLE = 0;
+        private static final long SHORT_ANIMATION_MS = 200;
+        private static final long LONG_ANIMATION_MS = 400;
+        private final Object mLock = new Object();
+        private final TextView mPermanentTextView;
+        private final TextView mTemporaryTextView;
+        @GuardedBy("mLock") private Runnable mRunnable;
+
+        public TextViewWithMessagesController(TextView permanentTextView,
+                TextView temporaryTextView) {
+            mPermanentTextView = permanentTextView;
+            mTemporaryTextView = temporaryTextView;
+        }
+
+        public void setPermanentText(String text) {
+            mPermanentTextView.setText(text);
+        }
+
+        public void setTemporaryText(String text, long duration, TimeUnit units) {
+            synchronized (mLock) {
+                mTemporaryTextView.setText(text);
+                mTemporaryTextView.animate().alpha(VISIBLE).setDuration(SHORT_ANIMATION_MS);
+                mPermanentTextView.animate().alpha(INVISIBLE).setDuration(SHORT_ANIMATION_MS);
+                mRunnable = new Runnable() {
+                    @Override
+                    public void run() {
+                        synchronized (mLock) {
+                            // We check for (mRunnable == this) becuase if not true, then another
+                            // setTemporaryText call has taken place in the meantime, and this
+                            // one is now defunct and needs to take no action.
+                            if (mRunnable == this) {
+                                mRunnable = null;
+                                mTemporaryTextView.animate()
+                                        .alpha(INVISIBLE).setDuration(LONG_ANIMATION_MS);
+                                mPermanentTextView.animate()
+                                        .alpha(VISIBLE).setDuration(LONG_ANIMATION_MS);
+                            }
+                        }
+                    }
+                };
+                mTemporaryTextView.postDelayed(mRunnable, units.toMillis(duration));
+            }
+        }
+    }
+}
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
new file mode 100644
index 0000000..93b60de
--- /dev/null
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
@@ -0,0 +1,630 @@
+/*
+ * Copyright (C) 2011 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.dialer.voicemail;
+
+import static android.util.MathUtils.constrain;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.view.View;
+import android.widget.SeekBar;
+
+import com.android.contacts.R;
+import com.android.contacts.util.AsyncTaskExecutor;
+import com.android.ex.variablespeed.MediaPlayerProxy;
+import com.android.ex.variablespeed.SingleThreadedMediaPlayerProxy;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.NotThreadSafe;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Contains the controlling logic for a voicemail playback ui.
+ * <p>
+ * Specifically right now this class is used to control the
+ * {@link com.android.dialer.voicemail.VoicemailPlaybackFragment}.
+ * <p>
+ * This class is not thread safe. The thread policy for this class is
+ * thread-confinement, all calls into this class from outside must be done from
+ * the main ui thread.
+ */
+@NotThreadSafe
+@VisibleForTesting
+public class VoicemailPlaybackPresenter {
+    /** The stream used to playback voicemail. */
+    private static final int PLAYBACK_STREAM = AudioManager.STREAM_VOICE_CALL;
+
+    /** Contract describing the behaviour we need from the ui we are controlling. */
+    public interface PlaybackView {
+        Context getDataSourceContext();
+        void runOnUiThread(Runnable runnable);
+        void setStartStopListener(View.OnClickListener listener);
+        void setPositionSeekListener(SeekBar.OnSeekBarChangeListener listener);
+        void setSpeakerphoneListener(View.OnClickListener listener);
+        void setIsBuffering();
+        void setClipPosition(int clipPositionInMillis, int clipLengthInMillis);
+        int getDesiredClipPosition();
+        void playbackStarted();
+        void playbackStopped();
+        void playbackError(Exception e);
+        boolean isSpeakerPhoneOn();
+        void setSpeakerPhoneOn(boolean on);
+        void finish();
+        void setRateDisplay(float rate, int stringResourceId);
+        void setRateIncreaseButtonListener(View.OnClickListener listener);
+        void setRateDecreaseButtonListener(View.OnClickListener listener);
+        void setIsFetchingContent();
+        void disableUiElements();
+        void enableUiElements();
+        void sendFetchVoicemailRequest(Uri voicemailUri);
+        boolean queryHasContent(Uri voicemailUri);
+        void setFetchContentTimeout();
+        void registerContentObserver(Uri uri, ContentObserver observer);
+        void unregisterContentObserver(ContentObserver observer);
+        void enableProximitySensor();
+        void disableProximitySensor();
+        void setVolumeControlStream(int streamType);
+    }
+
+    /** The enumeration of {@link AsyncTask} objects we use in this class. */
+    public enum Tasks {
+        CHECK_FOR_CONTENT,
+        CHECK_CONTENT_AFTER_CHANGE,
+        PREPARE_MEDIA_PLAYER,
+        RESET_PREPARE_START_MEDIA_PLAYER,
+    }
+
+    /** Update rate for the slider, 30fps. */
+    private static final int SLIDER_UPDATE_PERIOD_MILLIS = 1000 / 30;
+    /** Time our ui will wait for content to be fetched before reporting not available. */
+    private static final long FETCH_CONTENT_TIMEOUT_MS = 20000;
+    /**
+     * If present in the saved instance bundle, we should not resume playback on
+     * create.
+     */
+    private static final String PAUSED_STATE_KEY = VoicemailPlaybackPresenter.class.getName()
+            + ".PAUSED_STATE_KEY";
+    /**
+     * If present in the saved instance bundle, indicates where to set the
+     * playback slider.
+     */
+    private static final String CLIP_POSITION_KEY = VoicemailPlaybackPresenter.class.getName()
+            + ".CLIP_POSITION_KEY";
+
+    /** The preset variable-speed rates.  Each is greater than the previous by 25%. */
+    private static final float[] PRESET_RATES = new float[] {
+        0.64f, 0.8f, 1.0f, 1.25f, 1.5625f
+    };
+    /** The string resource ids corresponding to the names given to the above preset rates. */
+    private static final int[] PRESET_NAMES = new int[] {
+        R.string.voicemail_speed_slowest,
+        R.string.voicemail_speed_slower,
+        R.string.voicemail_speed_normal,
+        R.string.voicemail_speed_faster,
+        R.string.voicemail_speed_fastest,
+    };
+
+    /**
+     * Pointer into the {@link VoicemailPlaybackPresenter#PRESET_RATES} array.
+     * <p>
+     * This doesn't need to be synchronized, it's used only by the {@link RateChangeListener}
+     * which in turn is only executed on the ui thread.  This can't be encapsulated inside the
+     * rate change listener since multiple rate change listeners must share the same value.
+     */
+    private int mRateIndex = 2;
+
+    /**
+     * The most recently calculated duration.
+     * <p>
+     * We cache this in a field since we don't want to keep requesting it from the player, as
+     * this can easily lead to throwing {@link IllegalStateException} (any time the player is
+     * released, it's illegal to ask for the duration).
+     */
+    private final AtomicInteger mDuration = new AtomicInteger(0);
+
+    private final PlaybackView mView;
+    private final MediaPlayerProxy mPlayer;
+    private final PositionUpdater mPositionUpdater;
+
+    /** Voicemail uri to play. */
+    private final Uri mVoicemailUri;
+    /** Start playing in onCreate iff this is true. */
+    private final boolean mStartPlayingImmediately;
+    /** Used to run async tasks that need to interact with the ui. */
+    private final AsyncTaskExecutor mAsyncTaskExecutor;
+
+    /**
+     * Used to handle the result of a successful or time-out fetch result.
+     * <p>
+     * This variable is thread-contained, accessed only on the ui thread.
+     */
+    private FetchResultHandler mFetchResultHandler;
+    private PowerManager.WakeLock mWakeLock;
+    private AsyncTask<Void, ?, ?> mPrepareTask;
+
+    public VoicemailPlaybackPresenter(PlaybackView view, MediaPlayerProxy player,
+            Uri voicemailUri, ScheduledExecutorService executorService,
+            boolean startPlayingImmediately, AsyncTaskExecutor asyncTaskExecutor,
+            PowerManager.WakeLock wakeLock) {
+        mView = view;
+        mPlayer = player;
+        mVoicemailUri = voicemailUri;
+        mStartPlayingImmediately = startPlayingImmediately;
+        mAsyncTaskExecutor = asyncTaskExecutor;
+        mPositionUpdater = new PositionUpdater(executorService, SLIDER_UPDATE_PERIOD_MILLIS);
+        mWakeLock = wakeLock;
+    }
+
+    public void onCreate(Bundle bundle) {
+        mView.setVolumeControlStream(PLAYBACK_STREAM);
+        checkThatWeHaveContent();
+    }
+
+    /**
+     * Checks to see if we have content available for this voicemail.
+     * <p>
+     * This method will be called once, after the fragment has been created, before we know if the
+     * voicemail we've been asked to play has any content available.
+     * <p>
+     * This method will notify the user through the ui that we are fetching the content, then check
+     * to see if the content field in the db is set. If set, we proceed to
+     * {@link #postSuccessfullyFetchedContent()} method. If not set, we will make a request to fetch
+     * the content asynchronously via {@link #makeRequestForContent()}.
+     */
+    private void checkThatWeHaveContent() {
+        mView.setIsFetchingContent();
+        mAsyncTaskExecutor.submit(Tasks.CHECK_FOR_CONTENT, new AsyncTask<Void, Void, Boolean>() {
+            @Override
+            public Boolean doInBackground(Void... params) {
+                return mView.queryHasContent(mVoicemailUri);
+            }
+
+            @Override
+            public void onPostExecute(Boolean hasContent) {
+                if (hasContent) {
+                    postSuccessfullyFetchedContent();
+                } else {
+                    makeRequestForContent();
+                }
+            }
+        });
+    }
+
+    /**
+     * Makes a broadcast request to ask that a voicemail source fetch this content.
+     * <p>
+     * This method <b>must be called on the ui thread</b>.
+     * <p>
+     * This method will be called when we realise that we don't have content for this voicemail. It
+     * will trigger a broadcast to request that the content be downloaded. It will add a listener to
+     * the content resolver so that it will be notified when the has_content field changes. It will
+     * also set a timer. If the has_content field changes to true within the allowed time, we will
+     * proceed to {@link #postSuccessfullyFetchedContent()}. If the has_content field does not
+     * become true within the allowed time, we will update the ui to reflect the fact that content
+     * was not available.
+     */
+    private void makeRequestForContent() {
+        Handler handler = new Handler();
+        Preconditions.checkState(mFetchResultHandler == null, "mFetchResultHandler should be null");
+        mFetchResultHandler = new FetchResultHandler(handler);
+        mView.registerContentObserver(mVoicemailUri, mFetchResultHandler);
+        handler.postDelayed(mFetchResultHandler.getTimeoutRunnable(), FETCH_CONTENT_TIMEOUT_MS);
+        mView.sendFetchVoicemailRequest(mVoicemailUri);
+    }
+
+    @ThreadSafe
+    private class FetchResultHandler extends ContentObserver implements Runnable {
+        private AtomicBoolean mResultStillPending = new AtomicBoolean(true);
+        private final Handler mHandler;
+
+        public FetchResultHandler(Handler handler) {
+            super(handler);
+            mHandler = handler;
+        }
+
+        public Runnable getTimeoutRunnable() {
+            return this;
+        }
+
+        @Override
+        public void run() {
+            if (mResultStillPending.getAndSet(false)) {
+                mView.unregisterContentObserver(FetchResultHandler.this);
+                mView.setFetchContentTimeout();
+            }
+        }
+
+        public void destroy() {
+            if (mResultStillPending.getAndSet(false)) {
+                mView.unregisterContentObserver(FetchResultHandler.this);
+                mHandler.removeCallbacks(this);
+            }
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            mAsyncTaskExecutor.submit(Tasks.CHECK_CONTENT_AFTER_CHANGE,
+                    new AsyncTask<Void, Void, Boolean>() {
+                @Override
+                public Boolean doInBackground(Void... params) {
+                    return mView.queryHasContent(mVoicemailUri);
+                }
+
+                @Override
+                public void onPostExecute(Boolean hasContent) {
+                    if (hasContent) {
+                        if (mResultStillPending.getAndSet(false)) {
+                            mView.unregisterContentObserver(FetchResultHandler.this);
+                            postSuccessfullyFetchedContent();
+                        }
+                    }
+                }
+            });
+        }
+    }
+
+    /**
+     * Prepares the voicemail content for playback.
+     * <p>
+     * This method will be called once we know that our voicemail has content (according to the
+     * content provider). This method will try to prepare the data source through the media player.
+     * If preparing the media player works, we will call through to
+     * {@link #postSuccessfulPrepareActions()}. If preparing the media player fails (perhaps the
+     * file the content provider points to is actually missing, perhaps it is of an unknown file
+     * format that we can't play, who knows) then we will show an error on the ui.
+     */
+    private void postSuccessfullyFetchedContent() {
+        mView.setIsBuffering();
+        mAsyncTaskExecutor.submit(Tasks.PREPARE_MEDIA_PLAYER,
+                new AsyncTask<Void, Void, Exception>() {
+                    @Override
+                    public Exception doInBackground(Void... params) {
+                        try {
+                            mPlayer.reset();
+                            mPlayer.setDataSource(mView.getDataSourceContext(), mVoicemailUri);
+                            mPlayer.setAudioStreamType(PLAYBACK_STREAM);
+                            mPlayer.prepare();
+                            return null;
+                        } catch (Exception e) {
+                            return e;
+                        }
+                    }
+
+                    @Override
+                    public void onPostExecute(Exception exception) {
+                        if (exception == null) {
+                            postSuccessfulPrepareActions();
+                        } else {
+                            mView.playbackError(exception);
+                        }
+                    }
+                });
+    }
+
+    /**
+     * Enables the ui, and optionally starts playback immediately.
+     * <p>
+     * This will be called once we have successfully prepared the media player, and will optionally
+     * playback immediately.
+     */
+    private void postSuccessfulPrepareActions() {
+        mView.enableUiElements();
+        mView.setPositionSeekListener(new PlaybackPositionListener());
+        mView.setStartStopListener(new StartStopButtonListener());
+        mView.setSpeakerphoneListener(new SpeakerphoneListener());
+        mPlayer.setOnErrorListener(new MediaPlayerErrorListener());
+        mPlayer.setOnCompletionListener(new MediaPlayerCompletionListener());
+        mView.setSpeakerPhoneOn(mView.isSpeakerPhoneOn());
+        mView.setRateDecreaseButtonListener(createRateDecreaseListener());
+        mView.setRateIncreaseButtonListener(createRateIncreaseListener());
+        mView.setClipPosition(0, mPlayer.getDuration());
+        mView.playbackStopped();
+        // Always disable on stop.
+        mView.disableProximitySensor();
+        if (mStartPlayingImmediately) {
+            resetPrepareStartPlaying(0);
+        }
+        // TODO: Now I'm ignoring the bundle, when previously I was checking for contains against
+        // the PAUSED_STATE_KEY, and CLIP_POSITION_KEY.
+    }
+
+    public void onSaveInstanceState(Bundle outState) {
+        outState.putInt(CLIP_POSITION_KEY, mView.getDesiredClipPosition());
+        if (!mPlayer.isPlaying()) {
+            outState.putBoolean(PAUSED_STATE_KEY, true);
+        }
+    }
+
+    public void onDestroy() {
+        mPlayer.release();
+        if (mFetchResultHandler != null) {
+            mFetchResultHandler.destroy();
+            mFetchResultHandler = null;
+        }
+        mPositionUpdater.stopUpdating();
+        if (mWakeLock.isHeld()) {
+            mWakeLock.release();
+        }
+    }
+
+    private class MediaPlayerErrorListener implements MediaPlayer.OnErrorListener {
+        @Override
+        public boolean onError(MediaPlayer mp, int what, int extra) {
+            mView.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    handleError(new IllegalStateException("MediaPlayer error listener invoked"));
+                }
+            });
+            return true;
+        }
+    }
+
+    private class MediaPlayerCompletionListener implements MediaPlayer.OnCompletionListener {
+        @Override
+        public void onCompletion(final MediaPlayer mp) {
+            mView.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    handleCompletion(mp);
+                }
+            });
+        }
+    }
+
+    public View.OnClickListener createRateDecreaseListener() {
+        return new RateChangeListener(false);
+    }
+
+    public View.OnClickListener createRateIncreaseListener() {
+        return new RateChangeListener(true);
+    }
+
+    /**
+     * Listens to clicks on the rate increase and decrease buttons.
+     * <p>
+     * This class is not thread-safe, but all interactions with it will happen on the ui thread.
+     */
+    private class RateChangeListener implements View.OnClickListener {
+        private final boolean mIncrease;
+
+        public RateChangeListener(boolean increase) {
+            mIncrease = increase;
+        }
+
+        @Override
+        public void onClick(View v) {
+            // Adjust the current rate, then clamp it to the allowed values.
+            mRateIndex = constrain(mRateIndex + (mIncrease ? 1 : -1), 0, PRESET_RATES.length - 1);
+            // Whether or not we have actually changed the index, call changeRate().
+            // This will ensure that we show the "fastest" or "slowest" text on the ui to indicate
+            // to the user that it doesn't get any faster or slower.
+            changeRate(PRESET_RATES[mRateIndex], PRESET_NAMES[mRateIndex]);
+        }
+    }
+
+    private void resetPrepareStartPlaying(final int clipPositionInMillis) {
+        if (mPrepareTask != null) {
+            mPrepareTask.cancel(false);
+        }
+        mPrepareTask = mAsyncTaskExecutor.submit(Tasks.RESET_PREPARE_START_MEDIA_PLAYER,
+                new AsyncTask<Void, Void, Exception>() {
+                    @Override
+                    public Exception doInBackground(Void... params) {
+                        try {
+                            mPlayer.reset();
+                            mPlayer.setDataSource(mView.getDataSourceContext(), mVoicemailUri);
+                            mPlayer.setAudioStreamType(PLAYBACK_STREAM);
+                            mPlayer.prepare();
+                            return null;
+                        } catch (Exception e) {
+                            return e;
+                        }
+                    }
+
+                    @Override
+                    public void onPostExecute(Exception exception) {
+                        mPrepareTask = null;
+                        if (exception == null) {
+                            mDuration.set(mPlayer.getDuration());
+                            int startPosition =
+                                    constrain(clipPositionInMillis, 0, mDuration.get());
+                            mView.setClipPosition(startPosition, mDuration.get());
+                            mPlayer.seekTo(startPosition);
+                            mPlayer.start();
+                            mView.playbackStarted();
+                            if (!mWakeLock.isHeld()) {
+                                mWakeLock.acquire();
+                            }
+                            // Only enable if we are not currently using the speaker phone.
+                            if (!mView.isSpeakerPhoneOn()) {
+                                mView.enableProximitySensor();
+                            }
+                            mPositionUpdater.startUpdating(startPosition, mDuration.get());
+                        } else {
+                            handleError(exception);
+                        }
+                    }
+                });
+    }
+
+    private void handleError(Exception e) {
+        mView.playbackError(e);
+        mPositionUpdater.stopUpdating();
+        mPlayer.release();
+    }
+
+    public void handleCompletion(MediaPlayer mediaPlayer) {
+        stopPlaybackAtPosition(0, mDuration.get());
+    }
+
+    private void stopPlaybackAtPosition(int clipPosition, int duration) {
+        mPositionUpdater.stopUpdating();
+        mView.playbackStopped();
+        if (mWakeLock.isHeld()) {
+            mWakeLock.release();
+        }
+        // Always disable on stop.
+        mView.disableProximitySensor();
+        mView.setClipPosition(clipPosition, duration);
+        if (mPlayer.isPlaying()) {
+            mPlayer.pause();
+        }
+    }
+
+    private class PlaybackPositionListener implements SeekBar.OnSeekBarChangeListener {
+        private boolean mShouldResumePlaybackAfterSeeking;
+
+        @Override
+        public void onStartTrackingTouch(SeekBar arg0) {
+            if (mPlayer.isPlaying()) {
+                mShouldResumePlaybackAfterSeeking = true;
+                stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get());
+            } else {
+                mShouldResumePlaybackAfterSeeking = false;
+            }
+        }
+
+        @Override
+        public void onStopTrackingTouch(SeekBar arg0) {
+            if (mPlayer.isPlaying()) {
+                stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get());
+            }
+            if (mShouldResumePlaybackAfterSeeking) {
+                resetPrepareStartPlaying(mView.getDesiredClipPosition());
+            }
+        }
+
+        @Override
+        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+            mView.setClipPosition(seekBar.getProgress(), seekBar.getMax());
+        }
+    }
+
+    private void changeRate(float rate, int stringResourceId) {
+        ((SingleThreadedMediaPlayerProxy) mPlayer).setVariableSpeed(rate);
+        mView.setRateDisplay(rate, stringResourceId);
+    }
+
+    private class SpeakerphoneListener implements View.OnClickListener {
+        @Override
+        public void onClick(View v) {
+            boolean previousState = mView.isSpeakerPhoneOn();
+            mView.setSpeakerPhoneOn(!previousState);
+            if (mPlayer.isPlaying() && previousState) {
+                // If we are currently playing and we are disabling the speaker phone, enable the
+                // sensor.
+                mView.enableProximitySensor();
+            } else {
+                // If we are not currently playing, disable the sensor.
+                mView.disableProximitySensor();
+            }
+        }
+    }
+
+    private class StartStopButtonListener implements View.OnClickListener {
+        @Override
+        public void onClick(View arg0) {
+            if (mPlayer.isPlaying()) {
+                stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get());
+            } else {
+                resetPrepareStartPlaying(mView.getDesiredClipPosition());
+            }
+        }
+    }
+
+    /**
+     * Controls the animation of the playback slider.
+     */
+    @ThreadSafe
+    private final class PositionUpdater implements Runnable {
+        private final ScheduledExecutorService mExecutorService;
+        private final int mPeriodMillis;
+        private final Object mLock = new Object();
+        @GuardedBy("mLock") private ScheduledFuture<?> mScheduledFuture;
+        private final Runnable mSetClipPostitionRunnable = new Runnable() {
+            @Override
+            public void run() {
+                int currentPosition = 0;
+                synchronized (mLock) {
+                    if (mScheduledFuture == null) {
+                        // This task has been canceled. Just stop now.
+                        return;
+                    }
+                    currentPosition = mPlayer.getCurrentPosition();
+                }
+                mView.setClipPosition(currentPosition, mDuration.get());
+            }
+        };
+
+        public PositionUpdater(ScheduledExecutorService executorService, int periodMillis) {
+            mExecutorService = executorService;
+            mPeriodMillis = periodMillis;
+        }
+
+        @Override
+        public void run() {
+            mView.runOnUiThread(mSetClipPostitionRunnable);
+        }
+
+        public void startUpdating(int beginPosition, int endPosition) {
+            synchronized (mLock) {
+                if (mScheduledFuture != null) {
+                    mScheduledFuture.cancel(false);
+                }
+                mScheduledFuture = mExecutorService.scheduleAtFixedRate(this, 0, mPeriodMillis,
+                        TimeUnit.MILLISECONDS);
+            }
+        }
+
+        public void stopUpdating() {
+            synchronized (mLock) {
+                if (mScheduledFuture != null) {
+                    mScheduledFuture.cancel(false);
+                    mScheduledFuture = null;
+                }
+            }
+        }
+    }
+
+    public void onPause() {
+        if (mPlayer.isPlaying()) {
+            stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get());
+        }
+        if (mPrepareTask != null) {
+            mPrepareTask.cancel(false);
+        }
+        if (mWakeLock.isHeld()) {
+            mWakeLock.release();
+        }
+    }
+}
diff --git a/src/com/android/dialer/voicemail/VoicemailStatusHelper.java b/src/com/android/dialer/voicemail/VoicemailStatusHelper.java
new file mode 100644
index 0000000..545691e
--- /dev/null
+++ b/src/com/android/dialer/voicemail/VoicemailStatusHelper.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2011 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.dialer.voicemail;
+
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.VoicemailContract.Status;
+
+import java.util.List;
+
+/**
+ * Interface used by the call log UI to determine what user message, if any, related to voicemail
+ * source status needs to be shown. The messages are returned in the order of importance.
+ * <p>
+ * The implementation of this interface interacts with the voicemail content provider to fetch
+ * statuses of all the registered voicemail sources and determines if any status message needs to
+ * be shown. The user of this interface must observe/listen to provider changes and invoke
+ * this class to check if any message needs to be shown.
+ */
+public interface VoicemailStatusHelper {
+    public class StatusMessage {
+        /** Package of the source on behalf of which this message has to be shown.*/
+        public final String sourcePackage;
+        /**
+         * The string resource id of the status message that should be shown in the call log
+         * page. Set to -1, if this message is not to be shown in call log.
+         */
+        public final int callLogMessageId;
+        /**
+         * The string resource id of the status message that should be shown in the call details
+         * page. Set to -1, if this message is not to be shown in call details page.
+         */
+        public final int callDetailsMessageId;
+        /** The string resource id of the action message that should be shown. */
+        public final int actionMessageId;
+        /** URI for the corrective action, where applicable. Null if no action URI is available. */
+        public final Uri actionUri;
+        public StatusMessage(String sourcePackage, int callLogMessageId, int callDetailsMessageId,
+                int actionMessageId, Uri actionUri) {
+            this.sourcePackage = sourcePackage;
+            this.callLogMessageId = callLogMessageId;
+            this.callDetailsMessageId = callDetailsMessageId;
+            this.actionMessageId = actionMessageId;
+            this.actionUri = actionUri;
+        }
+
+        /** Whether this message should be shown in the call log page. */
+        public boolean showInCallLog() {
+            return callLogMessageId != -1;
+        }
+
+        /** Whether this message should be shown in the call details page. */
+        public boolean showInCallDetails() {
+            return callDetailsMessageId != -1;
+        }
+    }
+
+    /**
+     * Returns a list of messages, in the order or priority that should be shown to the user. An
+     * empty list is returned if no message needs to be shown.
+     * @param cursor The cursor pointing to the query on {@link Status#CONTENT_URI}. The projection
+     *      to be used is defined by the implementation class of this interface.
+     */
+    public List<StatusMessage> getStatusMessages(Cursor cursor);
+
+    /**
+     * Returns the number of active voicemail sources installed.
+     * <p>
+     * The number of sources is counted by querying the voicemail status table.
+     */
+    public int getNumberActivityVoicemailSources(Cursor cursor);
+}
diff --git a/src/com/android/dialer/voicemail/VoicemailStatusHelperImpl.java b/src/com/android/dialer/voicemail/VoicemailStatusHelperImpl.java
new file mode 100644
index 0000000..3a08e2b
--- /dev/null
+++ b/src/com/android/dialer/voicemail/VoicemailStatusHelperImpl.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2011 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.dialer.voicemail;
+
+import static android.provider.VoicemailContract.Status.CONFIGURATION_STATE_CAN_BE_CONFIGURED;
+import static android.provider.VoicemailContract.Status.CONFIGURATION_STATE_OK;
+import static android.provider.VoicemailContract.Status.DATA_CHANNEL_STATE_NO_CONNECTION;
+import static android.provider.VoicemailContract.Status.DATA_CHANNEL_STATE_OK;
+import static android.provider.VoicemailContract.Status.NOTIFICATION_CHANNEL_STATE_MESSAGE_WAITING;
+import static android.provider.VoicemailContract.Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION;
+import static android.provider.VoicemailContract.Status.NOTIFICATION_CHANNEL_STATE_OK;
+
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.VoicemailContract.Status;
+
+import com.android.contacts.R;
+import com.android.contacts.util.UriUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/** Implementation of {@link VoicemailStatusHelper}. */
+public class VoicemailStatusHelperImpl implements VoicemailStatusHelper {
+    private static final int SOURCE_PACKAGE_INDEX = 0;
+    private static final int CONFIGURATION_STATE_INDEX = 1;
+    private static final int DATA_CHANNEL_STATE_INDEX = 2;
+    private static final int NOTIFICATION_CHANNEL_STATE_INDEX = 3;
+    private static final int SETTINGS_URI_INDEX = 4;
+    private static final int VOICEMAIL_ACCESS_URI_INDEX = 5;
+    private static final int NUM_COLUMNS = 6;
+    /** Projection on the voicemail_status table used by this class. */
+    public static final String[] PROJECTION = new String[NUM_COLUMNS];
+    static {
+        PROJECTION[SOURCE_PACKAGE_INDEX] = Status.SOURCE_PACKAGE;
+        PROJECTION[CONFIGURATION_STATE_INDEX] = Status.CONFIGURATION_STATE;
+        PROJECTION[DATA_CHANNEL_STATE_INDEX] = Status.DATA_CHANNEL_STATE;
+        PROJECTION[NOTIFICATION_CHANNEL_STATE_INDEX] = Status.NOTIFICATION_CHANNEL_STATE;
+        PROJECTION[SETTINGS_URI_INDEX] = Status.SETTINGS_URI;
+        PROJECTION[VOICEMAIL_ACCESS_URI_INDEX] = Status.VOICEMAIL_ACCESS_URI;
+    }
+
+    /** Possible user actions. */
+    public static enum Action {
+        NONE(-1),
+        CALL_VOICEMAIL(R.string.voicemail_status_action_call_server),
+        CONFIGURE_VOICEMAIL(R.string.voicemail_status_action_configure);
+
+        private final int mMessageId;
+        private Action(int messageId) {
+            mMessageId = messageId;
+        }
+
+        public int getMessageId() {
+            return mMessageId;
+        }
+    }
+
+    /**
+     * Overall state of the source status. Each state is associated with the corresponding display
+     * string and the corrective action. The states are also assigned a relative priority which is
+     * used to order the messages from different sources.
+     */
+    private static enum OverallState {
+        // TODO: Add separate string for call details and call log pages for the states that needs
+        // to be shown in both.
+        /** Both notification and data channel are not working. */
+        NO_CONNECTION(0, Action.CALL_VOICEMAIL, R.string.voicemail_status_voicemail_not_available,
+                R.string.voicemail_status_audio_not_available),
+        /** Notifications working, but data channel is not working. Audio cannot be downloaded. */
+        NO_DATA(1, Action.CALL_VOICEMAIL, R.string.voicemail_status_voicemail_not_available,
+                R.string.voicemail_status_audio_not_available),
+        /** Messages are known to be waiting but data channel is not working. */
+        MESSAGE_WAITING(2, Action.CALL_VOICEMAIL, R.string.voicemail_status_messages_waiting,
+                R.string.voicemail_status_audio_not_available),
+        /** Notification channel not working, but data channel is. */
+        NO_NOTIFICATIONS(3, Action.CALL_VOICEMAIL,
+                R.string.voicemail_status_voicemail_not_available),
+        /** Invite user to set up voicemail. */
+        INVITE_FOR_CONFIGURATION(4, Action.CONFIGURE_VOICEMAIL,
+                R.string.voicemail_status_configure_voicemail),
+        /**
+         * No detailed notifications, but data channel is working.
+         * This is normal mode of operation for certain sources. No action needed.
+         */
+        NO_DETAILED_NOTIFICATION(5, Action.NONE, -1),
+        /** Visual voicemail not yet set up. No local action needed. */
+        NOT_CONFIGURED(6, Action.NONE, -1),
+        /** Everything is OK. */
+        OK(7, Action.NONE, -1),
+        /** If one or more state value set by the source is not valid. */
+        INVALID(8, Action.NONE, -1);
+
+        private final int mPriority;
+        private final Action mAction;
+        private final int mCallLogMessageId;
+        private final int mCallDetailsMessageId;
+
+        private OverallState(int priority, Action action, int callLogMessageId) {
+            this(priority, action, callLogMessageId, -1);
+        }
+
+        private OverallState(int priority, Action action, int callLogMessageId,
+                int callDetailsMessageId) {
+            mPriority = priority;
+            mAction = action;
+            mCallLogMessageId = callLogMessageId;
+            mCallDetailsMessageId = callDetailsMessageId;
+        }
+
+        public Action getAction() {
+            return mAction;
+        }
+
+        public int getPriority() {
+            return mPriority;
+        }
+
+        public int getCallLogMessageId() {
+            return mCallLogMessageId;
+        }
+
+        public int getCallDetailsMessageId() {
+            return mCallDetailsMessageId;
+        }
+    }
+
+    /** A wrapper on {@link StatusMessage} which additionally stores the priority of the message. */
+    private static class MessageStatusWithPriority {
+        private final StatusMessage mMessage;
+        private final int mPriority;
+
+        public MessageStatusWithPriority(StatusMessage message, int priority) {
+            mMessage = message;
+            mPriority = priority;
+        }
+    }
+
+    @Override
+    public List<StatusMessage> getStatusMessages(Cursor cursor) {
+        List<MessageStatusWithPriority> messages =
+            new ArrayList<VoicemailStatusHelperImpl.MessageStatusWithPriority>();
+        cursor.moveToPosition(-1);
+        while(cursor.moveToNext()) {
+            MessageStatusWithPriority message = getMessageForStatusEntry(cursor);
+            if (message != null) {
+                messages.add(message);
+            }
+        }
+        // Finally reorder the messages by their priority.
+        return reorderMessages(messages);
+    }
+
+    @Override
+    public int getNumberActivityVoicemailSources(Cursor cursor) {
+        int count = 0;
+        cursor.moveToPosition(-1);
+        while(cursor.moveToNext()) {
+            if (isVoicemailSourceActive(cursor)) {
+                ++count;
+            }
+        }
+        return count;
+    }
+
+    /** Returns whether the source status in the cursor corresponds to an active source. */
+    private boolean isVoicemailSourceActive(Cursor cursor) {
+        return cursor.getString(SOURCE_PACKAGE_INDEX) != null
+                &&  cursor.getInt(CONFIGURATION_STATE_INDEX) == Status.CONFIGURATION_STATE_OK;
+    }
+
+    private List<StatusMessage> reorderMessages(List<MessageStatusWithPriority> messageWrappers) {
+        Collections.sort(messageWrappers, new Comparator<MessageStatusWithPriority>() {
+            @Override
+            public int compare(MessageStatusWithPriority msg1, MessageStatusWithPriority msg2) {
+                return msg1.mPriority - msg2.mPriority;
+            }
+        });
+        List<StatusMessage> reorderMessages = new ArrayList<VoicemailStatusHelper.StatusMessage>();
+        // Copy the ordered message objects into the final list.
+        for (MessageStatusWithPriority messageWrapper : messageWrappers) {
+            reorderMessages.add(messageWrapper.mMessage);
+        }
+        return reorderMessages;
+    }
+
+    /**
+     * Returns the message for the status entry pointed to by the cursor.
+     */
+    private MessageStatusWithPriority getMessageForStatusEntry(Cursor cursor) {
+        final String sourcePackage = cursor.getString(SOURCE_PACKAGE_INDEX);
+        if (sourcePackage == null) {
+            return null;
+        }
+        final OverallState overallState = getOverallState(cursor.getInt(CONFIGURATION_STATE_INDEX),
+                cursor.getInt(DATA_CHANNEL_STATE_INDEX),
+                cursor.getInt(NOTIFICATION_CHANNEL_STATE_INDEX));
+        final Action action = overallState.getAction();
+
+        // No source package or no action, means no message shown.
+        if (action == Action.NONE) {
+            return null;
+        }
+
+        Uri actionUri = null;
+        if (action == Action.CALL_VOICEMAIL) {
+            actionUri = UriUtils.parseUriOrNull(cursor.getString(VOICEMAIL_ACCESS_URI_INDEX));
+            // Even if actionUri is null, it is still be useful to show the notification.
+        } else if (action == Action.CONFIGURE_VOICEMAIL) {
+            actionUri = UriUtils.parseUriOrNull(cursor.getString(SETTINGS_URI_INDEX));
+            // If there is no settings URI, there is no point in showing the notification.
+            if (actionUri == null) {
+                return null;
+            }
+        }
+        return new MessageStatusWithPriority(
+                new StatusMessage(sourcePackage, overallState.getCallLogMessageId(),
+                        overallState.getCallDetailsMessageId(), action.getMessageId(),
+                        actionUri),
+                overallState.getPriority());
+    }
+
+    private OverallState getOverallState(int configurationState, int dataChannelState,
+            int notificationChannelState) {
+        if (configurationState == CONFIGURATION_STATE_OK) {
+            // Voicemail is configured. Let's see how is the data channel.
+            if (dataChannelState == DATA_CHANNEL_STATE_OK) {
+                // Data channel is fine. What about notification channel?
+                if (notificationChannelState == NOTIFICATION_CHANNEL_STATE_OK) {
+                    return OverallState.OK;
+                } else if (notificationChannelState == NOTIFICATION_CHANNEL_STATE_MESSAGE_WAITING) {
+                    return OverallState.NO_DETAILED_NOTIFICATION;
+                } else if (notificationChannelState == NOTIFICATION_CHANNEL_STATE_NO_CONNECTION) {
+                    return OverallState.NO_NOTIFICATIONS;
+                }
+            } else if (dataChannelState == DATA_CHANNEL_STATE_NO_CONNECTION) {
+                // Data channel is not working. What about notification channel?
+                if (notificationChannelState == NOTIFICATION_CHANNEL_STATE_OK) {
+                    return OverallState.NO_DATA;
+                } else if (notificationChannelState == NOTIFICATION_CHANNEL_STATE_MESSAGE_WAITING) {
+                    return OverallState.MESSAGE_WAITING;
+                } else if (notificationChannelState == NOTIFICATION_CHANNEL_STATE_NO_CONNECTION) {
+                    return OverallState.NO_CONNECTION;
+                }
+            }
+        } else if (configurationState == CONFIGURATION_STATE_CAN_BE_CONFIGURED) {
+            // Voicemail not configured. data/notification channel states are irrelevant.
+            return OverallState.INVITE_FOR_CONFIGURATION;
+        } else if (configurationState == Status.CONFIGURATION_STATE_NOT_CONFIGURED) {
+            // Voicemail not configured. data/notification channel states are irrelevant.
+            return OverallState.NOT_CONFIGURED;
+        }
+        // Will reach here only if the source has set an invalid value.
+        return OverallState.INVALID;
+    }
+}
diff --git a/tests/Android.mk b/tests/Android.mk
new file mode 100644
index 0000000..d440f6a
--- /dev/null
+++ b/tests/Android.mk
@@ -0,0 +1,19 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+LOCAL_CERTIFICATE := shared
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
+# Include all test java files.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_STATIC_JAVA_LIBRARIES += com.android.contacts.common.test
+
+LOCAL_PACKAGE_NAME := DialerTests
+
+LOCAL_INSTRUMENTATION_FOR := Dialer
+
+include $(BUILD_PACKAGE)
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
new file mode 100644
index 0000000..3a714e3
--- /dev/null
+++ b/tests/AndroidManifest.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2012 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
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.dialer.tests">
+
+    <uses-permission android:name="android.permission.READ_CONTACTS" />
+    <uses-permission android:name="android.permission.WRITE_CONTACTS" />
+    <uses-permission android:name="android.permission.READ_CALL_LOG" />
+    <uses-permission android:name="android.permission.WRITE_CALL_LOG" />
+    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+
+    <uses-permission android:name="android.permission.USE_CREDENTIALS" />
+    <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
+    <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
+    <uses-permission android:name="android.permission.READ_SYNC_STATS" />
+    <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
+    <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
+
+    <uses-permission android:name="android.permission.READ_PROFILE" />
+    <uses-permission android:name="android.permission.READ_SOCIAL_STREAM" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+        <meta-data android:name="com.android.dialer.iconset" android:resource="@xml/iconset" />
+
+        <activity android:name=".calllog.FillCallLogTestActivity"
+            android:label="Call log filter test"
+            >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+    <instrumentation android:name="android.test.InstrumentationTestRunner"
+        android:targetPackage="com.android.dialer"
+        android:label="Dialer app tests">
+    </instrumentation>
+
+    <instrumentation android:name="com.android.dialer.DialerLaunchPerformance"
+        android:targetPackage="com.android.dialer"
+        android:label="Dialer launch performance">
+    </instrumentation>
+
+</manifest>
diff --git a/tests/proguard.flags b/tests/proguard.flags
new file mode 100644
index 0000000..39784b1
--- /dev/null
+++ b/tests/proguard.flags
@@ -0,0 +1,20 @@
+-keep class com.android.contacts.model.Sources {
+  public <init>(...);
+}
+
+# Xml files containing onClick (menus and layouts) require that proguard not
+# remove their handlers.
+-keepclassmembers class * extends android.app.Activity {
+  public void *(android.view.View);
+  public void *(android.view.MenuItem);
+}
+
+# Any class or method annotated with NeededForTesting or NeededForReflection.
+-keep @com.android.contacts.test.NeededForTesting class *
+-keep @com.android.contacts.test.NeededForReflection class *
+-keepclassmembers class * {
+@com.android.contacts.test.NeededForTesting *;
+@com.android.contacts.test.NeededForReflection *;
+}
+
+-verbose
diff --git a/tests/res/drawable/default_icon.png b/tests/res/drawable/default_icon.png
new file mode 100644
index 0000000..cea0eb3
--- /dev/null
+++ b/tests/res/drawable/default_icon.png
Binary files differ
diff --git a/tests/res/drawable/phone_icon.png b/tests/res/drawable/phone_icon.png
new file mode 100644
index 0000000..4e613ec
--- /dev/null
+++ b/tests/res/drawable/phone_icon.png
Binary files differ
diff --git a/tests/res/layout/fill_call_log_test.xml b/tests/res/layout/fill_call_log_test.xml
new file mode 100644
index 0000000..704b9c6
--- /dev/null
+++ b/tests/res/layout/fill_call_log_test.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2012 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="center_horizontal"
+>
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/numberOfCallLogEntries"
+    />
+    <EditText
+        android:id="@+id/number"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:inputType="number"
+        android:text="10"
+        />
+    <CheckBox
+        android:id="@+id/use_random_numbers"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/useRandomNumbers"
+    />
+    <Button
+        android:id="@+id/add"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/addToCallLogButton"
+    />
+    <ProgressBar
+        android:id="@+id/progress"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:indeterminate="false"
+        android:visibility="gone"
+    />
+</LinearLayout>
diff --git a/tests/res/values/donottranslate_strings.xml b/tests/res/values/donottranslate_strings.xml
new file mode 100644
index 0000000..ceba5ea
--- /dev/null
+++ b/tests/res/values/donottranslate_strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2012 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
+  -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+    <string-array name="allIntents">
+        <!-- List modes -->
+        <!-- Various ways to start Contacts -->
+        <item>DIAL</item>
+        <item>DIAL phone (deprecated)</item>
+        <item>DIAL person (deprecated)</item>
+        <item>DIAL voicemail</item>
+        <item>CALL BUTTON</item>
+        <item>DIAL tel</item>
+        <item>VIEW tel</item>
+        <item>VIEW calls (call-log after a phone call)</item>
+        <item>VIEW calls item</item>
+        <item>CallDetailActivity (legacy)</item>
+        <item>CallLogActivity (legacy)</item>
+    </string-array>
+
+    <string name="addToCallLogButton">Add</string>
+    <string name="useRandomNumbers">Use random numbers</string>
+    <string name="numberOfCallLogEntries">Number of call log entries to add:</string>
+    <string name="addedLogEntriesToast">Added %1$d call log entries.</string>
+    <string name="noLogEntriesToast">No entries in the call log yet.  Need at least one record for the template.  Or use random numbers.</string>
+
+</resources>
diff --git a/tests/res/xml/iconset.xml b/tests/res/xml/iconset.xml
new file mode 100644
index 0000000..ec38945
--- /dev/null
+++ b/tests/res/xml/iconset.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2012 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
+  -->
+
+<icon-set xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <icon-default android:icon="@drawable/default_icon" />
+    <icon android:mimeType="vnd.android.cursor.item/phone"
+        android:icon="@drawable/phone_icon" />
+
+</icon-set>
diff --git a/tests/src/com/android/dialer/CallDetailActivityTest.java b/tests/src/com/android/dialer/CallDetailActivityTest.java
new file mode 100644
index 0000000..4320465
--- /dev/null
+++ b/tests/src/com/android/dialer/CallDetailActivityTest.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2011 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.dialer;
+
+import static com.android.dialer.CallDetailActivity.Tasks.UPDATE_PHONE_CALL_DETAILS;
+import static com.android.dialer.voicemail.VoicemailPlaybackPresenter.Tasks.CHECK_FOR_CONTENT;
+import static com.android.dialer.voicemail.VoicemailPlaybackPresenter.Tasks.PREPARE_MEDIA_PLAYER;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.content.res.AssetManager;
+import android.net.Uri;
+import android.provider.CallLog;
+import android.provider.VoicemailContract;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.Suppress;
+import android.view.Menu;
+import android.widget.TextView;
+
+import com.android.contacts.util.AsyncTaskExecutors;
+import com.android.dialer.util.FakeAsyncTaskExecutor;
+import com.android.contacts.common.test.IntegrationTestUtils;
+import com.android.dialer.util.LocaleTestUtils;
+import com.android.internal.view.menu.ContextMenuBuilder;
+import com.google.common.io.Closeables;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Unit tests for the {@link CallDetailActivity}.
+ */
+@LargeTest
+public class CallDetailActivityTest extends ActivityInstrumentationTestCase2<CallDetailActivity> {
+    private static final String TEST_ASSET_NAME = "quick_test_recording.mp3";
+    private static final String MIME_TYPE = "audio/mp3";
+    private static final String CONTACT_NUMBER = "+1412555555";
+    private static final String VOICEMAIL_FILE_LOCATION = "/sdcard/sadlfj893w4j23o9sfu.mp3";
+
+    private Uri mCallLogUri;
+    private Uri mVoicemailUri;
+    private IntegrationTestUtils mTestUtils;
+    private LocaleTestUtils mLocaleTestUtils;
+    private FakeAsyncTaskExecutor mFakeAsyncTaskExecutor;
+    private CallDetailActivity mActivityUnderTest;
+
+    public CallDetailActivityTest() {
+        super(CallDetailActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mFakeAsyncTaskExecutor = new FakeAsyncTaskExecutor(getInstrumentation());
+        AsyncTaskExecutors.setFactoryForTest(mFakeAsyncTaskExecutor.getFactory());
+        // I don't like the default of focus-mode for tests, the green focus border makes the
+        // screenshots look weak.
+        setActivityInitialTouchMode(true);
+        mTestUtils = new IntegrationTestUtils(getInstrumentation());
+        // Some of the tests rely on the text that appears on screen - safest to force a
+        // specific locale.
+        mLocaleTestUtils = new LocaleTestUtils(getInstrumentation().getTargetContext());
+        mLocaleTestUtils.setLocale(Locale.US);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mLocaleTestUtils.restoreLocale();
+        mLocaleTestUtils = null;
+        cleanUpUri();
+        mTestUtils = null;
+        AsyncTaskExecutors.setFactoryForTest(null);
+        super.tearDown();
+    }
+
+    public void testInitialActivityStartsWithFetchingVoicemail() throws Throwable {
+        setActivityIntentForTestVoicemailEntry();
+        startActivityUnderTest();
+        // When the activity first starts, we will show "Fetching voicemail" on the screen.
+        // The duration should not be visible.
+        assertHasOneTextViewContaining("Fetching voicemail");
+        assertZeroTextViewsContaining("00:00");
+    }
+
+    public void testWhenCheckForContentCompletes_UiShowsBuffering() throws Throwable {
+        setActivityIntentForTestVoicemailEntry();
+        startActivityUnderTest();
+        // There is a background check that is testing to see if we have the content available.
+        // Once that task completes, we shouldn't be showing the fetching message, we should
+        // be showing "Buffering".
+        mFakeAsyncTaskExecutor.runTask(CHECK_FOR_CONTENT);
+        assertHasOneTextViewContaining("Buffering");
+        assertZeroTextViewsContaining("Fetching voicemail");
+    }
+
+    public void testInvalidVoicemailShowsErrorMessage() throws Throwable {
+        setActivityIntentForTestVoicemailEntry();
+        startActivityUnderTest();
+        mFakeAsyncTaskExecutor.runTask(CHECK_FOR_CONTENT);
+        // There should be exactly one background task ready to prepare the media player.
+        // Preparing the media player will have thrown an IOException since the file doesn't exist.
+        // This should have put a failed to play message on screen, buffering is gone.
+        mFakeAsyncTaskExecutor.runTask(PREPARE_MEDIA_PLAYER);
+        assertHasOneTextViewContaining("Couldn't play voicemail");
+        assertZeroTextViewsContaining("Buffering");
+    }
+
+    public void testOnResumeDoesNotCreateManyFragments() throws Throwable {
+        // There was a bug where every time the activity was resumed, a new fragment was created.
+        // Before the fix, this was failing reproducibly with at least 3 "Buffering" views.
+        setActivityIntentForTestVoicemailEntry();
+        startActivityUnderTest();
+        mFakeAsyncTaskExecutor.runTask(CHECK_FOR_CONTENT);
+        getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                getInstrumentation().callActivityOnPause(mActivityUnderTest);
+                getInstrumentation().callActivityOnResume(mActivityUnderTest);
+                getInstrumentation().callActivityOnPause(mActivityUnderTest);
+                getInstrumentation().callActivityOnResume(mActivityUnderTest);
+            }
+        });
+        assertHasOneTextViewContaining("Buffering");
+    }
+
+    /**
+     * Test for bug where increase rate button with invalid voicemail causes a crash.
+     * <p>
+     * The repro steps for this crash were to open a voicemail that does not have an attachment,
+     * then click the play button (which just reported an error), then after that try to adjust the
+     * rate.  See http://b/5047879.
+     */
+    public void testClickIncreaseRateButtonWithInvalidVoicemailDoesNotCrash() throws Throwable {
+        setActivityIntentForTestVoicemailEntry();
+        startActivityUnderTest();
+        mTestUtils.clickButton(mActivityUnderTest, R.id.playback_start_stop);
+        mTestUtils.clickButton(mActivityUnderTest, R.id.rate_increase_button);
+    }
+
+    /** Test for bug where missing Extras on intent used to start Activity causes NPE. */
+    public void testCallLogUriWithMissingExtrasShouldNotCauseNPE() throws Throwable {
+        setActivityIntentForTestCallEntry();
+        startActivityUnderTest();
+    }
+
+    /**
+     * Test for bug where voicemails should not have remove-from-call-log entry.
+     * <p>
+     * See http://b/5054103.
+     */
+    public void testVoicemailDoesNotHaveRemoveFromCallLog() throws Throwable {
+        setActivityIntentForTestVoicemailEntry();
+        startActivityUnderTest();
+        Menu menu = new ContextMenuBuilder(mActivityUnderTest);
+        mActivityUnderTest.onCreateOptionsMenu(menu);
+        mActivityUnderTest.onPrepareOptionsMenu(menu);
+        assertFalse(menu.findItem(R.id.menu_remove_from_call_log).isVisible());
+    }
+
+    /** Test to check that I haven't broken the remove-from-call-log entry from regular calls. */
+    public void testRegularCallDoesHaveRemoveFromCallLog() throws Throwable {
+        setActivityIntentForTestCallEntry();
+        startActivityUnderTest();
+        Menu menu = new ContextMenuBuilder(mActivityUnderTest);
+        mActivityUnderTest.onCreateOptionsMenu(menu);
+        mActivityUnderTest.onPrepareOptionsMenu(menu);
+        assertTrue(menu.findItem(R.id.menu_remove_from_call_log).isVisible());
+    }
+
+    /**
+     * Test to show that we are correctly displaying playback rate on the ui.
+     * <p>
+     * See bug http://b/5044075.
+     */
+    @Suppress
+    public void testVoicemailPlaybackRateDisplayedOnUi() throws Throwable {
+        setActivityIntentForTestVoicemailEntry();
+        startActivityUnderTest();
+        // Find the TextView containing the duration.  It should be initially displaying "00:00".
+        List<TextView> views = mTestUtils.getTextViewsWithString(mActivityUnderTest, "00:00");
+        assertEquals(1, views.size());
+        TextView timeDisplay = views.get(0);
+        // Hit the plus button.  At this point we should be displaying "fast speed".
+        mTestUtils.clickButton(mActivityUnderTest, R.id.rate_increase_button);
+        assertEquals("fast speed", mTestUtils.getText(timeDisplay));
+        // Hit the minus button.  We should be back to "normal" speed.
+        mTestUtils.clickButton(mActivityUnderTest, R.id.rate_decrease_button);
+        assertEquals("normal speed", mTestUtils.getText(timeDisplay));
+        // Wait for one and a half seconds.  The timer will be back.
+        Thread.sleep(1500);
+        assertEquals("00:00", mTestUtils.getText(timeDisplay));
+    }
+
+    @Suppress
+    public void testClickingCallStopsPlayback() throws Throwable {
+        setActivityIntentForRealFileVoicemailEntry();
+        startActivityUnderTest();
+        mFakeAsyncTaskExecutor.runTask(CHECK_FOR_CONTENT);
+        mFakeAsyncTaskExecutor.runTask(PREPARE_MEDIA_PLAYER);
+        mTestUtils.clickButton(mActivityUnderTest, R.id.playback_speakerphone);
+        mTestUtils.clickButton(mActivityUnderTest, R.id.playback_start_stop);
+        mTestUtils.clickButton(mActivityUnderTest, R.id.call_and_sms_main_action);
+        Thread.sleep(2000);
+        // TODO: Suppressed the test for now, because I'm looking for an easy way to say "the audio
+        // is not playing at this point", and I can't find it without doing dirty things.
+    }
+
+    private void setActivityIntentForTestCallEntry() {
+        assertNull(mCallLogUri);
+        ContentResolver contentResolver = getContentResolver();
+        ContentValues values = new ContentValues();
+        values.put(CallLog.Calls.NUMBER, CONTACT_NUMBER);
+        values.put(CallLog.Calls.TYPE, CallLog.Calls.INCOMING_TYPE);
+        mCallLogUri = contentResolver.insert(CallLog.Calls.CONTENT_URI, values);
+        setActivityIntent(new Intent(Intent.ACTION_VIEW, mCallLogUri));
+    }
+
+    private void setActivityIntentForTestVoicemailEntry() {
+        assertNull(mVoicemailUri);
+        ContentResolver contentResolver = getContentResolver();
+        ContentValues values = new ContentValues();
+        values.put(VoicemailContract.Voicemails.NUMBER, CONTACT_NUMBER);
+        values.put(VoicemailContract.Voicemails.HAS_CONTENT, 1);
+        values.put(VoicemailContract.Voicemails._DATA, VOICEMAIL_FILE_LOCATION);
+        mVoicemailUri = contentResolver.insert(VoicemailContract.Voicemails.CONTENT_URI, values);
+        Uri callLogUri = ContentUris.withAppendedId(CallLog.Calls.CONTENT_URI_WITH_VOICEMAIL,
+                ContentUris.parseId(mVoicemailUri));
+        Intent intent = new Intent(Intent.ACTION_VIEW, callLogUri);
+        intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI, mVoicemailUri);
+        setActivityIntent(intent);
+    }
+
+    private void setActivityIntentForRealFileVoicemailEntry() throws IOException {
+        assertNull(mVoicemailUri);
+        ContentValues values = new ContentValues();
+        values.put(VoicemailContract.Voicemails.DATE, String.valueOf(System.currentTimeMillis()));
+        values.put(VoicemailContract.Voicemails.NUMBER, CONTACT_NUMBER);
+        values.put(VoicemailContract.Voicemails.MIME_TYPE, MIME_TYPE);
+        values.put(VoicemailContract.Voicemails.HAS_CONTENT, 1);
+        String packageName = getInstrumentation().getTargetContext().getPackageName();
+        mVoicemailUri = getContentResolver().insert(
+                VoicemailContract.Voicemails.buildSourceUri(packageName), values);
+        AssetManager assets = getAssets();
+        OutputStream outputStream = null;
+        InputStream inputStream = null;
+        try {
+            inputStream = assets.open(TEST_ASSET_NAME);
+            outputStream = getContentResolver().openOutputStream(mVoicemailUri);
+            copyBetweenStreams(inputStream, outputStream);
+        } finally {
+            Closeables.closeQuietly(outputStream);
+            Closeables.closeQuietly(inputStream);
+        }
+        Uri callLogUri = ContentUris.withAppendedId(CallLog.Calls.CONTENT_URI_WITH_VOICEMAIL,
+                ContentUris.parseId(mVoicemailUri));
+        Intent intent = new Intent(Intent.ACTION_VIEW, callLogUri);
+        intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI, mVoicemailUri);
+        setActivityIntent(intent);
+    }
+
+    public void copyBetweenStreams(InputStream in, OutputStream out) throws IOException {
+        byte[] buffer = new byte[1024];
+        int bytesRead;
+        int total = 0;
+        while ((bytesRead = in.read(buffer)) != -1) {
+            total += bytesRead;
+            out.write(buffer, 0, bytesRead);
+        }
+    }
+
+    private void cleanUpUri() {
+        if (mVoicemailUri != null) {
+            getContentResolver().delete(VoicemailContract.Voicemails.CONTENT_URI,
+                    "_ID = ?", new String[] { String.valueOf(ContentUris.parseId(mVoicemailUri)) });
+            mVoicemailUri = null;
+        }
+        if (mCallLogUri != null) {
+            getContentResolver().delete(CallLog.Calls.CONTENT_URI_WITH_VOICEMAIL,
+                    "_ID = ?", new String[] { String.valueOf(ContentUris.parseId(mCallLogUri)) });
+            mCallLogUri = null;
+        }
+    }
+
+    private ContentResolver getContentResolver() {
+        return getInstrumentation().getTargetContext().getContentResolver();
+    }
+
+    private TextView assertHasOneTextViewContaining(String text) throws Throwable {
+        assertNotNull(mActivityUnderTest);
+        List<TextView> views = mTestUtils.getTextViewsWithString(mActivityUnderTest, text);
+        assertEquals("There should have been one TextView with text '" + text + "' but found "
+                + views, 1, views.size());
+        return views.get(0);
+    }
+
+    private void assertZeroTextViewsContaining(String text) throws Throwable {
+        assertNotNull(mActivityUnderTest);
+        List<TextView> views = mTestUtils.getTextViewsWithString(mActivityUnderTest, text);
+        assertEquals("There should have been no TextViews with text '" + text + "' but found "
+                + views, 0,  views.size());
+    }
+
+    private void startActivityUnderTest() throws Throwable {
+        assertNull(mActivityUnderTest);
+        mActivityUnderTest = getActivity();
+        assertNotNull("activity should not be null", mActivityUnderTest);
+        // We have to run all tasks, not just one.
+        // This is because it seems that we can have onResume, onPause, onResume during the course
+        // of a single unit test.
+        mFakeAsyncTaskExecutor.runAllTasks(UPDATE_PHONE_CALL_DETAILS);
+    }
+
+    private AssetManager getAssets() {
+        return getInstrumentation().getContext().getAssets();
+    }
+}
diff --git a/tests/src/com/android/dialer/DialerLaunchPerformance.java b/tests/src/com/android/dialer/DialerLaunchPerformance.java
new file mode 100644
index 0000000..cf64f94
--- /dev/null
+++ b/tests/src/com/android/dialer/DialerLaunchPerformance.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.Bundle;
+import android.test.LaunchPerformanceBase;
+
+/**
+ * Instrumentation class for Address Book launch performance testing.
+ */
+public class DialerLaunchPerformance extends LaunchPerformanceBase {
+
+    @Override
+    public void onCreate(Bundle arguments) {
+        mIntent.setAction(Intent.ACTION_MAIN);
+        mIntent.addCategory(Intent.CATEGORY_LAUNCHER);
+        mIntent.setComponent(new ComponentName("com.android.contacts",
+                "testcom.android.dialer.DialtactsActivity"));
+
+        start();
+    }
+
+    /**
+     * Calls LaunchApp and finish.
+     */
+    @Override
+    public void onStart() {
+        super.onStart();
+        LaunchApp();
+        finish(Activity.RESULT_OK, mResults);
+    }
+}
diff --git a/tests/src/com/android/dialer/PhoneCallDetailsHelperTest.java b/tests/src/com/android/dialer/PhoneCallDetailsHelperTest.java
new file mode 100644
index 0000000..9617644
--- /dev/null
+++ b/tests/src/com/android/dialer/PhoneCallDetailsHelperTest.java
@@ -0,0 +1,328 @@
+/*
+ * 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.dialer;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.provider.CallLog.Calls;
+import android.test.AndroidTestCase;
+import android.text.Html;
+import android.text.Spanned;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.dialer.calllog.CallTypeHelper;
+import com.android.dialer.calllog.PhoneNumberHelper;
+import com.android.dialer.calllog.TestPhoneNumberHelper;
+import com.android.dialer.util.LocaleTestUtils;
+import com.android.internal.telephony.CallerInfo;
+
+import java.util.GregorianCalendar;
+import java.util.Locale;
+
+/**
+ * Unit tests for {@link PhoneCallDetailsHelper}.
+ */
+public class PhoneCallDetailsHelperTest extends AndroidTestCase {
+    /** The number to be used to access the voicemail. */
+    private static final String TEST_VOICEMAIL_NUMBER = "125";
+    /** The date of the call log entry. */
+    private static final long TEST_DATE =
+        new GregorianCalendar(2011, 5, 3, 13, 0, 0).getTimeInMillis();
+    /** A test duration value for phone calls. */
+    private static final long TEST_DURATION = 62300;
+    /** The number of the caller/callee in the log entry. */
+    private static final String TEST_NUMBER = "14125555555";
+    /** The formatted version of {@link #TEST_NUMBER}. */
+    private static final String TEST_FORMATTED_NUMBER = "1-412-255-5555";
+    /** The country ISO name used in the tests. */
+    private static final String TEST_COUNTRY_ISO = "US";
+    /** The geocoded location used in the tests. */
+    private static final String TEST_GEOCODE = "United States";
+
+    /** The object under test. */
+    private PhoneCallDetailsHelper mHelper;
+    /** The views to fill. */
+    private PhoneCallDetailsViews mViews;
+    private TextView mNameView;
+    private PhoneNumberHelper mPhoneNumberHelper;
+    private LocaleTestUtils mLocaleTestUtils;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        Context context = getContext();
+        Resources resources = context.getResources();
+        CallTypeHelper callTypeHelper = new CallTypeHelper(resources);
+        mPhoneNumberHelper = new TestPhoneNumberHelper(resources, TEST_VOICEMAIL_NUMBER);
+        mHelper = new PhoneCallDetailsHelper(resources, callTypeHelper, mPhoneNumberHelper);
+        mHelper.setCurrentTimeForTest(
+                new GregorianCalendar(2011, 5, 4, 13, 0, 0).getTimeInMillis());
+        mViews = PhoneCallDetailsViews.createForTest(context);
+        mNameView = new TextView(context);
+        mLocaleTestUtils = new LocaleTestUtils(getContext());
+        mLocaleTestUtils.setLocale(Locale.US);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mLocaleTestUtils.restoreLocale();
+        mNameView = null;
+        mViews = null;
+        mHelper = null;
+        mPhoneNumberHelper = null;
+        super.tearDown();
+    }
+
+    public void testSetPhoneCallDetails_Unknown() {
+        setPhoneCallDetailsWithNumber(CallerInfo.UNKNOWN_NUMBER, CallerInfo.UNKNOWN_NUMBER);
+        assertNameEqualsResource(R.string.unknown);
+    }
+
+    public void testSetPhoneCallDetails_Private() {
+        setPhoneCallDetailsWithNumber(CallerInfo.PRIVATE_NUMBER, CallerInfo.PRIVATE_NUMBER);
+        assertNameEqualsResource(R.string.private_num);
+    }
+
+    public void testSetPhoneCallDetails_Payphone() {
+        setPhoneCallDetailsWithNumber(CallerInfo.PAYPHONE_NUMBER, CallerInfo.PAYPHONE_NUMBER);
+        assertNameEqualsResource(R.string.payphone);
+    }
+
+    public void testSetPhoneCallDetails_Voicemail() {
+        setPhoneCallDetailsWithNumber(TEST_VOICEMAIL_NUMBER, TEST_VOICEMAIL_NUMBER);
+        assertNameEqualsResource(R.string.voicemail);
+    }
+
+    public void testSetPhoneCallDetails_Normal() {
+        setPhoneCallDetailsWithNumber("14125551212", "1-412-555-1212");
+        assertEquals("yesterday", mViews.callTypeAndDate.getText().toString());
+        assertEqualsHtml("<font color='#33b5e5'><b>yesterday</b></font>",
+                mViews.callTypeAndDate.getText());
+    }
+
+    /** Asserts that a char sequence is actually a Spanned corresponding to the expected HTML. */
+    private void assertEqualsHtml(String expectedHtml, CharSequence actualText) {
+        // In order to contain HTML, the text should actually be a Spanned.
+        assertTrue(actualText instanceof Spanned);
+        Spanned actualSpanned = (Spanned) actualText;
+        // Convert from and to HTML to take care of alternative formatting of HTML.
+        assertEquals(Html.toHtml(Html.fromHtml(expectedHtml)), Html.toHtml(actualSpanned));
+
+    }
+
+    public void testSetPhoneCallDetails_Date() {
+        mHelper.setCurrentTimeForTest(
+                new GregorianCalendar(2011, 5, 3, 13, 0, 0).getTimeInMillis());
+
+        setPhoneCallDetailsWithDate(
+                new GregorianCalendar(2011, 5, 3, 13, 0, 0).getTimeInMillis());
+        assertDateEquals("0 mins ago");
+
+        setPhoneCallDetailsWithDate(
+                new GregorianCalendar(2011, 5, 3, 12, 0, 0).getTimeInMillis());
+        assertDateEquals("1 hour ago");
+
+        setPhoneCallDetailsWithDate(
+                new GregorianCalendar(2011, 5, 2, 13, 0, 0).getTimeInMillis());
+        assertDateEquals("yesterday");
+
+        setPhoneCallDetailsWithDate(
+                new GregorianCalendar(2011, 5, 1, 13, 0, 0).getTimeInMillis());
+        assertDateEquals("2 days ago");
+    }
+
+    public void testSetPhoneCallDetails_CallTypeIcons() {
+        setPhoneCallDetailsWithCallTypeIcons(Calls.INCOMING_TYPE);
+        assertCallTypeIconsEquals(Calls.INCOMING_TYPE);
+
+        setPhoneCallDetailsWithCallTypeIcons(Calls.OUTGOING_TYPE);
+        assertCallTypeIconsEquals(Calls.OUTGOING_TYPE);
+
+        setPhoneCallDetailsWithCallTypeIcons(Calls.MISSED_TYPE);
+        assertCallTypeIconsEquals(Calls.MISSED_TYPE);
+
+        setPhoneCallDetailsWithCallTypeIcons(Calls.VOICEMAIL_TYPE);
+        assertCallTypeIconsEquals(Calls.VOICEMAIL_TYPE);
+    }
+
+    public void testSetPhoneCallDetails_MultipleCallTypeIcons() {
+        setPhoneCallDetailsWithCallTypeIcons(Calls.INCOMING_TYPE, Calls.OUTGOING_TYPE);
+        assertCallTypeIconsEquals(Calls.INCOMING_TYPE, Calls.OUTGOING_TYPE);
+
+        setPhoneCallDetailsWithCallTypeIcons(Calls.MISSED_TYPE, Calls.MISSED_TYPE);
+        assertCallTypeIconsEquals(Calls.MISSED_TYPE, Calls.MISSED_TYPE);
+    }
+
+    public void testSetPhoneCallDetails_MultipleCallTypeIconsLastOneDropped() {
+        setPhoneCallDetailsWithCallTypeIcons(Calls.MISSED_TYPE, Calls.MISSED_TYPE,
+                Calls.INCOMING_TYPE, Calls.OUTGOING_TYPE);
+        assertCallTypeIconsEqualsPlusOverflow("(4)",
+                Calls.MISSED_TYPE, Calls.MISSED_TYPE, Calls.INCOMING_TYPE);
+    }
+
+    public void testSetPhoneCallDetails_Geocode() {
+        setPhoneCallDetailsWithNumberAndGeocode("+14125555555", "1-412-555-5555", "Pennsylvania");
+        assertNameEquals("1-412-555-5555");  // The phone number is shown as the name.
+        assertNumberEquals("Pennsylvania");  // The geocode is shown as the number.
+    }
+
+    public void testSetPhoneCallDetails_NoGeocode() {
+        setPhoneCallDetailsWithNumberAndGeocode("+14125555555", "1-412-555-5555", null);
+        assertNameEquals("1-412-555-5555");  // The phone number is shown as the name.
+        assertNumberEquals("-");  // The empty geocode is shown as the number.
+    }
+
+    public void testSetPhoneCallDetails_EmptyGeocode() {
+        setPhoneCallDetailsWithNumberAndGeocode("+14125555555", "1-412-555-5555", "");
+        assertNameEquals("1-412-555-5555");  // The phone number is shown as the name.
+        assertNumberEquals("-");  // The empty geocode is shown as the number.
+    }
+
+    public void testSetPhoneCallDetails_NoGeocodeForVoicemail() {
+        setPhoneCallDetailsWithNumberAndGeocode(TEST_VOICEMAIL_NUMBER, "", "United States");
+        assertNumberEquals("-");  // The empty geocode is shown as the number.
+    }
+
+    public void testSetPhoneCallDetails_Highlighted() {
+        setPhoneCallDetailsWithNumber(TEST_VOICEMAIL_NUMBER, "");
+    }
+
+    public void testSetCallDetailsHeader_NumberOnly() {
+        setCallDetailsHeaderWithNumberOnly(TEST_NUMBER);
+        assertEquals(View.VISIBLE, mNameView.getVisibility());
+        assertEquals("Add to contacts", mNameView.getText().toString());
+    }
+
+    public void testSetCallDetailsHeader_UnknownNumber() {
+        setCallDetailsHeaderWithNumberOnly(CallerInfo.UNKNOWN_NUMBER);
+        assertEquals(View.VISIBLE, mNameView.getVisibility());
+        assertEquals("Unknown", mNameView.getText().toString());
+    }
+
+    public void testSetCallDetailsHeader_PrivateNumber() {
+        setCallDetailsHeaderWithNumberOnly(CallerInfo.PRIVATE_NUMBER);
+        assertEquals(View.VISIBLE, mNameView.getVisibility());
+        assertEquals("Private number", mNameView.getText().toString());
+    }
+
+    public void testSetCallDetailsHeader_PayphoneNumber() {
+        setCallDetailsHeaderWithNumberOnly(CallerInfo.PAYPHONE_NUMBER);
+        assertEquals(View.VISIBLE, mNameView.getVisibility());
+        assertEquals("Pay phone", mNameView.getText().toString());
+    }
+
+    public void testSetCallDetailsHeader_VoicemailNumber() {
+        setCallDetailsHeaderWithNumberOnly(TEST_VOICEMAIL_NUMBER);
+        assertEquals(View.VISIBLE, mNameView.getVisibility());
+        assertEquals("Voicemail", mNameView.getText().toString());
+    }
+
+    public void testSetCallDetailsHeader() {
+        setCallDetailsHeader("John Doe");
+        assertEquals(View.VISIBLE, mNameView.getVisibility());
+        assertEquals("John Doe", mNameView.getText().toString());
+    }
+
+    /** Asserts that the name text field contains the value of the given string resource. */
+    private void assertNameEqualsResource(int resId) {
+        assertNameEquals(getContext().getString(resId));
+    }
+
+    /** Asserts that the name text field contains the given string value. */
+    private void assertNameEquals(String text) {
+        assertEquals(text, mViews.nameView.getText().toString());
+    }
+
+    /** Asserts that the number text field contains the given string value. */
+    private void assertNumberEquals(String text) {
+        assertEquals(text, mViews.numberView.getText().toString());
+    }
+
+    /** Asserts that the date text field contains the given string value. */
+    private void assertDateEquals(String text) {
+        assertEquals(text, mViews.callTypeAndDate.getText().toString());
+    }
+
+    /** Asserts that the call type contains the images with the given drawables. */
+    private void assertCallTypeIconsEquals(int... ids) {
+        assertEquals(ids.length, mViews.callTypeIcons.getCount());
+        for (int index = 0; index < ids.length; ++index) {
+            int id = ids[index];
+            assertEquals(id, mViews.callTypeIcons.getCallType(index));
+        }
+        assertEquals(View.VISIBLE, mViews.callTypeIcons.getVisibility());
+        assertEquals("yesterday", mViews.callTypeAndDate.getText().toString());
+    }
+
+    /**
+     * Asserts that the call type contains the images with the given drawables and shows the given
+     * text next to the icons.
+     */
+    private void assertCallTypeIconsEqualsPlusOverflow(String overflowText, int... ids) {
+        assertEquals(ids.length, mViews.callTypeIcons.getCount());
+        for (int index = 0; index < ids.length; ++index) {
+            int id = ids[index];
+            assertEquals(id, mViews.callTypeIcons.getCallType(index));
+        }
+        assertEquals(View.VISIBLE, mViews.callTypeIcons.getVisibility());
+        assertEquals(overflowText + " yesterday", mViews.callTypeAndDate.getText().toString());
+    }
+
+    /** Sets the phone call details with default values and the given number. */
+    private void setPhoneCallDetailsWithNumber(String number, String formattedNumber) {
+        setPhoneCallDetailsWithNumberAndGeocode(number, formattedNumber, TEST_GEOCODE);
+    }
+
+    /** Sets the phone call details with default values and the given number. */
+    private void setPhoneCallDetailsWithNumberAndGeocode(String number, String formattedNumber,
+            String geocodedLocation) {
+        mHelper.setPhoneCallDetails(mViews,
+                new PhoneCallDetails(number, formattedNumber, TEST_COUNTRY_ISO, geocodedLocation,
+                        new int[]{ Calls.VOICEMAIL_TYPE }, TEST_DATE, TEST_DURATION),
+                true);
+    }
+
+    /** Sets the phone call details with default values and the given date. */
+    private void setPhoneCallDetailsWithDate(long date) {
+        mHelper.setPhoneCallDetails(mViews,
+                new PhoneCallDetails(TEST_NUMBER, TEST_FORMATTED_NUMBER, TEST_COUNTRY_ISO,
+                        TEST_GEOCODE, new int[]{ Calls.INCOMING_TYPE }, date, TEST_DURATION),
+                false);
+    }
+
+    /** Sets the phone call details with default values and the given call types using icons. */
+    private void setPhoneCallDetailsWithCallTypeIcons(int... callTypes) {
+        mHelper.setPhoneCallDetails(mViews,
+                new PhoneCallDetails(TEST_NUMBER, TEST_FORMATTED_NUMBER, TEST_COUNTRY_ISO,
+                        TEST_GEOCODE, callTypes, TEST_DATE, TEST_DURATION),
+                false);
+    }
+
+    private void setCallDetailsHeaderWithNumberOnly(String number) {
+        mHelper.setCallDetailsHeader(mNameView,
+                new PhoneCallDetails(number, TEST_FORMATTED_NUMBER, TEST_COUNTRY_ISO,
+                        TEST_GEOCODE, new int[]{ Calls.INCOMING_TYPE }, TEST_DATE, TEST_DURATION));
+    }
+
+    private void setCallDetailsHeader(String name) {
+        mHelper.setCallDetailsHeader(mNameView,
+                new PhoneCallDetails(TEST_NUMBER, TEST_FORMATTED_NUMBER, TEST_COUNTRY_ISO,
+                        TEST_GEOCODE, new int[]{ Calls.INCOMING_TYPE }, TEST_DATE, TEST_DURATION,
+                        name, 0, "", null, null));
+    }
+}
diff --git a/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java b/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
new file mode 100644
index 0000000..6ec3e76
--- /dev/null
+++ b/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import android.content.Context;
+import android.database.MatrixCursor;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.View;
+
+import com.google.common.collect.Lists;
+
+import java.util.List;
+
+/**
+ * Unit tests for {@link CallLogAdapter}.
+ */
+@SmallTest
+public class CallLogAdapterTest extends AndroidTestCase {
+    private static final String TEST_NUMBER = "12345678";
+    private static final String TEST_NAME = "name";
+    private static final String TEST_NUMBER_LABEL = "label";
+    private static final int TEST_NUMBER_TYPE = 1;
+    private static final String TEST_COUNTRY_ISO = "US";
+
+    /** The object under test. */
+    private TestCallLogAdapter mAdapter;
+
+    private MatrixCursor mCursor;
+    private View mView;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        // Use a call fetcher that does not do anything.
+        CallLogAdapter.CallFetcher fakeCallFetcher = new CallLogAdapter.CallFetcher() {
+            @Override
+            public void fetchCalls() {}
+        };
+
+        ContactInfoHelper fakeContactInfoHelper =
+                new ContactInfoHelper(getContext(), TEST_COUNTRY_ISO) {
+                    @Override
+                    public ContactInfo lookupNumber(String number, String countryIso) {
+                        ContactInfo info = new ContactInfo();
+                        info.number = number;
+                        info.formattedNumber = number;
+                        return info;
+                    }
+                };
+
+        mAdapter = new TestCallLogAdapter(getContext(), fakeCallFetcher, fakeContactInfoHelper);
+        // The cursor used in the tests to store the entries to display.
+        mCursor = new MatrixCursor(CallLogQuery.EXTENDED_PROJECTION);
+        mCursor.moveToFirst();
+        // The views into which to store the data.
+        mView = new View(getContext());
+        mView.setTag(CallLogListItemViews.createForTest(getContext()));
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mAdapter = null;
+        mCursor = null;
+        mView = null;
+        super.tearDown();
+    }
+
+    public void testBindView_NoCallLogCacheNorMemoryCache_EnqueueRequest() {
+        mCursor.addRow(createCallLogEntry());
+
+        // Bind the views of a single row.
+        mAdapter.bindStandAloneView(mView, getContext(), mCursor);
+
+        // There is one request for contact details.
+        assertEquals(1, mAdapter.requests.size());
+
+        TestCallLogAdapter.Request request = mAdapter.requests.get(0);
+        // It is for the number we need to show.
+        assertEquals(TEST_NUMBER, request.number);
+        // It has the right country.
+        assertEquals(TEST_COUNTRY_ISO, request.countryIso);
+        // Since there is nothing in the cache, it is an immediate request.
+        assertTrue("should be immediate", request.immediate);
+    }
+
+    public void testBindView_CallLogCacheButNoMemoryCache_EnqueueRequest() {
+        mCursor.addRow(createCallLogEntryWithCachedValues());
+
+        // Bind the views of a single row.
+        mAdapter.bindStandAloneView(mView, getContext(), mCursor);
+
+        // There is one request for contact details.
+        assertEquals(1, mAdapter.requests.size());
+
+        TestCallLogAdapter.Request request = mAdapter.requests.get(0);
+        // The values passed to the request, match the ones in the call log cache.
+        assertEquals(TEST_NAME, request.callLogInfo.name);
+        assertEquals(1, request.callLogInfo.type);
+        assertEquals(TEST_NUMBER_LABEL, request.callLogInfo.label);
+    }
+
+
+    public void testBindView_NoCallLogButMemoryCache_EnqueueRequest() {
+        mCursor.addRow(createCallLogEntry());
+        mAdapter.injectContactInfoForTest(TEST_NUMBER, TEST_COUNTRY_ISO, createContactInfo());
+
+        // Bind the views of a single row.
+        mAdapter.bindStandAloneView(mView, getContext(), mCursor);
+
+        // There is one request for contact details.
+        assertEquals(1, mAdapter.requests.size());
+
+        TestCallLogAdapter.Request request = mAdapter.requests.get(0);
+        // Since there is something in the cache, it is not an immediate request.
+        assertFalse("should not be immediate", request.immediate);
+    }
+
+    public void testBindView_BothCallLogAndMemoryCache_NoEnqueueRequest() {
+        mCursor.addRow(createCallLogEntryWithCachedValues());
+        mAdapter.injectContactInfoForTest(TEST_NUMBER, TEST_COUNTRY_ISO, createContactInfo());
+
+        // Bind the views of a single row.
+        mAdapter.bindStandAloneView(mView, getContext(), mCursor);
+
+        // Cache and call log are up-to-date: no need to request update.
+        assertEquals(0, mAdapter.requests.size());
+    }
+
+    public void testBindView_MismatchBetwenCallLogAndMemoryCache_EnqueueRequest() {
+        mCursor.addRow(createCallLogEntryWithCachedValues());
+
+        // Contact info contains a different name.
+        ContactInfo info = createContactInfo();
+        info.name = "new name";
+        mAdapter.injectContactInfoForTest(TEST_NUMBER, TEST_COUNTRY_ISO, info);
+
+        // Bind the views of a single row.
+        mAdapter.bindStandAloneView(mView, getContext(), mCursor);
+
+        // There is one request for contact details.
+        assertEquals(1, mAdapter.requests.size());
+
+        TestCallLogAdapter.Request request = mAdapter.requests.get(0);
+        // Since there is something in the cache, it is not an immediate request.
+        assertFalse("should not be immediate", request.immediate);
+    }
+
+    /** Returns a contact info with default values. */
+    private ContactInfo createContactInfo() {
+        ContactInfo info = new ContactInfo();
+        info.number = TEST_NUMBER;
+        info.name = TEST_NAME;
+        info.type = TEST_NUMBER_TYPE;
+        info.label = TEST_NUMBER_LABEL;
+        return info;
+    }
+
+    /** Returns a call log entry without cached values. */
+    private Object[] createCallLogEntry() {
+        Object[] values = CallLogQueryTestUtils.createTestExtendedValues();
+        values[CallLogQuery.NUMBER] = TEST_NUMBER;
+        values[CallLogQuery.COUNTRY_ISO] = TEST_COUNTRY_ISO;
+        return values;
+    }
+
+    /** Returns a call log entry with a cached values. */
+    private Object[] createCallLogEntryWithCachedValues() {
+        Object[] values = createCallLogEntry();
+        values[CallLogQuery.CACHED_NAME] = TEST_NAME;
+        values[CallLogQuery.CACHED_NUMBER_TYPE] = TEST_NUMBER_TYPE;
+        values[CallLogQuery.CACHED_NUMBER_LABEL] = TEST_NUMBER_LABEL;
+        return values;
+    }
+
+    /**
+     * Subclass of {@link CallLogAdapter} used in tests to intercept certain calls.
+     */
+    // TODO: This would be better done by splitting the contact lookup into a collaborator class
+    // instead.
+    private static final class TestCallLogAdapter extends CallLogAdapter {
+        public static class Request {
+            public final String number;
+            public final String countryIso;
+            public final ContactInfo callLogInfo;
+            public final boolean immediate;
+
+            public Request(String number, String countryIso, ContactInfo callLogInfo,
+                    boolean immediate) {
+                this.number = number;
+                this.countryIso = countryIso;
+                this.callLogInfo = callLogInfo;
+                this.immediate = immediate;
+            }
+        }
+
+        public final List<Request> requests = Lists.newArrayList();
+
+        public TestCallLogAdapter(Context context, CallFetcher callFetcher,
+                ContactInfoHelper contactInfoHelper) {
+            super(context, callFetcher, contactInfoHelper);
+        }
+
+        @Override
+        void enqueueRequest(String number, String countryIso, ContactInfo callLogInfo,
+                boolean immediate) {
+            requests.add(new Request(number, countryIso, callLogInfo, immediate));
+        }
+    }
+}
diff --git a/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java b/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java
new file mode 100644
index 0000000..f453432
--- /dev/null
+++ b/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java
@@ -0,0 +1,632 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.calllog;
+
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.ComponentName;
+import android.content.ContentUris;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.MatrixCursor;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.net.Uri;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.VoicemailContract;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.dialer.CallDetailActivity;
+import com.android.contacts.R;
+import com.android.contacts.common.test.FragmentTestActivity;
+import com.android.internal.telephony.CallerInfo;
+
+import java.util.Date;
+import java.util.Formatter;
+import java.util.HashMap;
+import java.util.Random;
+
+/**
+ * Tests for the contact call list activity.
+ *
+ * Running all tests:
+ *
+ *   runtest contacts
+ * or
+ *   adb shell am instrument \
+ *     -w com.android.contacts.tests/android.test.InstrumentationTestRunner
+ */
+@LargeTest
+public class CallLogFragmentTest extends ActivityInstrumentationTestCase2<FragmentTestActivity> {
+    private static final int RAND_DURATION = -1;
+    private static final long NOW = -1L;
+
+    /** A test value for the URI of a contact. */
+    private static final Uri TEST_LOOKUP_URI = Uri.parse("content://contacts/2");
+    /** A test value for the country ISO of the phone number in the call log. */
+    private static final String TEST_COUNTRY_ISO = "US";
+    /** A phone number to be used in tests. */
+    private static final String TEST_NUMBER = "12125551000";
+    /** The formatted version of {@link #TEST_NUMBER}. */
+    private static final String TEST_FORMATTED_NUMBER = "1 212-555-1000";
+
+    /** The activity in which we are hosting the fragment. */
+    private FragmentTestActivity mActivity;
+    private CallLogFragment mFragment;
+    private FrameLayout mParentView;
+    /**
+     * The adapter used by the fragment to build the rows in the call log. We use it with our own in
+     * memory database.
+     */
+    private CallLogAdapter mAdapter;
+    private String mVoicemail;
+
+    // In memory array to hold the rows corresponding to the 'calls' table.
+    private MatrixCursor mCursor;
+    private int mIndex;  // Of the next row.
+
+    private Random mRnd;
+
+    // References to the icons bitmaps used to build the list are stored in a
+    // map mIcons. The keys to retrieve the icons are:
+    // Calls.INCOMING_TYPE, Calls.OUTGOING_TYPE and Calls.MISSED_TYPE.
+    private HashMap<Integer, Bitmap> mCallTypeIcons;
+
+    // An item in the call list. All the methods performing checks use it.
+    private CallLogListItemViews mItem;
+    // The list of views representing the data in the DB. View are in
+    // reverse order compare to the DB.
+    private View[] mList;
+
+    public CallLogFragmentTest() {
+        super("com.android.dialer", FragmentTestActivity.class);
+        mIndex = 1;
+        mRnd = new Random();
+    }
+
+    @Override
+    public void setUp() {
+        mActivity = getActivity();
+        // Needed by the CallLogFragment.
+        mActivity.setTheme(R.style.DialtactsTheme);
+
+        // Create the fragment and load it into the activity.
+        mFragment = new CallLogFragment();
+        FragmentManager fragmentManager = mActivity.getFragmentManager();
+        FragmentTransaction transaction = fragmentManager.beginTransaction();
+        transaction.add(FragmentTestActivity.LAYOUT_ID, mFragment);
+        transaction.commit();
+        // Wait for the fragment to be loaded.
+        getInstrumentation().waitForIdleSync();
+
+        mVoicemail = TelephonyManager.getDefault().getVoiceMailNumber();
+        mAdapter = mFragment.getAdapter();
+        // Do not process requests for details during tests. This would start a background thread,
+        // which makes the tests flaky.
+        mAdapter.disableRequestProcessingForTest();
+        mAdapter.stopRequestProcessing();
+        mParentView = new FrameLayout(mActivity);
+        mCursor = new MatrixCursor(CallLogQuery.EXTENDED_PROJECTION);
+        buildIconMap();
+    }
+
+    /**
+     * Checks that the call icon is not visible for private and
+     * unknown numbers.
+     * Use 2 passes, one where new views are created and one where
+     * half of the total views are updated and the other half created.
+     */
+    @MediumTest
+    public void testCallViewIsNotVisibleForPrivateAndUnknownNumbers() {
+        final int SIZE = 100;
+        mList = new View[SIZE];
+
+        // Insert the first batch of entries.
+        mCursor.moveToFirst();
+        insertRandomEntries(SIZE / 2);
+        int startOfSecondBatch = mCursor.getPosition();
+
+        buildViewListFromDb();
+        checkCallStatus();
+
+        // Append the rest of the entries. We keep the first set of
+        // views around so they get updated and not built from
+        // scratch, this exposes some bugs that are not there when the
+        // call log is launched for the 1st time but show up when the
+        // call log gets updated afterwards.
+        mCursor.move(startOfSecondBatch);
+        insertRandomEntries(SIZE / 2);
+
+        buildViewListFromDb();
+        checkCallStatus();
+    }
+
+    @MediumTest
+    public void testCallAndGroupViews_GroupView() {
+        mCursor.moveToFirst();
+        insert(CallerInfo.PRIVATE_NUMBER, NOW, 0, Calls.INCOMING_TYPE);
+        insert(CallerInfo.PRIVATE_NUMBER, NOW, 0, Calls.INCOMING_TYPE);
+        insert(CallerInfo.PRIVATE_NUMBER, NOW, 0, Calls.INCOMING_TYPE);
+        View view = mAdapter.newGroupView(getActivity(), mParentView);
+        mAdapter.bindGroupView(view, getActivity(), mCursor, 3, false);
+        assertNotNull(view.findViewById(R.id.secondary_action_icon));
+    }
+
+    @MediumTest
+    public void testCallAndGroupViews_StandAloneView() {
+        mCursor.moveToFirst();
+        insert(CallerInfo.PRIVATE_NUMBER, NOW, 0, Calls.INCOMING_TYPE);
+        View view = mAdapter.newStandAloneView(getActivity(), mParentView);
+        mAdapter.bindStandAloneView(view, getActivity(), mCursor);
+        assertNotNull(view.findViewById(R.id.secondary_action_icon));
+    }
+
+    @MediumTest
+    public void testCallAndGroupViews_ChildView() {
+        mCursor.moveToFirst();
+        insert(CallerInfo.PRIVATE_NUMBER, NOW, 0, Calls.INCOMING_TYPE);
+        View view = mAdapter.newChildView(getActivity(), mParentView);
+        mAdapter.bindChildView(view, getActivity(), mCursor);
+        assertNotNull(view.findViewById(R.id.secondary_action_icon));
+    }
+
+    @MediumTest
+    public void testBindView_NumberOnlyNoCache() {
+        mCursor.moveToFirst();
+        insert(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE);
+        View view = mAdapter.newStandAloneView(getActivity(), mParentView);
+        mAdapter.bindStandAloneView(view, getActivity(), mCursor);
+
+        CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+        assertNameIs(views, TEST_NUMBER);
+    }
+
+    @MediumTest
+    public void testBindView_NumberOnlyDbCachedFormattedNumber() {
+        mCursor.moveToFirst();
+        Object[] values = getValuesToInsert(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE);
+        values[CallLogQuery.CACHED_FORMATTED_NUMBER] = TEST_FORMATTED_NUMBER;
+        insertValues(values);
+        View view = mAdapter.newStandAloneView(getActivity(), mParentView);
+        mAdapter.bindStandAloneView(view, getActivity(), mCursor);
+
+        CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+        assertNameIs(views, TEST_FORMATTED_NUMBER);
+    }
+
+    @MediumTest
+    public void testBindView_WithCachedName() {
+        mCursor.moveToFirst();
+        insertWithCachedValues(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE,
+                "John Doe", Phone.TYPE_HOME, "");
+        View view = mAdapter.newStandAloneView(getActivity(), mParentView);
+        mAdapter.bindStandAloneView(view, getActivity(), mCursor);
+
+        CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+        assertNameIs(views, "John Doe");
+        assertNumberAndLabelAre(views, TEST_FORMATTED_NUMBER, getTypeLabel(Phone.TYPE_HOME));
+    }
+
+    @MediumTest
+    public void testBindView_UriNumber() {
+        mCursor.moveToFirst();
+        insertWithCachedValues("sip:johndoe@gmail.com", NOW, 0, Calls.INCOMING_TYPE,
+                "John Doe", Phone.TYPE_HOME, "");
+        View view = mAdapter.newStandAloneView(getActivity(), mParentView);
+        mAdapter.bindStandAloneView(view, getActivity(), mCursor);
+
+        CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+        assertNameIs(views, "John Doe");
+        assertNumberAndLabelAre(views, "sip:johndoe@gmail.com", null);
+    }
+
+    @MediumTest
+    public void testBindView_HomeLabel() {
+        mCursor.moveToFirst();
+        insertWithCachedValues(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE,
+                "John Doe", Phone.TYPE_HOME, "");
+        View view = mAdapter.newStandAloneView(getActivity(), mParentView);
+        mAdapter.bindStandAloneView(view, getActivity(), mCursor);
+
+        CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+        assertNameIs(views, "John Doe");
+        assertNumberAndLabelAre(views, TEST_FORMATTED_NUMBER, getTypeLabel(Phone.TYPE_HOME));
+    }
+
+    @MediumTest
+    public void testBindView_WorkLabel() {
+        mCursor.moveToFirst();
+        insertWithCachedValues(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE,
+                "John Doe", Phone.TYPE_WORK, "");
+        View view = mAdapter.newStandAloneView(getActivity(), mParentView);
+        mAdapter.bindStandAloneView(view, getActivity(), mCursor);
+
+        CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+        assertNameIs(views, "John Doe");
+        assertNumberAndLabelAre(views, TEST_FORMATTED_NUMBER, getTypeLabel(Phone.TYPE_WORK));
+    }
+
+    @MediumTest
+    public void testBindView_CustomLabel() {
+        mCursor.moveToFirst();
+        String numberLabel = "My label";
+        insertWithCachedValues(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE,
+                "John Doe", Phone.TYPE_CUSTOM, numberLabel);
+        View view = mAdapter.newStandAloneView(getActivity(), mParentView);
+        mAdapter.bindStandAloneView(view, getActivity(), mCursor);
+
+        CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+        assertNameIs(views, "John Doe");
+        assertNumberAndLabelAre(views, TEST_FORMATTED_NUMBER, numberLabel);
+    }
+
+    @MediumTest
+    public void testBindView_WithQuickContactBadge() {
+        mCursor.moveToFirst();
+        insertWithCachedValues(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE,
+                "John Doe", Phone.TYPE_HOME, "");
+        View view = mAdapter.newStandAloneView(getActivity(), mParentView);
+        mAdapter.bindStandAloneView(view, getActivity(), mCursor);
+
+        CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+        assertTrue(views.quickContactView.isEnabled());
+    }
+
+    @MediumTest
+    public void testBindView_WithoutQuickContactBadge() {
+        mCursor.moveToFirst();
+        insert(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE);
+        View view = mAdapter.newStandAloneView(getActivity(), mParentView);
+        mAdapter.bindStandAloneView(view, getActivity(), mCursor);
+
+        CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+        assertFalse(views.quickContactView.isEnabled());
+    }
+
+    @MediumTest
+    public void testBindView_CallButton() {
+        mCursor.moveToFirst();
+        insert(TEST_NUMBER, NOW, 0, Calls.INCOMING_TYPE);
+        View view = mAdapter.newStandAloneView(getActivity(), mParentView);
+        mAdapter.bindStandAloneView(view, getActivity(), mCursor);
+
+        CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+        IntentProvider intentProvider = (IntentProvider) views.secondaryActionView.getTag();
+        Intent intent = intentProvider.getIntent(mActivity);
+        // Starts a call.
+        assertEquals(Intent.ACTION_CALL_PRIVILEGED, intent.getAction());
+        // To the entry's number.
+        assertEquals(Uri.parse("tel:" + TEST_NUMBER), intent.getData());
+    }
+
+    @MediumTest
+    public void testBindView_PlayButton() {
+        mCursor.moveToFirst();
+        insertVoicemail(TEST_NUMBER, NOW, 0);
+        View view = mAdapter.newStandAloneView(getActivity(), mParentView);
+        mAdapter.bindStandAloneView(view, getActivity(), mCursor);
+
+        CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+        IntentProvider intentProvider = (IntentProvider) views.secondaryActionView.getTag();
+        Intent intent = intentProvider.getIntent(mActivity);
+        // Starts the call detail activity.
+        assertEquals(new ComponentName(mActivity, CallDetailActivity.class),
+                intent.getComponent());
+        // With the given entry.
+        assertEquals(ContentUris.withAppendedId(Calls.CONTENT_URI_WITH_VOICEMAIL, 1),
+                intent.getData());
+        // With the URI of the voicemail.
+        assertEquals(
+                ContentUris.withAppendedId(VoicemailContract.Voicemails.CONTENT_URI, 1),
+                intent.getParcelableExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI));
+        // And starts playback.
+        assertTrue(
+                intent.getBooleanExtra(CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK, false));
+    }
+
+    /** Returns the label associated with a given phone type. */
+    private CharSequence getTypeLabel(int phoneType) {
+        return Phone.getTypeLabel(getActivity().getResources(), phoneType, "");
+    }
+
+    //
+    // HELPERS to check conditions on the DB/views
+    //
+    /**
+     * Go over all the views in the list and check that the Call
+     * icon's visibility matches the nature of the number.
+     */
+    private void checkCallStatus() {
+        for (int i = 0; i < mList.length; i++) {
+            if (null == mList[i]) {
+                break;
+            }
+            mItem = (CallLogListItemViews) mList[i].getTag();
+            String number = getPhoneNumberForListEntry(i);
+            if (CallerInfo.PRIVATE_NUMBER.equals(number) ||
+                CallerInfo.UNKNOWN_NUMBER.equals(number)) {
+                assertFalse(View.VISIBLE == mItem.secondaryActionView.getVisibility());
+            } else {
+                assertEquals(View.VISIBLE, mItem.secondaryActionView.getVisibility());
+            }
+        }
+    }
+
+
+    //
+    // HELPERS to setup the tests.
+    //
+
+    /**
+     * Get the Bitmap from the icons in the contacts package.
+     */
+    private Bitmap getBitmap(String resName) {
+        Resources r = mActivity.getResources();
+        int resid = r.getIdentifier(resName, "drawable", "com.android.dialer");
+        BitmapDrawable d = (BitmapDrawable) r.getDrawable(resid);
+        assertNotNull(d);
+        return d.getBitmap();
+    }
+
+    /**
+     * Fetch all the icons we need in tests from the contacts app and store them in a map.
+     */
+    private void buildIconMap() {
+        mCallTypeIcons = new HashMap<Integer, Bitmap>(3);
+
+        mCallTypeIcons.put(Calls.INCOMING_TYPE, getBitmap("ic_call_incoming_holo_dark"));
+        mCallTypeIcons.put(Calls.MISSED_TYPE, getBitmap("ic_call_missed_holo_dark"));
+        mCallTypeIcons.put(Calls.OUTGOING_TYPE, getBitmap("ic_call_outgoing_holo_dark"));
+    }
+
+    //
+    // HELPERS to build/update the call entries (views) from the DB.
+    //
+
+    /**
+     * Read the DB and foreach call either update the existing view if
+     * one exists already otherwise create one.
+     * The list is build from a DESC view of the DB (last inserted entry is first).
+     */
+    private void buildViewListFromDb() {
+        int i = 0;
+        mCursor.moveToLast();
+        while(!mCursor.isBeforeFirst()) {
+            if (null == mList[i]) {
+                mList[i] = mAdapter.newStandAloneView(mActivity, mParentView);
+            }
+            mAdapter.bindStandAloneView(mList[i], mActivity, mCursor);
+            mCursor.moveToPrevious();
+            i++;
+        }
+    }
+
+    /** Returns the number associated with the given entry in {{@link #mList}. */
+    private String getPhoneNumberForListEntry(int index) {
+        // The entries are added backward, so count from the end of the cursor.
+        mCursor.moveToPosition(mCursor.getCount() - index - 1);
+        return mCursor.getString(CallLogQuery.NUMBER);
+    }
+
+    //
+    // HELPERS to insert numbers in the call log DB.
+    //
+
+    /**
+     * Insert a certain number of random numbers in the DB. Makes sure
+     * there is at least one private and one unknown number in the DB.
+     * @param num Of entries to be inserted.
+     */
+    private void insertRandomEntries(int num) {
+        if (num < 10) {
+            throw new IllegalArgumentException("num should be >= 10");
+        }
+        boolean privateOrUnknownOrVm[];
+        privateOrUnknownOrVm = insertRandomRange(0, num - 2);
+
+        if (privateOrUnknownOrVm[0] && privateOrUnknownOrVm[1]) {
+            insertRandomRange(num - 2, num);
+        } else {
+            insertPrivate(NOW, RAND_DURATION);
+            insertUnknown(NOW, RAND_DURATION);
+        }
+    }
+
+    /**
+     * Insert a new call entry in the test DB.
+     *
+     * It includes the values for the cached contact associated with the number.
+     *
+     * @param number The phone number. For unknown and private numbers,
+     *               use CallerInfo.UNKNOWN_NUMBER or CallerInfo.PRIVATE_NUMBER.
+     * @param date In millisec since epoch. Use NOW to use the current time.
+     * @param duration In seconds of the call. Use RAND_DURATION to pick a random one.
+     * @param type Either Call.OUTGOING_TYPE or Call.INCOMING_TYPE or Call.MISSED_TYPE.
+     * @param cachedName the name of the contact with this number
+     * @param cachedNumberType the type of the number, from the contact with this number
+     * @param cachedNumberLabel the label of the number, from the contact with this number
+     */
+    private void insertWithCachedValues(String number, long date, int duration, int type,
+            String cachedName, int cachedNumberType, String cachedNumberLabel) {
+        insert(number, date, duration, type);
+        ContactInfo contactInfo = new ContactInfo();
+        contactInfo.lookupUri = TEST_LOOKUP_URI;
+        contactInfo.name = cachedName;
+        contactInfo.type = cachedNumberType;
+        contactInfo.label = cachedNumberLabel;
+        String formattedNumber = PhoneNumberUtils.formatNumber(number, TEST_COUNTRY_ISO);
+        if (formattedNumber == null) {
+            formattedNumber = number;
+        }
+        contactInfo.formattedNumber = formattedNumber;
+        contactInfo.normalizedNumber = number;
+        contactInfo.photoId = 0;
+        mAdapter.injectContactInfoForTest(number, TEST_COUNTRY_ISO, contactInfo);
+    }
+
+    /**
+     * Insert a new call entry in the test DB.
+     * @param number The phone number. For unknown and private numbers,
+     *               use CallerInfo.UNKNOWN_NUMBER or CallerInfo.PRIVATE_NUMBER.
+     * @param date In millisec since epoch. Use NOW to use the current time.
+     * @param duration In seconds of the call. Use RAND_DURATION to pick a random one.
+     * @param type Either Call.OUTGOING_TYPE or Call.INCOMING_TYPE or Call.MISSED_TYPE.
+     */
+    private void insert(String number, long date, int duration, int type) {
+        insertValues(getValuesToInsert(number, date, duration, type));
+    }
+
+    /** Inserts the given values in the cursor. */
+    private void insertValues(Object[] values) {
+        mCursor.addRow(values);
+        ++mIndex;
+    }
+
+    /**
+     * Returns the values for a new call entry.
+     *
+     * @param number The phone number. For unknown and private numbers,
+     *               use CallerInfo.UNKNOWN_NUMBER or CallerInfo.PRIVATE_NUMBER.
+     * @param date In millisec since epoch. Use NOW to use the current time.
+     * @param duration In seconds of the call. Use RAND_DURATION to pick a random one.
+     * @param type Either Call.OUTGOING_TYPE or Call.INCOMING_TYPE or Call.MISSED_TYPE.
+     */
+    private Object[] getValuesToInsert(String number, long date, int duration, int type) {
+        Object[] values = CallLogQueryTestUtils.createTestExtendedValues();
+        values[CallLogQuery.ID] = mIndex;
+        values[CallLogQuery.NUMBER] = number;
+        values[CallLogQuery.DATE] = date == NOW ? new Date().getTime() : date;
+        values[CallLogQuery.DURATION] = duration < 0 ? mRnd.nextInt(10 * 60) : duration;
+        if (mVoicemail != null && mVoicemail.equals(number)) {
+            assertEquals(Calls.OUTGOING_TYPE, type);
+        }
+        values[CallLogQuery.CALL_TYPE] = type;
+        values[CallLogQuery.COUNTRY_ISO] = TEST_COUNTRY_ISO;
+        values[CallLogQuery.SECTION] = CallLogQuery.SECTION_OLD_ITEM;
+        return values;
+    }
+
+    /**
+     * Insert a new voicemail entry in the test DB.
+     * @param number The phone number. For unknown and private numbers,
+     *               use CallerInfo.UNKNOWN_NUMBER or CallerInfo.PRIVATE_NUMBER.
+     * @param date In millisec since epoch. Use NOW to use the current time.
+     * @param duration In seconds of the call. Use RAND_DURATION to pick a random one.
+     */
+    private void insertVoicemail(String number, long date, int duration) {
+        Object[] values = getValuesToInsert(number, date, duration, Calls.VOICEMAIL_TYPE);
+        // Must have the same index as the row.
+        values[CallLogQuery.VOICEMAIL_URI] =
+                ContentUris.withAppendedId(VoicemailContract.Voicemails.CONTENT_URI, mIndex);
+        insertValues(values);
+    }
+
+    /**
+     * Insert a new private call entry in the test DB.
+     * @param date In millisec since epoch. Use NOW to use the current time.
+     * @param duration In seconds of the call. Use RAND_DURATION to pick a random one.
+     */
+    private void insertPrivate(long date, int duration) {
+        insert(CallerInfo.PRIVATE_NUMBER, date, duration, Calls.INCOMING_TYPE);
+    }
+
+    /**
+     * Insert a new unknown call entry in the test DB.
+     * @param date In millisec since epoch. Use NOW to use the current time.
+     * @param duration In seconds of the call. Use RAND_DURATION to pick a random one.
+     */
+    private void insertUnknown(long date, int duration) {
+        insert(CallerInfo.UNKNOWN_NUMBER, date, duration, Calls.INCOMING_TYPE);
+    }
+
+    /**
+     * Insert a new call to voicemail entry in the test DB.
+     * @param date In millisec since epoch. Use NOW to use the current time.
+     * @param duration In seconds of the call. Use RAND_DURATION to pick a random one.
+     */
+    private void insertCalltoVoicemail(long date, int duration) {
+        // mVoicemail may be null
+        if (mVoicemail != null) {
+            insert(mVoicemail, date, duration, Calls.OUTGOING_TYPE);
+        }
+    }
+
+    /**
+     * Insert a range [start, end) of random numbers in the DB. For
+     * each row, there is a 1/10 probability that the number will be
+     * marked as PRIVATE or UNKNOWN or VOICEMAIL. For regular numbers, a number is
+     * inserted, its last 4 digits will be the number of the iteration
+     * in the range.
+     * @param start Of the range.
+     * @param end Of the range (excluded).
+     * @return An array with 2 booleans [0 = private number, 1 =
+     * unknown number, 2 = voicemail] to indicate if at least one
+     * private or unknown or voicemail number has been inserted. Since
+     * the numbers are random some tests may want to enforce the
+     * insertion of such numbers.
+     */
+    // TODO: Should insert numbers with contact entries too.
+    private boolean[] insertRandomRange(int start, int end) {
+        boolean[] privateOrUnknownOrVm = new boolean[] {false, false, false};
+
+        for (int i = start; i < end; i++ ) {
+            int type = mRnd.nextInt(10);
+
+            if (0 == type) {
+                insertPrivate(NOW, RAND_DURATION);
+                privateOrUnknownOrVm[0] = true;
+            } else if (1 == type) {
+                insertUnknown(NOW, RAND_DURATION);
+                privateOrUnknownOrVm[1] = true;
+            } else if (2 == type) {
+                insertCalltoVoicemail(NOW, RAND_DURATION);
+                privateOrUnknownOrVm[2] = true;
+            } else {
+                int inout = mRnd.nextBoolean() ? Calls.OUTGOING_TYPE :  Calls.INCOMING_TYPE;
+                String number = new Formatter().format("1800123%04d", i).toString();
+                insert(number, NOW, RAND_DURATION, inout);
+            }
+        }
+        return privateOrUnknownOrVm;
+    }
+
+    /** Asserts that the name text view is shown and contains the given text. */
+    private void assertNameIs(CallLogListItemViews views, String name) {
+        assertEquals(View.VISIBLE, views.phoneCallDetailsViews.nameView.getVisibility());
+        assertEquals(name, views.phoneCallDetailsViews.nameView.getText());
+    }
+
+    /** Asserts that the number and label text view contains the given text. */
+    private void assertNumberAndLabelAre(CallLogListItemViews views, CharSequence number,
+            CharSequence label) {
+        assertEquals(View.VISIBLE, views.phoneCallDetailsViews.numberView.getVisibility());
+        assertEquals(number, views.phoneCallDetailsViews.numberView.getText().toString());
+
+        assertEquals(label == null ? View.GONE : View.VISIBLE,
+                views.phoneCallDetailsViews.labelView.getVisibility());
+        if (label != null) {
+            assertEquals(label, views.phoneCallDetailsViews.labelView.getText().toString());
+        }
+    }
+}
diff --git a/tests/src/com/android/dialer/calllog/CallLogGroupBuilderTest.java b/tests/src/com/android/dialer/calllog/CallLogGroupBuilderTest.java
new file mode 100644
index 0000000..6c20afe
--- /dev/null
+++ b/tests/src/com/android/dialer/calllog/CallLogGroupBuilderTest.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import static com.google.common.collect.Lists.newArrayList;
+
+import android.database.MatrixCursor;
+import android.provider.CallLog.Calls;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.util.List;
+
+/**
+ * Unit tests for {@link CallLogGroupBuilder}
+ */
+@SmallTest
+public class CallLogGroupBuilderTest extends AndroidTestCase {
+    /** A phone number for testing. */
+    private static final String TEST_NUMBER1 = "14125551234";
+    /** A phone number for testing. */
+    private static final String TEST_NUMBER2 = "14125555555";
+
+    /** The object under test. */
+    private CallLogGroupBuilder mBuilder;
+    /** Records the created groups. */
+    private FakeGroupCreator mFakeGroupCreator;
+    /** Cursor to store the values. */
+    private MatrixCursor mCursor;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mFakeGroupCreator = new FakeGroupCreator();
+        mBuilder = new CallLogGroupBuilder(mFakeGroupCreator);
+        createCursor();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mCursor = null;
+        mBuilder = null;
+        mFakeGroupCreator = null;
+        super.tearDown();
+    }
+
+    public void testAddGroups_NoCalls() {
+        mBuilder.addGroups(mCursor);
+        assertEquals(0, mFakeGroupCreator.groups.size());
+    }
+
+    public void testAddGroups_OneCall() {
+        addOldCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
+        mBuilder.addGroups(mCursor);
+        assertEquals(0, mFakeGroupCreator.groups.size());
+    }
+
+    public void testAddGroups_TwoCallsNotMatching() {
+        addOldCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
+        addOldCallLogEntry(TEST_NUMBER2, Calls.INCOMING_TYPE);
+        mBuilder.addGroups(mCursor);
+        assertEquals(0, mFakeGroupCreator.groups.size());
+    }
+
+    public void testAddGroups_ThreeCallsMatching() {
+        addOldCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
+        addOldCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
+        addOldCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
+        mBuilder.addGroups(mCursor);
+        assertEquals(1, mFakeGroupCreator.groups.size());
+        assertGroupIs(0, 3, false, mFakeGroupCreator.groups.get(0));
+    }
+
+    public void testAddGroups_MatchingIncomingAndOutgoing() {
+        addOldCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
+        addOldCallLogEntry(TEST_NUMBER1, Calls.OUTGOING_TYPE);
+        addOldCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
+        mBuilder.addGroups(mCursor);
+        assertEquals(1, mFakeGroupCreator.groups.size());
+        assertGroupIs(0, 3, false, mFakeGroupCreator.groups.get(0));
+    }
+
+    public void testAddGroups_HeaderSplitsGroups() {
+        addNewCallLogHeader();
+        addNewCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
+        addNewCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
+        addOldCallLogHeader();
+        addOldCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
+        addOldCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
+        mBuilder.addGroups(mCursor);
+        assertEquals(2, mFakeGroupCreator.groups.size());
+        assertGroupIs(1, 2, false, mFakeGroupCreator.groups.get(0));
+        assertGroupIs(4, 2, false, mFakeGroupCreator.groups.get(1));
+    }
+
+    public void testAddGroups_Voicemail() {
+        // Does not group with other types of calls, include voicemail themselves.
+        assertCallsAreNotGrouped(Calls.VOICEMAIL_TYPE, Calls.MISSED_TYPE);
+        //assertCallsAreNotGrouped(Calls.VOICEMAIL_TYPE, Calls.MISSED_TYPE, Calls.MISSED_TYPE);
+        assertCallsAreNotGrouped(Calls.VOICEMAIL_TYPE, Calls.VOICEMAIL_TYPE);
+        assertCallsAreNotGrouped(Calls.VOICEMAIL_TYPE, Calls.INCOMING_TYPE);
+        assertCallsAreNotGrouped(Calls.VOICEMAIL_TYPE, Calls.OUTGOING_TYPE);
+    }
+
+    public void testAddGroups_Missed() {
+        // Groups with one or more missed calls.
+        assertCallsAreGrouped(Calls.MISSED_TYPE, Calls.MISSED_TYPE);
+        assertCallsAreGrouped(Calls.MISSED_TYPE, Calls.MISSED_TYPE, Calls.MISSED_TYPE);
+        // Does not group with other types of calls.
+        assertCallsAreNotGrouped(Calls.MISSED_TYPE, Calls.VOICEMAIL_TYPE);
+        assertCallsAreGrouped(Calls.MISSED_TYPE, Calls.INCOMING_TYPE);
+        assertCallsAreGrouped(Calls.MISSED_TYPE, Calls.OUTGOING_TYPE);
+    }
+
+    public void testAddGroups_Incoming() {
+        // Groups with one or more incoming or outgoing.
+        assertCallsAreGrouped(Calls.INCOMING_TYPE, Calls.INCOMING_TYPE);
+        assertCallsAreGrouped(Calls.INCOMING_TYPE, Calls.OUTGOING_TYPE);
+        assertCallsAreGrouped(Calls.INCOMING_TYPE, Calls.INCOMING_TYPE, Calls.OUTGOING_TYPE);
+        assertCallsAreGrouped(Calls.INCOMING_TYPE, Calls.OUTGOING_TYPE, Calls.INCOMING_TYPE);
+        assertCallsAreGrouped(Calls.INCOMING_TYPE, Calls.MISSED_TYPE);
+        // Does not group with voicemail and missed calls.
+        assertCallsAreNotGrouped(Calls.INCOMING_TYPE, Calls.VOICEMAIL_TYPE);
+    }
+
+    public void testAddGroups_Outgoing() {
+        // Groups with one or more incoming or outgoing.
+        assertCallsAreGrouped(Calls.OUTGOING_TYPE, Calls.INCOMING_TYPE);
+        assertCallsAreGrouped(Calls.OUTGOING_TYPE, Calls.OUTGOING_TYPE);
+        assertCallsAreGrouped(Calls.OUTGOING_TYPE, Calls.INCOMING_TYPE, Calls.OUTGOING_TYPE);
+        assertCallsAreGrouped(Calls.OUTGOING_TYPE, Calls.OUTGOING_TYPE, Calls.INCOMING_TYPE);
+        assertCallsAreGrouped(Calls.INCOMING_TYPE, Calls.MISSED_TYPE);
+        // Does not group with voicemail and missed calls.
+        assertCallsAreNotGrouped(Calls.INCOMING_TYPE, Calls.VOICEMAIL_TYPE);
+    }
+
+    public void testAddGroups_Mixed() {
+        addMultipleOldCallLogEntries(TEST_NUMBER1,
+                Calls.VOICEMAIL_TYPE,  // Stand-alone
+                Calls.INCOMING_TYPE,  // Group 1: 1-4
+                Calls.OUTGOING_TYPE,
+                Calls.MISSED_TYPE,
+                Calls.MISSED_TYPE,
+                Calls.VOICEMAIL_TYPE,  // Stand-alone
+                Calls.INCOMING_TYPE,  // Stand-alone
+                Calls.VOICEMAIL_TYPE,  // Stand-alone
+                Calls.MISSED_TYPE, // Group 2: 8-10
+                Calls.MISSED_TYPE,
+                Calls.OUTGOING_TYPE);
+        mBuilder.addGroups(mCursor);
+        assertEquals(2, mFakeGroupCreator.groups.size());
+        assertGroupIs(1, 4, false, mFakeGroupCreator.groups.get(0));
+        assertGroupIs(8, 3, false, mFakeGroupCreator.groups.get(1));
+    }
+
+    public void testEqualPhoneNumbers() {
+        // Identical.
+        assertTrue(mBuilder.equalNumbers("6505555555", "6505555555"));
+        assertTrue(mBuilder.equalNumbers("650 555 5555", "650 555 5555"));
+        // Formatting.
+        assertTrue(mBuilder.equalNumbers("6505555555", "650 555 5555"));
+        assertTrue(mBuilder.equalNumbers("6505555555", "(650) 555-5555"));
+        assertTrue(mBuilder.equalNumbers("650 555 5555", "(650) 555-5555"));
+        // Short codes.
+        assertTrue(mBuilder.equalNumbers("55555", "55555"));
+        assertTrue(mBuilder.equalNumbers("55555", "555 55"));
+        // Different numbers.
+        assertFalse(mBuilder.equalNumbers("6505555555", "650555555"));
+        assertFalse(mBuilder.equalNumbers("6505555555", "6505555551"));
+        assertFalse(mBuilder.equalNumbers("650 555 5555", "650 555 555"));
+        assertFalse(mBuilder.equalNumbers("650 555 5555", "650 555 5551"));
+        assertFalse(mBuilder.equalNumbers("55555", "5555"));
+        assertFalse(mBuilder.equalNumbers("55555", "55551"));
+        // SIP addresses.
+        assertTrue(mBuilder.equalNumbers("6505555555@host.com", "6505555555@host.com"));
+        assertTrue(mBuilder.equalNumbers("6505555555@host.com", "6505555555@HOST.COM"));
+        assertTrue(mBuilder.equalNumbers("user@host.com", "user@host.com"));
+        assertTrue(mBuilder.equalNumbers("user@host.com", "user@HOST.COM"));
+        assertFalse(mBuilder.equalNumbers("USER@host.com", "user@host.com"));
+        assertFalse(mBuilder.equalNumbers("user@host.com", "user@host1.com"));
+        // SIP address vs phone number.
+        assertFalse(mBuilder.equalNumbers("6505555555@host.com", "6505555555"));
+        assertFalse(mBuilder.equalNumbers("6505555555", "6505555555@host.com"));
+        assertFalse(mBuilder.equalNumbers("user@host.com", "6505555555"));
+        assertFalse(mBuilder.equalNumbers("6505555555", "user@host.com"));
+        // Nulls.
+        assertTrue(mBuilder.equalNumbers(null, null));
+        assertFalse(mBuilder.equalNumbers(null, "6505555555"));
+        assertFalse(mBuilder.equalNumbers("6505555555", null));
+        assertFalse(mBuilder.equalNumbers(null, "6505555555@host.com"));
+        assertFalse(mBuilder.equalNumbers("6505555555@host.com", null));
+    }
+
+    public void testCompareSipAddresses() {
+        // Identical.
+        assertTrue(mBuilder.compareSipAddresses("6505555555@host.com", "6505555555@host.com"));
+        assertTrue(mBuilder.compareSipAddresses("user@host.com", "user@host.com"));
+        // Host is case insensitive.
+        assertTrue(mBuilder.compareSipAddresses("6505555555@host.com", "6505555555@HOST.COM"));
+        assertTrue(mBuilder.compareSipAddresses("user@host.com", "user@HOST.COM"));
+        // Userinfo is case sensitive.
+        assertFalse(mBuilder.compareSipAddresses("USER@host.com", "user@host.com"));
+        // Different hosts.
+        assertFalse(mBuilder.compareSipAddresses("user@host.com", "user@host1.com"));
+        // Different users.
+        assertFalse(mBuilder.compareSipAddresses("user1@host.com", "user@host.com"));
+        // Nulls.
+        assertTrue(mBuilder.compareSipAddresses(null, null));
+        assertFalse(mBuilder.compareSipAddresses(null, "6505555555@host.com"));
+        assertFalse(mBuilder.compareSipAddresses("6505555555@host.com", null));
+    }
+
+    /** Creates (or recreates) the cursor used to store the call log content for the tests. */
+    private void createCursor() {
+        mCursor = new MatrixCursor(CallLogQuery.EXTENDED_PROJECTION);
+    }
+
+    /** Clears the content of the {@link FakeGroupCreator} used in the tests. */
+    private void clearFakeGroupCreator() {
+        mFakeGroupCreator.groups.clear();
+    }
+
+    /** Asserts that calls of the given types are grouped together into a single group. */
+    private void assertCallsAreGrouped(int... types) {
+        createCursor();
+        clearFakeGroupCreator();
+        addMultipleOldCallLogEntries(TEST_NUMBER1, types);
+        mBuilder.addGroups(mCursor);
+        assertEquals(1, mFakeGroupCreator.groups.size());
+        assertGroupIs(0, types.length, false, mFakeGroupCreator.groups.get(0));
+
+    }
+
+    /** Asserts that calls of the given types are not grouped together at all. */
+    private void assertCallsAreNotGrouped(int... types) {
+        createCursor();
+        clearFakeGroupCreator();
+        addMultipleOldCallLogEntries(TEST_NUMBER1, types);
+        mBuilder.addGroups(mCursor);
+        assertEquals(0, mFakeGroupCreator.groups.size());
+    }
+
+    /** Adds a set of calls with the given types, all from the same number, in the old section. */
+    private void addMultipleOldCallLogEntries(String number, int... types) {
+        for (int type : types) {
+            addOldCallLogEntry(number, type);
+        }
+    }
+
+    /** Adds a call with the given number and type to the old section of the call log. */
+    private void addOldCallLogEntry(String number, int type) {
+        addCallLogEntry(number, type, CallLogQuery.SECTION_OLD_ITEM);
+    }
+
+    /** Adds a call with the given number and type to the new section of the call log. */
+    private void addNewCallLogEntry(String number, int type) {
+        addCallLogEntry(number, type, CallLogQuery.SECTION_NEW_ITEM);
+    }
+
+    /** Adds a call log entry with the given number and type to the cursor. */
+    private void addCallLogEntry(String number, int type, int section) {
+        if (section != CallLogQuery.SECTION_NEW_ITEM
+                && section != CallLogQuery.SECTION_OLD_ITEM) {
+            throw new IllegalArgumentException("not an item section: " + section);
+        }
+        mCursor.moveToNext();
+        Object[] values = CallLogQueryTestUtils.createTestExtendedValues();
+        values[CallLogQuery.ID] = mCursor.getPosition();
+        values[CallLogQuery.NUMBER] = number;
+        values[CallLogQuery.CALL_TYPE] = type;
+        values[CallLogQuery.SECTION] = section;
+        mCursor.addRow(values);
+    }
+
+    /** Adds the old section header to the call log. */
+    private void addOldCallLogHeader() {
+        addCallLogHeader(CallLogQuery.SECTION_OLD_HEADER);
+    }
+
+    /** Adds the new section header to the call log. */
+    private void addNewCallLogHeader() {
+        addCallLogHeader(CallLogQuery.SECTION_NEW_HEADER);
+    }
+
+    /** Adds a call log entry with a header to the cursor. */
+    private void addCallLogHeader(int section) {
+        if (section != CallLogQuery.SECTION_NEW_HEADER
+                && section != CallLogQuery.SECTION_OLD_HEADER) {
+            throw new IllegalArgumentException("not a header section: " + section);
+        }
+        mCursor.moveToNext();
+        Object[] values = CallLogQueryTestUtils.createTestExtendedValues();
+        values[CallLogQuery.ID] = mCursor.getPosition();
+        values[CallLogQuery.SECTION] = section;
+        mCursor.addRow(values);
+    }
+
+    /** Asserts that the group matches the given values. */
+    private void assertGroupIs(int cursorPosition, int size, boolean expanded, GroupSpec group) {
+        assertEquals(cursorPosition, group.cursorPosition);
+        assertEquals(size, group.size);
+        assertEquals(expanded, group.expanded);
+    }
+
+    /** Defines an added group. Used by the {@link FakeGroupCreator}. */
+    private static class GroupSpec {
+        /** The starting position of the group. */
+        public final int cursorPosition;
+        /** The number of elements in the group. */
+        public final int size;
+        /** Whether the group should be initially expanded. */
+        public final boolean expanded;
+
+        public GroupSpec(int cursorPosition, int size, boolean expanded) {
+            this.cursorPosition = cursorPosition;
+            this.size = size;
+            this.expanded = expanded;
+        }
+    }
+
+    /** Fake implementation of a GroupCreator which stores the created groups in a member field. */
+    private static class FakeGroupCreator implements CallLogGroupBuilder.GroupCreator {
+        /** The list of created groups. */
+        public final List<GroupSpec> groups = newArrayList();
+
+        @Override
+        public void addGroup(int cursorPosition, int size, boolean expanded) {
+            groups.add(new GroupSpec(cursorPosition, size, expanded));
+        }
+    }
+}
diff --git a/tests/src/com/android/dialer/calllog/CallLogListItemHelperTest.java b/tests/src/com/android/dialer/calllog/CallLogListItemHelperTest.java
new file mode 100644
index 0000000..3ad5abe
--- /dev/null
+++ b/tests/src/com/android/dialer/calllog/CallLogListItemHelperTest.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.provider.CallLog.Calls;
+import android.test.AndroidTestCase;
+import android.view.View;
+
+import com.android.dialer.PhoneCallDetails;
+import com.android.dialer.PhoneCallDetailsHelper;
+import com.android.internal.telephony.CallerInfo;
+
+/**
+ * Unit tests for {@link CallLogListItemHelper}.
+ */
+public class CallLogListItemHelperTest extends AndroidTestCase {
+    /** A test phone number for phone calls. */
+    private static final String TEST_NUMBER = "14125555555";
+    /** The formatted version of {@link #TEST_NUMBER}. */
+    private static final String TEST_FORMATTED_NUMBER = "1-412-255-5555";
+    /** A test date value for phone calls. */
+    private static final long TEST_DATE = 1300000000;
+    /** A test duration value for phone calls. */
+    private static final long TEST_DURATION = 62300;
+    /** A test voicemail number. */
+    private static final String TEST_VOICEMAIL_NUMBER = "123";
+    /** The country ISO name used in the tests. */
+    private static final String TEST_COUNTRY_ISO = "US";
+    /** The geocoded location used in the tests. */
+    private static final String TEST_GEOCODE = "United States";
+
+    /** The object under test. */
+    private CallLogListItemHelper mHelper;
+
+    /** The views used in the tests. */
+    private CallLogListItemViews mViews;
+    private PhoneNumberHelper mPhoneNumberHelper;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        Context context = getContext();
+        Resources resources = context.getResources();
+        CallTypeHelper callTypeHelper = new CallTypeHelper(resources);
+        mPhoneNumberHelper = new TestPhoneNumberHelper(resources, TEST_VOICEMAIL_NUMBER);
+        PhoneCallDetailsHelper phoneCallDetailsHelper = new PhoneCallDetailsHelper(
+                resources, callTypeHelper, mPhoneNumberHelper);
+        mHelper = new CallLogListItemHelper(phoneCallDetailsHelper, mPhoneNumberHelper, resources);
+        mViews = CallLogListItemViews.createForTest(context);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mHelper = null;
+        mViews = null;
+        super.tearDown();
+    }
+
+    public void testSetPhoneCallDetails() {
+        setPhoneCallDetailsWithNumber("12125551234", "1-212-555-1234");
+        assertEquals(View.VISIBLE, mViews.secondaryActionView.getVisibility());
+    }
+
+    public void testSetPhoneCallDetails_Unknown() {
+        setPhoneCallDetailsWithNumber(CallerInfo.UNKNOWN_NUMBER, CallerInfo.UNKNOWN_NUMBER);
+        assertNoCallButton();
+    }
+
+    public void testSetPhoneCallDetails_Private() {
+        setPhoneCallDetailsWithNumber(CallerInfo.PRIVATE_NUMBER, CallerInfo.PRIVATE_NUMBER);
+        assertNoCallButton();
+    }
+
+    public void testSetPhoneCallDetails_Payphone() {
+        setPhoneCallDetailsWithNumber(CallerInfo.PAYPHONE_NUMBER, CallerInfo.PAYPHONE_NUMBER);
+        assertNoCallButton();
+    }
+
+    public void testSetPhoneCallDetails_VoicemailNumber() {
+        setPhoneCallDetailsWithNumber(TEST_VOICEMAIL_NUMBER, TEST_VOICEMAIL_NUMBER);
+        assertEquals(View.VISIBLE, mViews.secondaryActionView.getVisibility());
+    }
+
+    public void testSetPhoneCallDetails_ReadVoicemail() {
+        setPhoneCallDetailsWithTypes(Calls.VOICEMAIL_TYPE);
+        assertEquals(View.VISIBLE, mViews.secondaryActionView.getVisibility());
+    }
+
+    public void testSetPhoneCallDetails_UnreadVoicemail() {
+        setUnreadPhoneCallDetailsWithTypes(Calls.VOICEMAIL_TYPE);
+        assertEquals(View.VISIBLE, mViews.secondaryActionView.getVisibility());
+    }
+
+    public void testSetPhoneCallDetails_VoicemailFromUnknown() {
+        setPhoneCallDetailsWithNumberAndType(CallerInfo.UNKNOWN_NUMBER, CallerInfo.UNKNOWN_NUMBER,
+                Calls.VOICEMAIL_TYPE);
+        assertEquals(View.VISIBLE, mViews.secondaryActionView.getVisibility());
+    }
+
+    /** Asserts that the whole call area is gone. */
+    private void assertNoCallButton() {
+        assertEquals(View.GONE, mViews.secondaryActionView.getVisibility());
+        assertEquals(View.GONE, mViews.dividerView.getVisibility());
+    }
+
+    /** Sets the details of a phone call using the specified phone number. */
+    private void setPhoneCallDetailsWithNumber(String number, String formattedNumber) {
+        setPhoneCallDetailsWithNumberAndType(number, formattedNumber, Calls.INCOMING_TYPE);
+    }
+
+    /** Sets the details of a phone call using the specified phone number. */
+    private void setPhoneCallDetailsWithNumberAndType(String number, String formattedNumber,
+            int callType) {
+        mHelper.setPhoneCallDetails(mViews,
+                new PhoneCallDetails(number, formattedNumber, TEST_COUNTRY_ISO, TEST_GEOCODE,
+                        new int[]{ callType }, TEST_DATE, TEST_DURATION),
+                false);
+    }
+
+    /** Sets the details of a phone call using the specified call type. */
+    private void setPhoneCallDetailsWithTypes(int... types) {
+        mHelper.setPhoneCallDetails(mViews,
+                new PhoneCallDetails(TEST_NUMBER, TEST_FORMATTED_NUMBER, TEST_COUNTRY_ISO,
+                        TEST_GEOCODE, types, TEST_DATE, TEST_DURATION),
+                false);
+    }
+
+    /** Sets the details of a phone call using the specified call type. */
+    private void setUnreadPhoneCallDetailsWithTypes(int... types) {
+        mHelper.setPhoneCallDetails(mViews,
+                new PhoneCallDetails(TEST_NUMBER, TEST_FORMATTED_NUMBER, TEST_COUNTRY_ISO,
+                        TEST_GEOCODE, types, TEST_DATE, TEST_DURATION),
+                true);
+    }
+}
diff --git a/tests/src/com/android/dialer/calllog/CallLogQueryTestUtils.java b/tests/src/com/android/dialer/calllog/CallLogQueryTestUtils.java
new file mode 100644
index 0000000..4be84ae
--- /dev/null
+++ b/tests/src/com/android/dialer/calllog/CallLogQueryTestUtils.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import static junit.framework.Assert.assertEquals;
+
+import android.provider.CallLog.Calls;
+
+import junit.framework.Assert;
+
+/**
+ * Helper class to create test values for {@link CallLogQuery}.
+ */
+public class CallLogQueryTestUtils {
+    public static Object[] createTestValues() {
+        Object[] values = new Object[]{
+                0L, "", 0L, 0L, Calls.INCOMING_TYPE, "", "", "", null, 0, null, null, null, null,
+                0L, null, 0,
+        };
+        assertEquals(CallLogQuery._PROJECTION.length, values.length);
+        return values;
+    }
+
+    public static Object[] createTestExtendedValues() {
+        Object[] values = new Object[]{
+                0L, "", 0L, 0L, Calls.INCOMING_TYPE, "", "", "", null, 0, null, null, null, null,
+                0L, null, 1, CallLogQuery.SECTION_OLD_ITEM
+        };
+        Assert.assertEquals(CallLogQuery.EXTENDED_PROJECTION.length, values.length);
+        return values;
+    }
+}
diff --git a/tests/src/com/android/dialer/calllog/TestPhoneNumberHelper.java b/tests/src/com/android/dialer/calllog/TestPhoneNumberHelper.java
new file mode 100644
index 0000000..1446359
--- /dev/null
+++ b/tests/src/com/android/dialer/calllog/TestPhoneNumberHelper.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2011 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.dialer.calllog;
+
+import android.content.res.Resources;
+
+/**
+ * Modified version of {@link PhoneNumberHelper} to be used in tests that allows injecting the
+ * voicemail number.
+ */
+public final class TestPhoneNumberHelper extends PhoneNumberHelper {
+    private CharSequence mVoicemailNumber;
+
+    public TestPhoneNumberHelper(Resources resources, CharSequence voicemailNumber) {
+        super(resources);
+        mVoicemailNumber = voicemailNumber;
+    }
+
+    @Override
+    public boolean isVoicemailNumber(CharSequence number) {
+        return mVoicemailNumber.equals(number);
+    }
+}
diff --git a/tests/src/com/android/dialer/tests/calllog/FillCallLogTestActivity.java b/tests/src/com/android/dialer/tests/calllog/FillCallLogTestActivity.java
new file mode 100644
index 0000000..ed49220
--- /dev/null
+++ b/tests/src/com/android/dialer/tests/calllog/FillCallLogTestActivity.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (C) 2011 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.dialer.tests.calllog;
+
+import android.app.Activity;
+import android.app.LoaderManager;
+import android.content.ContentProviderClient;
+import android.content.ContentValues;
+import android.content.CursorLoader;
+import android.content.Loader;
+import android.database.Cursor;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.provider.CallLog.Calls;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.dialer.tests.R;
+
+import java.util.Random;
+
+/**
+ * Activity to add entries to the call log for testing.
+ */
+public class FillCallLogTestActivity extends Activity {
+    private static final String TAG = "FillCallLogTestActivity";
+    /** Identifier of the loader for querying the call log. */
+    private static final int CALLLOG_LOADER_ID = 1;
+
+    private static final Random RNG = new Random();
+    private static final int[] CALL_TYPES = new int[] {
+        Calls.INCOMING_TYPE, Calls.OUTGOING_TYPE, Calls.MISSED_TYPE,
+    };
+
+    private TextView mNumberTextView;
+    private Button mAddButton;
+    private ProgressBar mProgressBar;
+    private CheckBox mUseRandomNumbers;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.fill_call_log_test);
+        mNumberTextView = (TextView) findViewById(R.id.number);
+        mAddButton = (Button) findViewById(R.id.add);
+        mProgressBar = (ProgressBar) findViewById(R.id.progress);
+        mUseRandomNumbers = (CheckBox) findViewById(R.id.use_random_numbers);
+
+        mAddButton.setOnClickListener(new View.OnClickListener(){
+            @Override
+            public void onClick(View v) {
+                int count;
+                try {
+                    count = Integer.parseInt(mNumberTextView.getText().toString());
+                    if (count > 100) {
+                        throw new RuntimeException("Number too large.  Max=100");
+                    }
+                } catch (RuntimeException e) {
+                    Toast.makeText(FillCallLogTestActivity.this, e.toString(), Toast.LENGTH_LONG)
+                            .show();
+                    return;
+                }
+                addEntriesToCallLog(count, mUseRandomNumbers.isChecked());
+                mNumberTextView.setEnabled(false);
+                mAddButton.setEnabled(false);
+                mProgressBar.setProgress(0);
+                mProgressBar.setMax(count);
+                mProgressBar.setVisibility(View.VISIBLE);
+            }
+        });
+    }
+
+    /**
+     * Adds a number of entries to the call log. The content of the entries is based on existing
+     * entries.
+     *
+     * @param count the number of entries to add
+     */
+    private void addEntriesToCallLog(final int count, boolean useRandomNumbers) {
+        if (useRandomNumbers) {
+            addRandomNumbers(count);
+        } else {
+            getLoaderManager().initLoader(CALLLOG_LOADER_ID, null,
+                    new CallLogLoaderListener(count));
+        }
+    }
+
+    /**
+     * Calls when the insertion has completed.
+     *
+     * @param message the message to show in a toast to the user
+     */
+    private void insertCompleted(String message) {
+        // Hide the progress bar.
+        mProgressBar.setVisibility(View.GONE);
+        // Re-enable the add button.
+        mNumberTextView.setEnabled(true);
+        mAddButton.setEnabled(true);
+        mNumberTextView.setText("");
+        Toast.makeText(this, message, Toast.LENGTH_LONG).show();
+    }
+
+
+    /**
+     * Creates a {@link ContentValues} object containing values corresponding to the given cursor.
+     *
+     * @param cursor the cursor from which to get the values
+     * @return a newly created content values object
+     */
+    private ContentValues createContentValuesFromCursor(Cursor cursor) {
+        ContentValues values = new ContentValues();
+        for (int column = 0; column < cursor.getColumnCount();
+                ++column) {
+            String name = cursor.getColumnName(column);
+            switch (cursor.getType(column)) {
+                case Cursor.FIELD_TYPE_STRING:
+                    values.put(name, cursor.getString(column));
+                    break;
+                case Cursor.FIELD_TYPE_INTEGER:
+                    values.put(name, cursor.getLong(column));
+                    break;
+                case Cursor.FIELD_TYPE_FLOAT:
+                    values.put(name, cursor.getDouble(column));
+                    break;
+                case Cursor.FIELD_TYPE_BLOB:
+                    values.put(name, cursor.getBlob(column));
+                    break;
+                case Cursor.FIELD_TYPE_NULL:
+                    values.putNull(name);
+                    break;
+                default:
+                    Log.d(TAG, "Invalid value in cursor: " + cursor.getType(column));
+                    break;
+            }
+        }
+        return values;
+    }
+
+    private void addRandomNumbers(int count) {
+        ContentValues[] values = new ContentValues[count];
+        for (int i = 0; i < count; i++) {
+            values[i] = new ContentValues();
+            values[i].put(Calls.NUMBER, generateRandomNumber());
+            values[i].put(Calls.DATE, System.currentTimeMillis()); // Will be randomized later
+            values[i].put(Calls.DURATION, 1); // Will be overwritten later
+        }
+        new AsyncCallLogInserter(values).execute(new Void[0]);
+    }
+
+    private static String generateRandomNumber() {
+        return String.format("5%09d", RNG.nextInt(1000000000));
+    }
+
+    /** Invokes {@link AsyncCallLogInserter} when the call log has loaded. */
+    private final class CallLogLoaderListener implements LoaderManager.LoaderCallbacks<Cursor> {
+        /** The number of items to insert when done. */
+        private final int mCount;
+
+        private CallLogLoaderListener(int count) {
+            mCount = count;
+        }
+
+        @Override
+        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+            Log.d(TAG, "onCreateLoader");
+            return new CursorLoader(FillCallLogTestActivity.this, Calls.CONTENT_URI,
+                    null, null, null, null);
+        }
+
+        @Override
+        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+            try {
+                Log.d(TAG, "onLoadFinished");
+
+                if (data.getCount() == 0) {
+                    // If there are no entries in the call log, we cannot generate new ones.
+                    insertCompleted(getString(R.string.noLogEntriesToast));
+                    return;
+                }
+
+                data.moveToPosition(-1);
+
+                ContentValues[] values = new ContentValues[mCount];
+                for (int index = 0; index < mCount; ++index) {
+                    if (!data.moveToNext()) {
+                        data.moveToFirst();
+                    }
+                    values[index] = createContentValuesFromCursor(data);
+                }
+                new AsyncCallLogInserter(values).execute(new Void[0]);
+            } finally {
+                // This is a one shot loader.
+                getLoaderManager().destroyLoader(CALLLOG_LOADER_ID);
+            }
+        }
+
+        @Override
+        public void onLoaderReset(Loader<Cursor> loader) {}
+    }
+
+    /** Inserts a given number of entries in the call log based on the values given. */
+    private final class AsyncCallLogInserter extends AsyncTask<Void, Integer, Integer> {
+        /** The number of items to insert. */
+        private final ContentValues[] mValues;
+
+        public AsyncCallLogInserter(ContentValues[] values) {
+            mValues = values;
+        }
+
+        @Override
+        protected Integer doInBackground(Void... params) {
+            Log.d(TAG, "doInBackground");
+            return insertIntoCallLog();
+        }
+
+        @Override
+        protected void onProgressUpdate(Integer... values) {
+            Log.d(TAG, "onProgressUpdate");
+            updateCount(values[0]);
+        }
+
+        @Override
+        protected void onPostExecute(Integer count) {
+            Log.d(TAG, "onPostExecute");
+            insertCompleted(getString(R.string.addedLogEntriesToast, count));
+        }
+
+        /**
+         * Inserts a number of entries in the call log based on the given templates.
+         *
+         * @return the number of inserted entries
+         */
+        private Integer insertIntoCallLog() {
+            int inserted = 0;
+
+            for (int index = 0; index < mValues.length; ++index) {
+                ContentValues values = mValues[index];
+                // These should not be set.
+                values.putNull(Calls._ID);
+                // Add some randomness to the date. For each new entry being added, add an extra
+                // day to the maximum possible offset from the original.
+                values.put(Calls.DATE,
+                        values.getAsLong(Calls.DATE)
+                        - RNG.nextInt(24 * 60 * 60 * (index + 1)) * 1000L);
+                // Add some randomness to the duration.
+                if (values.getAsLong(Calls.DURATION) > 0) {
+                    values.put(Calls.DURATION, RNG.nextInt(30 * 60 * 60 * 1000));
+                }
+
+                // Overwrite type.
+                values.put(Calls.TYPE, CALL_TYPES[RNG.nextInt(CALL_TYPES.length)]);
+
+                // Clear cached columns.
+                values.putNull(Calls.CACHED_FORMATTED_NUMBER);
+                values.putNull(Calls.CACHED_LOOKUP_URI);
+                values.putNull(Calls.CACHED_MATCHED_NUMBER);
+                values.putNull(Calls.CACHED_NAME);
+                values.putNull(Calls.CACHED_NORMALIZED_NUMBER);
+                values.putNull(Calls.CACHED_NUMBER_LABEL);
+                values.putNull(Calls.CACHED_NUMBER_TYPE);
+                values.putNull(Calls.CACHED_PHOTO_ID);
+
+                // Insert into the call log the newly generated entry.
+                ContentProviderClient contentProvider =
+                        getContentResolver().acquireContentProviderClient(
+                                Calls.CONTENT_URI);
+                try {
+                    Log.d(TAG, "adding entry to call log");
+                    contentProvider.insert(Calls.CONTENT_URI, values);
+                    ++inserted;
+                    this.publishProgress(inserted);
+                } catch (RemoteException e) {
+                    Log.d(TAG, "insert failed", e);
+                }
+            }
+            return inserted;
+        }
+    }
+
+    /**
+     * Updates the count shown to the user corresponding to the number of entries added.
+     *
+     * @param count the number of entries inserted so far
+     */
+    public void updateCount(Integer count) {
+        mProgressBar.setProgress(count);
+    }
+}
diff --git a/tests/src/com/android/dialer/util/ExpirableCacheTest.java b/tests/src/com/android/dialer/util/ExpirableCacheTest.java
new file mode 100644
index 0000000..b81ad75
--- /dev/null
+++ b/tests/src/com/android/dialer/util/ExpirableCacheTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2011 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.dialer.util;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.LruCache;
+
+import com.android.dialer.util.ExpirableCache.CachedValue;
+
+/**
+ * Unit tests for {@link ExpirableCache}.
+ */
+@SmallTest
+public class ExpirableCacheTest extends AndroidTestCase {
+    /** The object under test. */
+    private ExpirableCache<String, Integer> mCache;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        LruCache<String, CachedValue<Integer>> lruCache =
+            new LruCache<String, ExpirableCache.CachedValue<Integer>>(20);
+        mCache = ExpirableCache.create(lruCache);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mCache = null;
+        super.tearDown();
+    }
+
+    public void testPut() {
+        mCache.put("a", 1);
+        mCache.put("b", 2);
+        assertEquals(1, mCache.getPossiblyExpired("a").intValue());
+        assertEquals(2, mCache.getPossiblyExpired("b").intValue());
+        mCache.put("a", 3);
+        assertEquals(3, mCache.getPossiblyExpired("a").intValue());
+    }
+
+    public void testGet_NotExisting() {
+        assertNull(mCache.getPossiblyExpired("a"));
+        mCache.put("b", 1);
+        assertNull(mCache.getPossiblyExpired("a"));
+    }
+
+    public void testGet_Expired() {
+        mCache.put("a", 1);
+        assertEquals(1, mCache.getPossiblyExpired("a").intValue());
+        mCache.expireAll();
+        assertEquals(1, mCache.getPossiblyExpired("a").intValue());
+    }
+
+    public void testGetNotExpired_NotExisting() {
+        assertNull(mCache.get("a"));
+        mCache.put("b", 1);
+        assertNull(mCache.get("a"));
+    }
+
+    public void testGetNotExpired_Expired() {
+        mCache.put("a", 1);
+        assertEquals(1, mCache.get("a").intValue());
+        mCache.expireAll();
+        assertNull(mCache.get("a"));
+    }
+
+    public void testGetCachedValue_NotExisting() {
+        assertNull(mCache.getCachedValue("a"));
+        mCache.put("b", 1);
+        assertNull(mCache.getCachedValue("a"));
+    }
+
+    public void testGetCachedValue_Expired() {
+        mCache.put("a", 1);
+        assertFalse("Should not be expired", mCache.getCachedValue("a").isExpired());
+        mCache.expireAll();
+        assertTrue("Should be expired", mCache.getCachedValue("a").isExpired());
+    }
+
+    public void testGetChangedValue_PutAfterExpired() {
+        mCache.put("a", 1);
+        mCache.expireAll();
+        mCache.put("a", 1);
+        assertFalse("Should not be expired", mCache.getCachedValue("a").isExpired());
+    }
+
+    public void testComputingCache() {
+        // Creates a cache in which all unknown values default to zero.
+        mCache = ExpirableCache.create(
+                new LruCache<String, ExpirableCache.CachedValue<Integer>>(10) {
+                    @Override
+                    protected CachedValue<Integer> create(String key) {
+                        return mCache.newCachedValue(0);
+                    }
+                });
+
+        // The first time we request a new value, we add it to the cache.
+        CachedValue<Integer> cachedValue = mCache.getCachedValue("a");
+        assertNotNull("Should have been created implicitly", cachedValue);
+        assertEquals(0, cachedValue.getValue().intValue());
+        assertFalse("Should not be expired", cachedValue.isExpired());
+
+        // If we expire all the values, the implicitly created value will also be marked as expired.
+        mCache.expireAll();
+        CachedValue<Integer> expiredCachedValue = mCache.getCachedValue("a");
+        assertNotNull("Should have been created implicitly", expiredCachedValue);
+        assertEquals(0, expiredCachedValue.getValue().intValue());
+        assertTrue("Should be expired", expiredCachedValue.isExpired());
+    }
+}
diff --git a/tests/src/com/android/dialer/util/FakeAsyncTaskExecutor.java b/tests/src/com/android/dialer/util/FakeAsyncTaskExecutor.java
new file mode 100644
index 0000000..064587e
--- /dev/null
+++ b/tests/src/com/android/dialer/util/FakeAsyncTaskExecutor.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2011 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.dialer.util;
+
+import android.app.Instrumentation;
+import android.os.AsyncTask;
+
+import com.android.contacts.util.AsyncTaskExecutor;
+import com.android.contacts.util.AsyncTaskExecutors;
+import com.google.common.collect.Lists;
+
+import junit.framework.Assert;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Test implementation of AsyncTaskExecutor.
+ * <p>
+ * This class is thread-safe. As per the contract of the AsyncTaskExecutor, the submit methods must
+ * be called from the main ui thread, however the other public methods may be called from any thread
+ * (most commonly the test thread).
+ * <p>
+ * Tasks submitted to this executor will not be run immediately. Rather they will be stored in a
+ * list of submitted tasks, where they can be examined. They can also be run on-demand using the run
+ * methods, so that different ordering of AsyncTask execution can be simulated.
+ * <p>
+ * The onPreExecute method of the submitted AsyncTask will be called synchronously during the
+ * call to {@link #submit(Object, AsyncTask, Object...)}.
+ */
+@ThreadSafe
+public class FakeAsyncTaskExecutor implements AsyncTaskExecutor {
+    private static final long DEFAULT_TIMEOUT_MS = 10000;
+
+    /** The maximum length of time in ms to wait for tasks to execute during tests. */
+    private final long mTimeoutMs = DEFAULT_TIMEOUT_MS;
+
+    private final Object mLock = new Object();
+    @GuardedBy("mLock") private final List<SubmittedTask> mSubmittedTasks = Lists.newArrayList();
+
+    private final DelayedExecutor mBlockingExecutor = new DelayedExecutor();
+    private final Instrumentation mInstrumentation;
+
+    /** Create a fake AsyncTaskExecutor for use in unit tests. */
+    public FakeAsyncTaskExecutor(Instrumentation instrumentation) {
+        Assert.assertNotNull(instrumentation);
+        mInstrumentation = instrumentation;
+    }
+
+    /** Encapsulates an async task with the params and identifier it was submitted with. */
+    public interface SubmittedTask {
+        Runnable getRunnable();
+        Object getIdentifier();
+        AsyncTask<?, ?, ?> getAsyncTask();
+    }
+
+    private static final class SubmittedTaskImpl implements SubmittedTask {
+        private final Object mIdentifier;
+        private final Runnable mRunnable;
+        private final AsyncTask<?, ?, ?> mAsyncTask;
+
+        public SubmittedTaskImpl(Object identifier, Runnable runnable,
+                AsyncTask<?, ?, ?> asyncTask) {
+            mIdentifier = identifier;
+            mRunnable = runnable;
+            mAsyncTask = asyncTask;
+        }
+
+        @Override
+        public Object getIdentifier() {
+            return mIdentifier;
+        }
+
+        @Override
+        public Runnable getRunnable() {
+            return mRunnable;
+        }
+
+        @Override
+        public AsyncTask<?, ?, ?> getAsyncTask() {
+            return mAsyncTask;
+        }
+
+        @Override
+        public String toString() {
+            return "SubmittedTaskImpl [mIdentifier=" + mIdentifier + "]";
+        }
+    }
+
+    private class DelayedExecutor implements Executor {
+        private final Object mNextLock = new Object();
+        @GuardedBy("mNextLock") private Object mNextIdentifier;
+        @GuardedBy("mNextLock") private AsyncTask<?, ?, ?> mNextTask;
+
+        @Override
+        public void execute(Runnable command) {
+            synchronized (mNextLock) {
+                Assert.assertNotNull(mNextTask);
+                mSubmittedTasks.add(new SubmittedTaskImpl(mNextIdentifier,
+                        command, mNextTask));
+                mNextIdentifier = null;
+                mNextTask = null;
+            }
+        }
+
+        public <T> AsyncTask<T, ?, ?> submit(Object identifier,
+                AsyncTask<T, ?, ?> task, T... params) {
+            synchronized (mNextLock) {
+                Assert.assertNull(mNextIdentifier);
+                Assert.assertNull(mNextTask);
+                mNextIdentifier = identifier;
+                Assert.assertNotNull("Already had a valid task.\n"
+                        + "Are you calling AsyncTaskExecutor.submit(...) from within the "
+                        + "onPreExecute() method of another task being submitted?\n"
+                        + "Sorry!  Not that's not supported.", task);
+                mNextTask = task;
+            }
+            return task.executeOnExecutor(this, params);
+        }
+    }
+
+    @Override
+    public <T> AsyncTask<T, ?, ?> submit(Object identifier, AsyncTask<T, ?, ?> task, T... params) {
+        AsyncTaskExecutors.checkCalledFromUiThread();
+        return mBlockingExecutor.submit(identifier, task, params);
+    }
+
+    /**
+     * Runs a single task matching the given identifier.
+     * <p>
+     * Removes the matching task from the list of submitted tasks, then runs it. The executor used
+     * to execute this async task will be a same-thread executor.
+     * <p>
+     * Fails if there was not exactly one task matching the given identifier.
+     * <p>
+     * This method blocks until the AsyncTask has completely finished executing.
+     */
+    public void runTask(Object identifier) throws InterruptedException {
+        List<SubmittedTask> tasks = getSubmittedTasksByIdentifier(identifier, true);
+        Assert.assertEquals("Expected one task " + identifier + ", got " + tasks, 1, tasks.size());
+        runTask(tasks.get(0));
+    }
+
+    /**
+     * Runs all tasks whose identifier matches the given identifier.
+     * <p>
+     * Removes all matching tasks from the list of submitted tasks, and runs them. The executor used
+     * to execute these async tasks will be a same-thread executor.
+     * <p>
+     * Fails if there were no tasks matching the given identifier.
+     * <p>
+     * This method blocks until the AsyncTask objects have completely finished executing.
+     */
+    public void runAllTasks(Object identifier) throws InterruptedException {
+        List<SubmittedTask> tasks = getSubmittedTasksByIdentifier(identifier, true);
+        Assert.assertTrue("There were no tasks with identifier " + identifier, tasks.size() > 0);
+        for (SubmittedTask task : tasks) {
+            runTask(task);
+        }
+    }
+
+    /**
+     * Executes a single {@link SubmittedTask}.
+     * <p>
+     * Blocks until the task has completed running.
+     */
+    private <T> void runTask(final SubmittedTask submittedTask) throws InterruptedException {
+        submittedTask.getRunnable().run();
+        // Block until the onPostExecute or onCancelled has finished.
+        // Unfortunately we can't be sure when the AsyncTask will have posted its result handling
+        // code to the main ui thread, the best we can do is wait for the Status to be FINISHED.
+        final CountDownLatch latch = new CountDownLatch(1);
+        class AsyncTaskHasFinishedRunnable implements Runnable {
+            @Override
+            public void run() {
+                if (submittedTask.getAsyncTask().getStatus() == AsyncTask.Status.FINISHED) {
+                    latch.countDown();
+                } else {
+                    mInstrumentation.waitForIdle(this);
+                }
+            }
+        }
+        mInstrumentation.waitForIdle(new AsyncTaskHasFinishedRunnable());
+        Assert.assertTrue(latch.await(mTimeoutMs, TimeUnit.MILLISECONDS));
+    }
+
+    private List<SubmittedTask> getSubmittedTasksByIdentifier(
+            Object identifier, boolean remove) {
+        Assert.assertNotNull(identifier);
+        List<SubmittedTask> results = Lists.newArrayList();
+        synchronized (mLock) {
+            Iterator<SubmittedTask> iter = mSubmittedTasks.iterator();
+            while (iter.hasNext()) {
+                SubmittedTask task = iter.next();
+                if (identifier.equals(task.getIdentifier())) {
+                    results.add(task);
+                    iter.remove();
+                }
+            }
+        }
+        return results;
+    }
+
+    /** Get a factory that will return this instance - useful for testing. */
+    public AsyncTaskExecutors.AsyncTaskExecutorFactory getFactory() {
+        return new AsyncTaskExecutors.AsyncTaskExecutorFactory() {
+            @Override
+            public AsyncTaskExecutor createAsyncTaskExeuctor() {
+                return FakeAsyncTaskExecutor.this;
+            }
+        };
+    }
+}
diff --git a/tests/src/com/android/dialer/util/LocaleTestUtils.java b/tests/src/com/android/dialer/util/LocaleTestUtils.java
new file mode 100644
index 0000000..b893ccb
--- /dev/null
+++ b/tests/src/com/android/dialer/util/LocaleTestUtils.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2011 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.dialer.util;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+
+import java.util.Locale;
+
+/**
+ * Utility class to save and restore the locale of the system.
+ * <p>
+ * This can be used for tests that assume to be run in a certain locale, e.g., because they
+ * check against strings in a particular language or require an assumption on how the system
+ * will behave in a specific locale.
+ * <p>
+ * In your test, you can change the locale with the following code:
+ * <pre>
+ * public class CanadaFrenchTest extends AndroidTestCase {
+ *     private LocaleTestUtils mLocaleTestUtils;
+ *
+ *     &#64;Override
+ *     public void setUp() throws Exception {
+ *         super.setUp();
+ *         mLocaleTestUtils = new LocaleTestUtils(getContext());
+ *         mLocaleTestUtils.setLocale(Locale.CANADA_FRENCH);
+ *     }
+ *
+ *     &#64;Override
+ *     public void tearDown() throws Exception {
+ *         mLocaleTestUtils.restoreLocale();
+ *         mLocaleTestUtils = null;
+ *         super.tearDown();
+ *     }
+ *
+ *     ...
+ * }
+ * </pre>
+ * Note that one should not call {@link #setLocale(Locale)} more than once without calling
+ * {@link #restoreLocale()} first.
+ * <p>
+ * This class is not thread-safe. Usually its methods should be invoked only from the test thread.
+ */
+public class LocaleTestUtils {
+    private final Context mContext;
+    private boolean mSaved;
+    private Locale mSavedContextLocale;
+    private Locale mSavedSystemLocale;
+
+    /**
+     * Create a new instance that can be used to set and reset the locale for the given context.
+     *
+     * @param context the context on which to alter the locale
+     */
+    public LocaleTestUtils(Context context) {
+        mContext = context;
+        mSaved = false;
+    }
+
+    /**
+     * Set the locale to the given value and saves the previous value.
+     *
+     * @param locale the value to which the locale should be set
+     * @throws IllegalStateException if the locale was already set
+     */
+    public void setLocale(Locale locale) {
+        if (mSaved) {
+            throw new IllegalStateException(
+                    "call restoreLocale() before calling setLocale() again");
+        }
+        mSavedContextLocale = setResourcesLocale(mContext.getResources(), locale);
+        mSavedSystemLocale = setResourcesLocale(Resources.getSystem(), locale);
+        mSaved = true;
+    }
+
+    /**
+     * Restores the previously set locale.
+     *
+     * @throws IllegalStateException if the locale was not set using {@link #setLocale(Locale)}
+     */
+    public void restoreLocale() {
+        if (!mSaved) {
+            throw new IllegalStateException("call setLocale() before calling restoreLocale()");
+        }
+        setResourcesLocale(mContext.getResources(), mSavedContextLocale);
+        setResourcesLocale(Resources.getSystem(), mSavedSystemLocale);
+        mSaved = false;
+    }
+
+    /**
+     * Sets the locale for the given resources and returns the previous locale.
+     *
+     * @param resources the resources on which to set the locale
+     * @param locale the value to which to set the locale
+     * @return the previous value of the locale for the resources
+     */
+    private Locale setResourcesLocale(Resources resources, Locale locale) {
+        Configuration contextConfiguration = new Configuration(resources.getConfiguration());
+        Locale savedLocale = contextConfiguration.locale;
+        contextConfiguration.locale = locale;
+        resources.updateConfiguration(contextConfiguration, null);
+        return savedLocale;
+    }
+}
diff --git a/tests/src/com/android/dialer/voicemail/VoicemailStatusHelperImplTest.java b/tests/src/com/android/dialer/voicemail/VoicemailStatusHelperImplTest.java
new file mode 100644
index 0000000..2e75d1d
--- /dev/null
+++ b/tests/src/com/android/dialer/voicemail/VoicemailStatusHelperImplTest.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2011 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.dialer.voicemail;
+
+import static android.provider.VoicemailContract.Status.CONFIGURATION_STATE;
+import static android.provider.VoicemailContract.Status.CONFIGURATION_STATE_CAN_BE_CONFIGURED;
+import static android.provider.VoicemailContract.Status.CONFIGURATION_STATE_NOT_CONFIGURED;
+import static android.provider.VoicemailContract.Status.DATA_CHANNEL_STATE;
+import static android.provider.VoicemailContract.Status.DATA_CHANNEL_STATE_NO_CONNECTION;
+import static android.provider.VoicemailContract.Status.DATA_CHANNEL_STATE_OK;
+import static android.provider.VoicemailContract.Status.NOTIFICATION_CHANNEL_STATE;
+import static android.provider.VoicemailContract.Status.NOTIFICATION_CHANNEL_STATE_MESSAGE_WAITING;
+import static android.provider.VoicemailContract.Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION;
+import static android.provider.VoicemailContract.Status.NOTIFICATION_CHANNEL_STATE_OK;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.VoicemailContract.Status;
+import android.test.AndroidTestCase;
+
+import com.android.contacts.R;
+import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage;
+
+import java.util.List;
+
+/**
+ * Unit tests for {@link VoicemailStatusHelperImpl}.
+ */
+public class VoicemailStatusHelperImplTest extends AndroidTestCase {
+    private static final String[] TEST_PACKAGES = new String[] {
+        "com.test.package1",
+        "com.test.package2"
+    };
+
+    private static final Uri TEST_SETTINGS_URI = Uri.parse("http://www.visual.voicemail.setup");
+    private static final Uri TEST_VOICEMAIL_URI = Uri.parse("tel:901");
+
+    private static final int ACTION_MSG_CALL_VOICEMAIL =
+            R.string.voicemail_status_action_call_server;
+    private static final int ACTION_MSG_CONFIGURE = R.string.voicemail_status_action_configure;
+
+    private static final int STATUS_MSG_NONE = -1;
+    private static final int STATUS_MSG_VOICEMAIL_NOT_AVAILABLE =
+            R.string.voicemail_status_voicemail_not_available;
+    private static final int STATUS_MSG_AUDIO_NOT_AVAIALABLE =
+            R.string.voicemail_status_audio_not_available;
+    private static final int STATUS_MSG_MESSAGE_WAITING = R.string.voicemail_status_messages_waiting;
+    private static final int STATUS_MSG_INVITE_FOR_CONFIGURATION =
+            R.string.voicemail_status_configure_voicemail;
+
+    // Object under test.
+    private VoicemailStatusHelper mStatusHelper;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mStatusHelper = new VoicemailStatusHelperImpl();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        for (String sourcePackage : TEST_PACKAGES) {
+            deleteEntryForPackage(sourcePackage);
+        }
+        // Set member variables to null so that they are garbage collected across different runs
+        // of the tests.
+        mStatusHelper = null;
+        super.tearDown();
+    }
+
+
+    public void testNoStatusEntries() {
+        assertEquals(0, getStatusMessages().size());
+    }
+
+    public void testAllOK() {
+        insertEntryForPackage(TEST_PACKAGES[0], getAllOkStatusValues());
+        insertEntryForPackage(TEST_PACKAGES[1], getAllOkStatusValues());
+        assertEquals(0, getStatusMessages().size());
+    }
+
+    public void testNotAllOKForOnePackage() {
+        insertEntryForPackage(TEST_PACKAGES[0], getAllOkStatusValues());
+        insertEntryForPackage(TEST_PACKAGES[1], getAllOkStatusValues());
+
+        ContentValues values = new ContentValues();
+        // Good data channel + no notification
+        // action: call voicemail
+        // msg: voicemail not available in call log page & none in call details page.
+        values.put(NOTIFICATION_CHANNEL_STATE, NOTIFICATION_CHANNEL_STATE_NO_CONNECTION);
+        values.put(DATA_CHANNEL_STATE, DATA_CHANNEL_STATE_OK);
+        updateEntryForPackage(TEST_PACKAGES[1], values);
+        checkExpectedMessage(TEST_PACKAGES[1], values, STATUS_MSG_VOICEMAIL_NOT_AVAILABLE,
+                STATUS_MSG_NONE, ACTION_MSG_CALL_VOICEMAIL);
+
+        // Message waiting + good data channel - no action.
+        values.put(NOTIFICATION_CHANNEL_STATE, NOTIFICATION_CHANNEL_STATE_MESSAGE_WAITING);
+        values.put(DATA_CHANNEL_STATE, DATA_CHANNEL_STATE_OK);
+        updateEntryForPackage(TEST_PACKAGES[1], values);
+        checkNoMessages(TEST_PACKAGES[1], values);
+
+        // No data channel + no notification
+        // action: call voicemail
+        // msg: voicemail not available in call log page & audio not available in call details page.
+        values.put(NOTIFICATION_CHANNEL_STATE, NOTIFICATION_CHANNEL_STATE_OK);
+        values.put(DATA_CHANNEL_STATE, DATA_CHANNEL_STATE_NO_CONNECTION);
+        updateEntryForPackage(TEST_PACKAGES[1], values);
+        checkExpectedMessage(TEST_PACKAGES[1], values, STATUS_MSG_VOICEMAIL_NOT_AVAILABLE,
+                STATUS_MSG_AUDIO_NOT_AVAIALABLE, ACTION_MSG_CALL_VOICEMAIL);
+
+        // No data channel + Notification OK
+        // action: call voicemail
+        // msg: voicemail not available in call log page & audio not available in call details page.
+        values.put(NOTIFICATION_CHANNEL_STATE, NOTIFICATION_CHANNEL_STATE_NO_CONNECTION);
+        values.put(DATA_CHANNEL_STATE, DATA_CHANNEL_STATE_NO_CONNECTION);
+        updateEntryForPackage(TEST_PACKAGES[1], values);
+        checkExpectedMessage(TEST_PACKAGES[1], values, STATUS_MSG_VOICEMAIL_NOT_AVAILABLE,
+                STATUS_MSG_AUDIO_NOT_AVAIALABLE, ACTION_MSG_CALL_VOICEMAIL);
+
+        // No data channel + Notification OK
+        // action: call voicemail
+        // msg: message waiting in call log page & audio not available in call details page.
+        values.put(NOTIFICATION_CHANNEL_STATE, NOTIFICATION_CHANNEL_STATE_MESSAGE_WAITING);
+        values.put(DATA_CHANNEL_STATE, DATA_CHANNEL_STATE_NO_CONNECTION);
+        updateEntryForPackage(TEST_PACKAGES[1], values);
+        checkExpectedMessage(TEST_PACKAGES[1], values, STATUS_MSG_MESSAGE_WAITING,
+                STATUS_MSG_AUDIO_NOT_AVAIALABLE, ACTION_MSG_CALL_VOICEMAIL);
+
+        // Not configured. No user action, so no message.
+        values.put(CONFIGURATION_STATE, CONFIGURATION_STATE_NOT_CONFIGURED);
+        updateEntryForPackage(TEST_PACKAGES[1], values);
+        checkNoMessages(TEST_PACKAGES[1], values);
+
+        // Can be configured - invite user for configure voicemail.
+        values.put(CONFIGURATION_STATE, CONFIGURATION_STATE_CAN_BE_CONFIGURED);
+        updateEntryForPackage(TEST_PACKAGES[1], values);
+        checkExpectedMessage(TEST_PACKAGES[1], values, STATUS_MSG_INVITE_FOR_CONFIGURATION,
+                STATUS_MSG_NONE, ACTION_MSG_CONFIGURE, TEST_SETTINGS_URI);
+    }
+
+    // Test that priority of messages are handled well.
+    public void testMessageOrdering() {
+        insertEntryForPackage(TEST_PACKAGES[0], getAllOkStatusValues());
+        insertEntryForPackage(TEST_PACKAGES[1], getAllOkStatusValues());
+
+        final ContentValues valuesNoNotificationGoodDataChannel = new ContentValues();
+        valuesNoNotificationGoodDataChannel.put(NOTIFICATION_CHANNEL_STATE,
+                NOTIFICATION_CHANNEL_STATE_NO_CONNECTION);
+        valuesNoNotificationGoodDataChannel.put(DATA_CHANNEL_STATE, DATA_CHANNEL_STATE_OK);
+
+        final ContentValues valuesNoNotificationNoDataChannel = new ContentValues();
+        valuesNoNotificationNoDataChannel.put(NOTIFICATION_CHANNEL_STATE,
+                NOTIFICATION_CHANNEL_STATE_NO_CONNECTION);
+        valuesNoNotificationNoDataChannel.put(DATA_CHANNEL_STATE, DATA_CHANNEL_STATE_NO_CONNECTION);
+
+        // Package1 with valuesNoNotificationGoodDataChannel and
+        // package2 with  valuesNoNotificationNoDataChannel. Package2 should be above.
+        updateEntryForPackage(TEST_PACKAGES[0], valuesNoNotificationGoodDataChannel);
+        updateEntryForPackage(TEST_PACKAGES[1], valuesNoNotificationNoDataChannel);
+        List<StatusMessage> messages = getStatusMessages();
+        assertEquals(2, messages.size());
+        assertEquals(TEST_PACKAGES[0], messages.get(1).sourcePackage);
+        assertEquals(TEST_PACKAGES[1], messages.get(0).sourcePackage);
+
+        // Now reverse the values - ordering should be reversed as well.
+        updateEntryForPackage(TEST_PACKAGES[0], valuesNoNotificationNoDataChannel);
+        updateEntryForPackage(TEST_PACKAGES[1], valuesNoNotificationGoodDataChannel);
+        messages = getStatusMessages();
+        assertEquals(2, messages.size());
+        assertEquals(TEST_PACKAGES[0], messages.get(0).sourcePackage);
+        assertEquals(TEST_PACKAGES[1], messages.get(1).sourcePackage);
+    }
+
+    /** Checks that the expected source status message is returned by VoicemailStatusHelper. */
+    private void checkExpectedMessage(String sourcePackage, ContentValues values,
+            int expectedCallLogMsg, int expectedCallDetailsMsg, int expectedActionMsg,
+            Uri expectedUri) {
+        List<StatusMessage> messages = getStatusMessages();
+        assertEquals(1, messages.size());
+        checkMessageMatches(messages.get(0), sourcePackage, expectedCallLogMsg,
+                expectedCallDetailsMsg, expectedActionMsg, expectedUri);
+    }
+
+    private void checkExpectedMessage(String sourcePackage, ContentValues values,
+            int expectedCallLogMsg, int expectedCallDetailsMessage, int expectedActionMsg) {
+        checkExpectedMessage(sourcePackage, values, expectedCallLogMsg, expectedCallDetailsMessage,
+                expectedActionMsg, TEST_VOICEMAIL_URI);
+    }
+
+    private void checkMessageMatches(StatusMessage message, String expectedSourcePackage,
+            int expectedCallLogMsg, int expectedCallDetailsMsg, int expectedActionMsg,
+            Uri expectedUri) {
+        assertEquals(expectedSourcePackage, message.sourcePackage);
+        assertEquals(expectedCallLogMsg, message.callLogMessageId);
+        assertEquals(expectedCallDetailsMsg, message.callDetailsMessageId);
+        assertEquals(expectedActionMsg, message.actionMessageId);
+        if (expectedUri == null) {
+            assertNull(message.actionUri);
+        } else {
+            assertEquals(expectedUri, message.actionUri);
+        }
+    }
+
+    private void checkNoMessages(String sourcePackage, ContentValues values) {
+        assertEquals(1, updateEntryForPackage(sourcePackage, values));
+        List<StatusMessage> messages = getStatusMessages();
+        assertEquals(0, messages.size());
+    }
+
+    private ContentValues getAllOkStatusValues() {
+        ContentValues values = new ContentValues();
+        values.put(Status.SETTINGS_URI, TEST_SETTINGS_URI.toString());
+        values.put(Status.VOICEMAIL_ACCESS_URI, TEST_VOICEMAIL_URI.toString());
+        values.put(Status.CONFIGURATION_STATE, Status.CONFIGURATION_STATE_OK);
+        values.put(Status.DATA_CHANNEL_STATE, Status.DATA_CHANNEL_STATE_OK);
+        values.put(Status.NOTIFICATION_CHANNEL_STATE, Status.NOTIFICATION_CHANNEL_STATE_OK);
+        return values;
+    }
+
+    private void insertEntryForPackage(String sourcePackage, ContentValues values) {
+        // If insertion fails then try update as the record might already exist.
+        if (getContentResolver().insert(Status.buildSourceUri(sourcePackage), values) == null) {
+            updateEntryForPackage(sourcePackage, values);
+        }
+    }
+
+    private void deleteEntryForPackage(String sourcePackage) {
+        getContentResolver().delete(Status.buildSourceUri(sourcePackage), null, null);
+    }
+
+    private int updateEntryForPackage(String sourcePackage, ContentValues values) {
+        return getContentResolver().update(
+                Status.buildSourceUri(sourcePackage), values, null, null);
+    }
+
+    private List<StatusMessage> getStatusMessages() {
+        // Restrict the cursor to only the the test packages to eliminate any side effects if there
+        // are other status messages already stored on the device.
+        Cursor cursor = getContentResolver().query(Status.CONTENT_URI,
+                VoicemailStatusHelperImpl.PROJECTION, getTestPackageSelection(), null, null);
+        return mStatusHelper.getStatusMessages(cursor);
+    }
+
+    private String getTestPackageSelection() {
+        StringBuilder sb = new StringBuilder();
+        for (String sourcePackage : TEST_PACKAGES) {
+            if (sb.length() > 0) {
+                sb.append(" OR ");
+            }
+            sb.append(String.format("(source_package='%s')", sourcePackage));
+        }
+        return sb.toString();
+    }
+
+    private ContentResolver getContentResolver() {
+        return getContext().getContentResolver();
+    }
+}