diff options
author | Chiao Cheng <chiaocheng@google.com> | 2012-08-17 16:59:12 -0700 |
---|---|---|
committer | Chiao Cheng <chiaocheng@google.com> | 2012-08-21 13:31:19 -0700 |
commit | 94b10b530c0fc297e2974e57e094c500d3ee6003 (patch) | |
tree | b74d663c2663b5db2f6da888081648ce054480f5 /tests | |
parent | dab5cd8890c0d0ca9001a13c2197114a4002338a (diff) |
Initial move of dialer features from contacts app.
Bug: 6993891
Change-Id: I758ce359ca7e87a1d184303822979318be171921
Diffstat (limited to 'tests')
22 files changed, 3429 insertions, 0 deletions
diff --git a/tests/Android.mk b/tests/Android.mk new file mode 100644 index 000000000..d440f6a6d --- /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 000000000..3a714e39a --- /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 000000000..39784b11b --- /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 Binary files differnew file mode 100644 index 000000000..cea0eb3b7 --- /dev/null +++ b/tests/res/drawable/default_icon.png diff --git a/tests/res/drawable/phone_icon.png b/tests/res/drawable/phone_icon.png Binary files differnew file mode 100644 index 000000000..4e613ecce --- /dev/null +++ b/tests/res/drawable/phone_icon.png 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 000000000..704b9c66e --- /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 000000000..ceba5ea96 --- /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 000000000..ec3894566 --- /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 000000000..43204652a --- /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 000000000..cf64f9449 --- /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 000000000..961764475 --- /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 000000000..6ec3e76ef --- /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 000000000..f4534320f --- /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 000000000..6c20afe1d --- /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 000000000..3ad5abe0a --- /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 000000000..4be84aede --- /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 000000000..144635945 --- /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 000000000..ed492205c --- /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 000000000..b81ad754f --- /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 000000000..064587e4b --- /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 000000000..b893ccb76 --- /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; + * + * @Override + * public void setUp() throws Exception { + * super.setUp(); + * mLocaleTestUtils = new LocaleTestUtils(getContext()); + * mLocaleTestUtils.setLocale(Locale.CANADA_FRENCH); + * } + * + * @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 000000000..2e75d1d01 --- /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(); + } +} |