Initial move of dialer features from contacts app.

Bug: 6993891
Change-Id: I758ce359ca7e87a1d184303822979318be171921
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();
+    }
+}