/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.dialer.calllog; import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.database.MatrixCursor; import android.net.Uri; import android.provider.CallLog.Calls; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.VoicemailContract; import android.support.v7.widget.RecyclerView.ViewHolder; import android.telephony.PhoneNumberUtils; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; import android.test.suitebuilder.annotation.MediumTest; import android.test.suitebuilder.annotation.LargeTest; import android.text.TextUtils; import android.view.View; import com.android.dialer.contactinfo.ContactInfoCache; import com.android.dialer.contactinfo.ContactInfoCache.OnContactInfoChangedListener; import com.android.dialer.util.TestConstants; import com.google.common.collect.Lists; import java.util.Date; import java.util.List; import java.util.Random; /** * Unit tests for {@link CallLogAdapter}. * * adb shell am instrument \ * -e com.android.dialer.calllog.CallLogAdapterTest \ * -w com.android.dialer.tests/android.test.InstrumentationTestRunner */ public class CallLogAdapterTest extends AndroidTestCase { private static final String EMPTY_STRING = ""; private static final int NO_VALUE_SET = -1; private static final String TEST_CACHED_NAME = "name"; private static final String TEST_CACHED_NUMBER_LABEL = "label"; private static final int TEST_CACHED_NUMBER_TYPE = 1; private static final String TEST_COUNTRY_ISO = "US"; private static final String TEST_DEFAULT_CUSTOM_LABEL = "myLabel"; private static final Uri TEST_LOOKUP_URI = Uri.parse("content://contacts/2"); private static final String TEST_NUMBER = "12125551000"; private static final String TEST_NUMBER_1 = "12345678"; private static final String TEST_NUMBER_2 = "87654321"; private static final String TEST_NUMBER_3 = "18273645"; private static final String TEST_FORMATTED_NUMBER = "1 212-555-1000"; // The object under test. private TestCallLogAdapter mAdapter; private MatrixCursor mCursor; private View mView; private CallLogListItemViewHolder mViewHolder; private Random mRandom; @Override protected void setUp() throws Exception { super.setUp(); mRandom = new Random(); // 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._PROJECTION); mCursor.moveToFirst(); // The views into which to store the data. mViewHolder = CallLogListItemViewHolder.createForTest(getContext()); } @Override protected void tearDown() throws Exception { mAdapter = null; mCursor = null; super.tearDown(); } @MediumTest public void testBindView_NumberOnlyNoCache() { createCallLogEntry(); mAdapter.changeCursor(mCursor); mAdapter.onBindViewHolder(mViewHolder, 0); assertNameIs(mViewHolder, TEST_NUMBER); } @MediumTest public void testBindView_PrivateCall() { createPrivateCallLogEntry(); mAdapter.changeCursor(mCursor); mAdapter.onBindViewHolder(mViewHolder, 0); assertEquals(Calls.PRESENTATION_RESTRICTED, mViewHolder.numberPresentation); assertNull(mViewHolder.primaryActionButtonView.getTag()); } @MediumTest public void testBindView_UnknownCall() { createUnknownCallLogEntry(); mAdapter.changeCursor(mCursor); mAdapter.onBindViewHolder(mViewHolder, 0); assertEquals(Calls.PRESENTATION_UNKNOWN, mViewHolder.numberPresentation); assertNull(mViewHolder.primaryActionButtonView.getTag()); } @MediumTest public void testBindView_WithoutQuickContactBadge() { createCallLogEntry(); mAdapter.changeCursor(mCursor); mAdapter.onBindViewHolder(mViewHolder, 0); assertFalse(mViewHolder.quickContactView.isEnabled()); } @MediumTest public void testBindView_CallButton() { createCallLogEntry(); mAdapter.changeCursor(mCursor); mAdapter.onBindViewHolder(mViewHolder, 0); // The primaryActionView tag is set when the ViewHolder is binded. If it is possible // to place a call to the phone number, a call intent will have been created which // starts a phone call to the entry's number. assertHasCallAction(mViewHolder); } @MediumTest public void testBindView_VoicemailUri() { createVoicemailCallLogEntry(); mAdapter.changeCursor(mCursor); mAdapter.onBindViewHolder(mViewHolder, 0); assertEquals(Uri.parse(mViewHolder.voicemailUri), ContentUris.withAppendedId(VoicemailContract.Voicemails.CONTENT_URI, 0)); assertNull(mViewHolder.primaryActionButtonView.getTag()); } @MediumTest public void testPresentationAfterRebindingViewHolders() { final int increment = 10; final int size = increment * 4; // Instantiate list of ViewHolders. CallLogListItemViewHolder[] holders = new CallLogListItemViewHolder[size]; for (int i = 0; i < size; i++) { holders[i] = CallLogListItemViewHolder.createForTest(getContext()); } // Add first set of entries to the cursor. for (int i = 0; i < increment; i++) { createCallLogEntry(); createPrivateCallLogEntry(); createCallLogEntry(); createUnknownCallLogEntry(); } mAdapter.changeCursor(mCursor); // Verify correct appearance for presentation. for (int i = 0; i < size; i++) { mAdapter.onBindViewHolder(holders[i], i); if (holders[i].numberPresentation == Calls.PRESENTATION_ALLOWED) { assertHasCallAction(holders[i]); } else { assertNull(holders[i].primaryActionButtonView.getTag()); assertEquals(holders[i].number, EMPTY_STRING); } } // Append the rest of the entries to the cursor. Keep the first set of ViewHolders // so they are updated and not buitl from scratch. This checks for bugs which may // be evident only after the call log is updated. for (int i = 0; i < increment; i++) { createPrivateCallLogEntry(); createCallLogEntry(); createUnknownCallLogEntry(); createCallLogEntry(); } mCursor.move(size); // Verify correct appearnce for presentation. for (int i = 0; i < size; i++) { mAdapter.onBindViewHolder(holders[i], i + size); if (holders[i].numberPresentation == Calls.PRESENTATION_ALLOWED) { assertHasCallAction(holders[i]); } else { assertNull(holders[i].primaryActionButtonView.getTag()); assertEquals(holders[i].number, EMPTY_STRING); } } } @MediumTest public void testBindView_NoCallLogCacheNorMemoryCache_EnqueueRequest() { createCallLogEntry(); // Bind the views of a single row. mAdapter.changeCursor(mCursor); mAdapter.onBindViewHolder(mViewHolder, 0); // There is one request for contact details. assertEquals(1, mAdapter.getContactInfoCache().requests.size()); TestContactInfoCache.Request request = mAdapter.getContactInfoCache().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); } @MediumTest public void testBindView_CallLogCacheButNoMemoryCache_EnqueueRequest() { createCallLogEntryWithCachedValues(false); // Bind the views of a single row. mAdapter.changeCursor(mCursor); mAdapter.onBindViewHolder(mViewHolder, 0); // There is one request for contact details. assertEquals(1, mAdapter.getContactInfoCache().requests.size()); TestContactInfoCache.Request request = mAdapter.getContactInfoCache().requests.get(0); // The values passed to the request, match the ones in the call log cache. assertEquals(TEST_CACHED_NAME, request.callLogInfo.name); assertEquals(TEST_CACHED_NUMBER_TYPE, request.callLogInfo.type); assertEquals(TEST_CACHED_NUMBER_LABEL, request.callLogInfo.label); } @MediumTest public void testBindView_NoCallLogButMemoryCache_EnqueueRequest() { createCallLogEntry(); mAdapter.injectContactInfoForTest(TEST_NUMBER, TEST_COUNTRY_ISO, createContactInfo()); // Bind the views of a single row. mAdapter.changeCursor(mCursor); mAdapter.onBindViewHolder(mViewHolder, 0); // There is one request for contact details. assertEquals(1, mAdapter.getContactInfoCache().requests.size()); TestContactInfoCache.Request request = mAdapter.getContactInfoCache().requests.get(0); // Since there is something in the cache, it is not an immediate request. assertFalse("should not be immediate", request.immediate); } @MediumTest public void testBindView_BothCallLogAndMemoryCache_NoEnqueueRequest() { createCallLogEntryWithCachedValues(true); // Bind the views of a single row. mAdapter.changeCursor(mCursor); mAdapter.onBindViewHolder(mViewHolder, 0); // Cache and call log are up-to-date: no need to request update. assertEquals(0, mAdapter.getContactInfoCache().requests.size()); } @MediumTest public void testBindView_MismatchBetweenCallLogAndMemoryCache_EnqueueRequest() { createCallLogEntryWithCachedValues(false); // 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.changeCursor(mCursor); mAdapter.onBindViewHolder(mViewHolder, 0); // There is one request for contact details. assertEquals(1, mAdapter.getContactInfoCache().requests.size()); TestContactInfoCache.Request request = mAdapter.getContactInfoCache().requests.get(0); // Since there is something in the cache, it is not an immediate request. assertFalse("should not be immediate", request.immediate); } @MediumTest public void testBindView_WithCachedName() { createCallLogEntryWithCachedValues( "John Doe", Phone.TYPE_HOME, TEST_CACHED_NUMBER_LABEL); mAdapter.changeCursor(mCursor); mAdapter.onBindViewHolder(mViewHolder, 0); assertNameIs(mViewHolder, "John Doe"); assertLabel(mViewHolder, TEST_FORMATTED_NUMBER, getTypeLabel(Phone.TYPE_HOME)); } @MediumTest public void testBindView_UriNumber() { createCallLogEntryWithCachedValues( "sip:johndoe@gmail.com", Calls.INCOMING_TYPE, "John Doe", Phone.TYPE_HOME, TEST_DEFAULT_CUSTOM_LABEL, EMPTY_STRING, false /* inject */); mAdapter.changeCursor(mCursor); mAdapter.onBindViewHolder(mViewHolder, 0); assertNameIs(mViewHolder, "John Doe"); assertLabel(mViewHolder, "sip:johndoe@gmail.com", "sip:johndoe@gmail.com"); } @MediumTest public void testBindView_HomeLabel() { createCallLogEntryWithCachedValues( "John Doe", Phone.TYPE_HOME, TEST_CACHED_NUMBER_LABEL); mAdapter.changeCursor(mCursor); mAdapter.onBindViewHolder(mViewHolder, 0); assertNameIs(mViewHolder, "John Doe"); assertLabel(mViewHolder, TEST_FORMATTED_NUMBER, getTypeLabel(Phone.TYPE_HOME)); } @MediumTest public void testBindView_WorkLabel() { createCallLogEntryWithCachedValues( "John Doe", Phone.TYPE_WORK, TEST_CACHED_NUMBER_LABEL); mAdapter.changeCursor(mCursor); mAdapter.onBindViewHolder(mViewHolder, 0); assertNameIs(mViewHolder, "John Doe"); assertLabel(mViewHolder, TEST_FORMATTED_NUMBER, getTypeLabel(Phone.TYPE_WORK)); } @MediumTest public void testBindView_CustomLabel() { createCallLogEntryWithCachedValues( "John Doe", Phone.TYPE_CUSTOM, TEST_DEFAULT_CUSTOM_LABEL); mAdapter.changeCursor(mCursor); mAdapter.onBindViewHolder(mViewHolder, 0); assertNameIs(mViewHolder, "John Doe"); assertLabel(mViewHolder, TEST_FORMATTED_NUMBER, TEST_DEFAULT_CUSTOM_LABEL); } @MediumTest public void testBindView_NumberOnlyDbCachedFormattedNumber() { createCallLogEntryWithCachedValues( TEST_NUMBER, Calls.INCOMING_TYPE, EMPTY_STRING, TEST_CACHED_NUMBER_TYPE, TEST_CACHED_NUMBER_LABEL, TEST_FORMATTED_NUMBER, false /* inject */); mAdapter.changeCursor(mCursor); mAdapter.onBindViewHolder(mViewHolder, 0); assertNameIs(mViewHolder, TEST_FORMATTED_NUMBER); } @MediumTest public void testBindVoicemailPromoCard() { createCallLogEntry(TEST_NUMBER_1); createCallLogEntry(TEST_NUMBER_1); createCallLogEntry(TEST_NUMBER_2); createCallLogEntry(TEST_NUMBER_2); createCallLogEntry(TEST_NUMBER_2); createCallLogEntry(TEST_NUMBER_3); // Bind the voicemail promo card. mAdapter.showVoicemailPromoCard(true); mAdapter.changeCursor(mCursor); mAdapter.onBindViewHolder(PromoCardViewHolder.createForTest(getContext()), 0); // Check that displaying the promo card does not affect the grouping or list display. mAdapter.onBindViewHolder(mViewHolder, 1); assertEquals(2, mAdapter.getGroupSize(1)); assertEquals(TEST_NUMBER_1, mViewHolder.number); mAdapter.onBindViewHolder(mViewHolder, 2); assertEquals(3, mAdapter.getGroupSize(2)); assertEquals(TEST_NUMBER_2, mViewHolder.number); mAdapter.onBindViewHolder(mViewHolder, 3); assertEquals(1, mAdapter.getGroupSize(3)); assertEquals(TEST_NUMBER_3, mViewHolder.number); } private void createCallLogEntry() { createCallLogEntry(TEST_NUMBER); } private void createCallLogEntry(String testNumber) { createCallLogEntry(testNumber, NO_VALUE_SET, NO_VALUE_SET); } private void createPrivateCallLogEntry() { createCallLogEntry(EMPTY_STRING, Calls.PRESENTATION_RESTRICTED, Calls.INCOMING_TYPE); } private void createUnknownCallLogEntry() { createCallLogEntry(EMPTY_STRING, Calls.PRESENTATION_UNKNOWN, Calls.INCOMING_TYPE); } private void createVoicemailCallLogEntry() { createCallLogEntry(TEST_NUMBER, NO_VALUE_SET, Calls.VOICEMAIL_TYPE); } private void createCallLogEntry(String number, int presentation, int type) { Object[] values = getValues(number, presentation, type); mCursor.addRow(values); } private void createCallLogEntryWithCachedValues(boolean inject) { createCallLogEntryWithCachedValues( TEST_NUMBER, NO_VALUE_SET, TEST_CACHED_NAME, TEST_CACHED_NUMBER_TYPE, TEST_CACHED_NUMBER_LABEL, EMPTY_STRING, inject); } private void createCallLogEntryWithCachedValues( String cachedName, int cachedNumberType, String cachedNumberLabel) { createCallLogEntryWithCachedValues( TEST_NUMBER, NO_VALUE_SET, cachedName, cachedNumberType, cachedNumberLabel, EMPTY_STRING, false /* inject */); } /** * Inserts a new call log entry * * It includes the values for the cached contact associated with the number. * * @param number The phone number. * @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. * @param cachedFormattedNumber The formatted number, from the contact with this number. * @param inject Whether to inject the contact info into the adapter's ContactInfoCache. */ private void createCallLogEntryWithCachedValues( String number, int type, String cachedName, int cachedNumberType, String cachedNumberLabel, String cachedFormattedNumber, boolean inject) { Object[] values = getValues(number, NO_VALUE_SET, type); values[CallLogQuery.CACHED_NAME] = cachedName; values[CallLogQuery.CACHED_NUMBER_TYPE] = cachedNumberType; values[CallLogQuery.CACHED_NUMBER_LABEL] = cachedNumberLabel; values[CallLogQuery.CACHED_FORMATTED_NUMBER] = cachedFormattedNumber; mCursor.addRow(values); if (inject) { ContactInfo contactInfo = createContactInfo(cachedName, cachedNumberType, cachedNumberLabel); mAdapter.injectContactInfoForTest(number, TEST_COUNTRY_ISO, contactInfo); } } /** * @param number The phone number. * @param presentation Number representing display rules for "allowed", * "payphone", "restricted", or "unknown". * @param date In millisec since epoch. Use NOW to use the current time. */ private Object[] getValues( String number, int presentation, int type) { Object[] values = CallLogQueryTestUtils.createTestValues(); values[CallLogQuery.ID] = mCursor.getCount(); values[CallLogQuery.COUNTRY_ISO] = TEST_COUNTRY_ISO; values[CallLogQuery.DATE] = new Date().getTime(); values[CallLogQuery.DURATION] = mRandom.nextInt(10 * 60); if (!TextUtils.isEmpty(number)) { values[CallLogQuery.NUMBER] = number; } if (presentation != NO_VALUE_SET) { values[CallLogQuery.NUMBER_PRESENTATION] = presentation; } if (type != NO_VALUE_SET) { values[CallLogQuery.CALL_TYPE] = type; } if (type == Calls.VOICEMAIL_TYPE) { values[CallLogQuery.VOICEMAIL_URI] = ContentUris.withAppendedId( VoicemailContract.Voicemails.CONTENT_URI, mCursor.getCount()); } return values; } private ContactInfo createContactInfo() { return createContactInfo( TEST_CACHED_NAME, TEST_CACHED_NUMBER_TYPE, TEST_CACHED_NUMBER_LABEL); } /** Returns a contact info with default values. */ private ContactInfo createContactInfo(String name, int type, String label) { ContactInfo info = new ContactInfo(); info.number = TEST_NUMBER; info.name = name; info.type = type; info.label = label; info.formattedNumber = TEST_FORMATTED_NUMBER; info.normalizedNumber = TEST_NUMBER; info.lookupUri = TEST_LOOKUP_URI; return info; } // Asserts that the name text view is shown and contains the given text. private void assertNameIs(CallLogListItemViewHolder viewHolder, String name) { assertEquals(View.VISIBLE, viewHolder.phoneCallDetailsViews.nameView.getVisibility()); assertEquals(name, viewHolder.phoneCallDetailsViews.nameView.getText()); } // Asserts that the label text view contains the given text. private void assertLabel( CallLogListItemViewHolder viewHolder, CharSequence number, CharSequence label) { if (label != null) { assertTrue(viewHolder.phoneCallDetailsViews.callLocationAndDate.getText() .toString().contains(label)); } } private void assertHasCallAction(CallLogListItemViewHolder viewHolder) { // The primaryActionView tag is set when the ViewHolder is binded. If it is possible // to place a call to the phone number, a call intent will have been created which // starts a phone call to the entry's number. IntentProvider intentProvider = (IntentProvider) viewHolder.primaryActionButtonView.getTag(); Intent intent = intentProvider.getIntent(getContext()); assertEquals(TestConstants.CALL_INTENT_ACTION, intent.getAction()); assertEquals(Uri.parse("tel:" + TEST_NUMBER), intent.getData()); } /** Returns the label associated with a given phone type. */ private CharSequence getTypeLabel(int phoneType) { return Phone.getTypeLabel(getContext().getResources(), phoneType, ""); } /// Subclass of {@link CallLogAdapter} used in tests to intercept certain calls. private static final class TestCallLogAdapter extends CallLogAdapter { public TestCallLogAdapter(Context context, CallFetcher callFetcher, ContactInfoHelper contactInfoHelper) { super(context, callFetcher, contactInfoHelper, null, false); mContactInfoCache = new TestContactInfoCache( contactInfoHelper, mOnContactInfoChangedListener); } public TestContactInfoCache getContactInfoCache() { return (TestContactInfoCache) mContactInfoCache; } public void showVoicemailPromoCard(boolean show) { mShowVoicemailPromoCard = show; } } private static final class TestContactInfoCache extends ContactInfoCache { 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 requests = Lists.newArrayList(); public TestContactInfoCache( ContactInfoHelper contactInfoHelper, OnContactInfoChangedListener listener) { super(contactInfoHelper, listener); } @Override protected void enqueueRequest(String number, String countryIso, ContactInfo callLogInfo, boolean immediate) { requests.add(new Request(number, countryIso, callLogInfo, immediate)); } } }