From 5df8de7b7c9ce0dee435d4a461dd9bbe6731e5d7 Mon Sep 17 00:00:00 2001 From: Tyler Gunn Date: Thu, 7 Apr 2016 09:53:20 -0700 Subject: Adding unit tests for ExternalCallList. - Also adding TestTelecomCall class, which is a reflection wrapper that can create instances of android.telecom.Call. These calls are particularly problematic as they are final, so Mockito cannot mock them. Bug: 27458894 Change-Id: Ibd3786dc27eb4280eb32b8fb0baa18d42738a98c --- .../com/android/incallui/ExternalCallListTest.java | 142 ++++++++++++++ .../android/incallui/ExternalCallNotifierTest.java | 212 +++++++++++++++++++++ .../src/com/android/incallui/TestTelecomCall.java | 161 ++++++++++++++++ 3 files changed, 515 insertions(+) create mode 100644 InCallUI/tests/src/com/android/incallui/ExternalCallListTest.java create mode 100644 InCallUI/tests/src/com/android/incallui/ExternalCallNotifierTest.java create mode 100644 InCallUI/tests/src/com/android/incallui/TestTelecomCall.java (limited to 'InCallUI/tests') diff --git a/InCallUI/tests/src/com/android/incallui/ExternalCallListTest.java b/InCallUI/tests/src/com/android/incallui/ExternalCallListTest.java new file mode 100644 index 000000000..070bdf522 --- /dev/null +++ b/InCallUI/tests/src/com/android/incallui/ExternalCallListTest.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2016 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.incallui; + +import android.content.ComponentName; +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.telecom.*; +import android.telecom.Call; +import android.test.AndroidTestCase; + +import java.lang.reflect.Constructor; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class ExternalCallListTest extends AndroidTestCase { + + private static class Listener implements ExternalCallList.ExternalCallListener { + private CountDownLatch mCallAddedLatch = new CountDownLatch(1); + private CountDownLatch mCallRemovedLatch = new CountDownLatch(1); + private CountDownLatch mCallUpdatedLatch = new CountDownLatch(1); + + @Override + public void onExternalCallAdded(Call call) { + mCallAddedLatch.countDown(); + } + + @Override + public void onExternalCallRemoved(Call call) { + mCallRemovedLatch.countDown(); + } + + @Override + public void onExternalCallUpdated(Call call) { + mCallUpdatedLatch.countDown(); + } + + public boolean awaitCallAdded() { + try { + return mCallAddedLatch.await(WAIT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + return false; + } + } + + public boolean awaitCallRemoved() { + try { + return mCallRemovedLatch.await(WAIT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + return false; + } + } + + public boolean awaitCallUpdated() { + try { + return mCallUpdatedLatch.await(WAIT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + return false; + } + } + } + + private static final int WAIT_TIMEOUT_MILLIS = 5000; + + private ExternalCallList mExternalCallList = new ExternalCallList(); + private Listener mExternalCallListener = new Listener(); + + @Override + public void setUp() throws Exception { + super.setUp(); + mExternalCallList.addExternalCallListener(mExternalCallListener); + } + + public void testAddCallSuccess() { + TestTelecomCall call = getTestCall(Call.Details.PROPERTY_IS_EXTERNAL_CALL); + mExternalCallList.onCallAdded(call.getCall()); + assertTrue(mExternalCallListener.awaitCallAdded()); + } + + public void testAddCallFail() { + TestTelecomCall call = getTestCall(0 /* no properties */); + try { + mExternalCallList.onCallAdded(call.getCall()); + fail(); + } catch (IllegalArgumentException e) { + } + } + + public void testUpdateCall() { + TestTelecomCall call = getTestCall(Call.Details.PROPERTY_IS_EXTERNAL_CALL); + mExternalCallList.onCallAdded(call.getCall()); + assertTrue(mExternalCallListener.awaitCallAdded()); + + call.forceDetailsUpdate(); + assertTrue(mExternalCallListener.awaitCallUpdated()); + } + + public void testRemoveCall() { + TestTelecomCall call = getTestCall(Call.Details.PROPERTY_IS_EXTERNAL_CALL); + mExternalCallList.onCallAdded(call.getCall()); + assertTrue(mExternalCallListener.awaitCallAdded()); + + mExternalCallList.onCallRemoved(call.getCall()); + assertTrue(mExternalCallListener.awaitCallRemoved()); + } + + private TestTelecomCall getTestCall(int properties) { + TestTelecomCall testCall = TestTelecomCall.createInstance( + "1", + Uri.parse("tel:650-555-1212"), /* handle */ + TelecomManager.PRESENTATION_ALLOWED, /* handlePresentation */ + "Joe", /* callerDisplayName */ + TelecomManager.PRESENTATION_ALLOWED, /* callerDisplayNamePresentation */ + new PhoneAccountHandle(new ComponentName("test", "class"), + "handle"), /* accountHandle */ + Call.Details.CAPABILITY_CAN_PULL_CALL, /* capabilities */ + properties, /* properties */ + null, /* disconnectCause */ + 0, /* connectTimeMillis */ + null, /* GatewayInfo */ + VideoProfile.STATE_AUDIO_ONLY, /* videoState */ + null, /* statusHints */ + null, /* extras */ + null /* intentExtras */); + return testCall; + } +} diff --git a/InCallUI/tests/src/com/android/incallui/ExternalCallNotifierTest.java b/InCallUI/tests/src/com/android/incallui/ExternalCallNotifierTest.java new file mode 100644 index 000000000..e57efef67 --- /dev/null +++ b/InCallUI/tests/src/com/android/incallui/ExternalCallNotifierTest.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2016 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.incallui; + +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyBoolean; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.android.contacts.common.preference.ContactsPreferences; + +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import android.app.Notification; +import android.app.NotificationManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.res.Resources; +import android.net.Uri; +import android.telecom.*; +import android.telecom.Call; +import android.telephony.TelephonyManager; +import android.test.AndroidTestCase; +import android.test.mock.MockContext; + +/** + * Unit tests for {@link ExternalCallNotifier}. + */ +public class ExternalCallNotifierTest extends AndroidTestCase { + private static final int TIMEOUT_MILLIS = 5000; + private static final String NAME_PRIMARY = "Full Name"; + private static final String NAME_ALTERNATIVE = "Name, Full"; + private static final String LOCATION = "US"; + private static final String NUMBER = "6505551212"; + + @Mock private ContactsPreferences mContactsPreferences; + @Mock private NotificationManager mNotificationManager; + @Mock private MockContext mMockContext; + @Mock private Resources mResources; + @Mock private StatusBarNotifier mStatusBarNotifier; + @Mock private ContactInfoCache mContactInfoCache; + @Mock private TelecomManager mTelecomManager; + @Mock private TelephonyManager mTelephonyManager; + @Mock private ProximitySensor mProximitySensor; + @Mock private CallList mCallList; + private InCallPresenter mInCallPresenter; + private ExternalCallNotifier mExternalCallNotifier; + private ContactInfoCache.ContactCacheEntry mContactInfo; + + @Override + public void setUp() throws Exception { + super.setUp(); + MockitoAnnotations.initMocks(this); + + when(mContactsPreferences.getDisplayOrder()) + .thenReturn(ContactsPreferences.DISPLAY_ORDER_PRIMARY); + + // Setup the mock context to return mocks for some of the needed services; the notification + // service is especially important as we want to be able to intercept calls into it and + // validate the notifcations. + when(mMockContext.getSystemService(eq(Context.NOTIFICATION_SERVICE))) + .thenReturn(mNotificationManager); + when(mMockContext.getSystemService(eq(Context.TELECOM_SERVICE))) + .thenReturn(mTelecomManager); + when(mMockContext.getSystemService(eq(Context.TELEPHONY_SERVICE))) + .thenReturn(mTelephonyManager); + + // These aspects of the context are used by the notification builder to build the actual + // notification; we will rely on the actual implementations of these. + when(mMockContext.getPackageManager()).thenReturn(mContext.getPackageManager()); + when(mMockContext.getResources()).thenReturn(mContext.getResources()); + when(mMockContext.getApplicationInfo()).thenReturn(mContext.getApplicationInfo()); + when(mMockContext.getContentResolver()).thenReturn(mContext.getContentResolver()); + when(mMockContext.getPackageName()).thenReturn(mContext.getPackageName()); + + ContactsPreferencesFactory.setTestInstance(null); + mExternalCallNotifier = new ExternalCallNotifier(mMockContext, mContactInfoCache); + + // We don't directly use the InCallPresenter in the test, or even in ExternalCallNotifier + // itself. However, ExternalCallNotifier needs to make instances of + // com.android.incallui.Call for the purpose of performing contact cache lookups. The + // Call class depends on the static InCallPresenter for a number of things, so we need to + // set it up here to prevent crashes. + mInCallPresenter = InCallPresenter.getInstance(); + mInCallPresenter.setUp(mMockContext, mCallList, new ExternalCallList(), + null, mStatusBarNotifier, mExternalCallNotifier, mContactInfoCache, + mProximitySensor); + + // Unlocked all contact info is available + mContactInfo = new ContactInfoCache.ContactCacheEntry(); + mContactInfo.namePrimary = NAME_PRIMARY; + mContactInfo.nameAlternative = NAME_ALTERNATIVE; + mContactInfo.location = LOCATION; + mContactInfo.number = NUMBER; + + // Given the mock ContactInfoCache cache, we need to mock out what happens when the + // ExternalCallNotifier calls into the contact info cache to do a lookup. We will always + // return mock info stored in mContactInfo. + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + com.android.incallui.Call call = (com.android.incallui.Call) args[0]; + ContactInfoCache.ContactInfoCacheCallback callback + = (ContactInfoCache.ContactInfoCacheCallback) args[2]; + callback.onContactInfoComplete(call.getId(), mContactInfo); + return null; + } + }).when(mContactInfoCache).findInfo(any(com.android.incallui.Call.class), anyBoolean(), + any(ContactInfoCache.ContactInfoCacheCallback.class)); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + ContactsPreferencesFactory.setTestInstance(null); + mInCallPresenter.tearDown(); + } + + public void testPostNonPullable() { + TestTelecomCall call = getTestCall(false); + mExternalCallNotifier.onExternalCallAdded(call.getCall()); + Notification notification = verifyNotificationPosted(); + assertNull(notification.actions); + } + + public void testPostPullable() { + TestTelecomCall call = getTestCall(true); + mExternalCallNotifier.onExternalCallAdded(call.getCall()); + Notification notification = verifyNotificationPosted(); + assertEquals(1, notification.actions.length); + } + + public void testNotificationDismissed() { + TestTelecomCall call = getTestCall(false); + mExternalCallNotifier.onExternalCallAdded(call.getCall()); + verifyNotificationPosted(); + + mExternalCallNotifier.onExternalCallRemoved(call.getCall()); + verify(mNotificationManager, timeout(TIMEOUT_MILLIS)).cancel(eq("EXTERNAL_CALL"), eq(0)); + } + + public void testNotificationUpdated() { + TestTelecomCall call = getTestCall(false); + mExternalCallNotifier.onExternalCallAdded(call.getCall()); + verifyNotificationPosted(); + + call.setCapabilities(android.telecom.Call.Details.CAPABILITY_CAN_PULL_CALL); + mExternalCallNotifier.onExternalCallUpdated(call.getCall()); + + ArgumentCaptor notificationCaptor = + ArgumentCaptor.forClass(Notification.class); + verify(mNotificationManager, timeout(TIMEOUT_MILLIS).times(2)) + .notify(eq("EXTERNAL_CALL"), eq(0), notificationCaptor.capture()); + Notification notification1 = notificationCaptor.getAllValues().get(0); + assertNull(notification1.actions); + Notification notification2 = notificationCaptor.getAllValues().get(1); + assertEquals(1, notification2.actions.length); + } + + private Notification verifyNotificationPosted() { + ArgumentCaptor notificationCaptor = + ArgumentCaptor.forClass(Notification.class); + verify(mNotificationManager, timeout(TIMEOUT_MILLIS)) + .notify(eq("EXTERNAL_CALL"), eq(0), notificationCaptor.capture()); + return notificationCaptor.getValue(); + } + + private TestTelecomCall getTestCall(boolean canPull) { + TestTelecomCall testCall = TestTelecomCall.createInstance( + "1", + Uri.parse("tel:650-555-1212"), /* handle */ + TelecomManager.PRESENTATION_ALLOWED, /* handlePresentation */ + "Joe", /* callerDisplayName */ + TelecomManager.PRESENTATION_ALLOWED, /* callerDisplayNamePresentation */ + new PhoneAccountHandle(new ComponentName("test", "class"), + "handle"), /* accountHandle */ + canPull ? android.telecom.Call.Details.CAPABILITY_CAN_PULL_CALL : 0, /* capabilities */ + Call.Details.PROPERTY_IS_EXTERNAL_CALL, /* properties */ + null, /* disconnectCause */ + 0, /* connectTimeMillis */ + null, /* GatewayInfo */ + VideoProfile.STATE_AUDIO_ONLY, /* videoState */ + null, /* statusHints */ + null, /* extras */ + null /* intentExtras */); + return testCall; + } +} diff --git a/InCallUI/tests/src/com/android/incallui/TestTelecomCall.java b/InCallUI/tests/src/com/android/incallui/TestTelecomCall.java new file mode 100644 index 000000000..48ac6e18f --- /dev/null +++ b/InCallUI/tests/src/com/android/incallui/TestTelecomCall.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2016 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.incallui; + +import com.google.common.base.Preconditions; + +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.telecom.DisconnectCause; +import android.telecom.GatewayInfo; +import android.telecom.PhoneAccountHandle; +import android.telecom.StatusHints; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Wrapper class which uses reflection to create instances of {@link android.telecom.Call} for use + * with unit testing. Since {@link android.telecom.Call} is final, it cannot be mocked using + * mockito, and since all setter methods are hidden, it is necessary to use reflection. In the + * future, it would be desirable to replace this if a different mocking solution is used. + */ +public class TestTelecomCall { + + private android.telecom.Call mCall; + + public static @Nullable TestTelecomCall createInstance(String callId, + Uri handle, + int handlePresentation, + String callerDisplayName, + int callerDisplayNamePresentation, + PhoneAccountHandle accountHandle, + int capabilities, + int properties, + DisconnectCause disconnectCause, + long connectTimeMillis, + GatewayInfo gatewayInfo, + int videoState, + StatusHints statusHints, + Bundle extras, + Bundle intentExtras) { + + try { + // Phone and InCall adapter are @hide, so we cannot refer to them directly. + Class phoneClass = Class.forName("android.telecom.Phone"); + Class incallAdapterClass = Class.forName("android.telecom.InCallAdapter"); + Class callClass = android.telecom.Call.class; + Constructor cons = callClass + .getDeclaredConstructor(phoneClass, String.class, incallAdapterClass); + cons.setAccessible(true); + + android.telecom.Call call = (android.telecom.Call) cons.newInstance(null, callId, null); + + // Create an instance of the call details. + Class callDetailsClass = android.telecom.Call.Details.class; + Constructor detailsCons = callDetailsClass.getDeclaredConstructor( + String.class, /* telecomCallId */ + Uri.class, /* handle */ + int.class, /* handlePresentation */ + String.class, /* callerDisplayName */ + int.class, /* callerDisplayNamePresentation */ + PhoneAccountHandle.class, /* accountHandle */ + int.class, /* capabilities */ + int.class, /* properties */ + DisconnectCause.class, /* disconnectCause */ + long.class, /* connectTimeMillis */ + GatewayInfo.class, /* gatewayInfo */ + int.class, /* videoState */ + StatusHints.class, /* statusHints */ + Bundle.class, /* extras */ + Bundle.class /* intentExtras */); + detailsCons.setAccessible(true); + + android.telecom.Call.Details details = (android.telecom.Call.Details) + detailsCons.newInstance(callId, handle, handlePresentation, callerDisplayName, + callerDisplayNamePresentation, accountHandle, capabilities, properties, + disconnectCause, connectTimeMillis, gatewayInfo, videoState, + statusHints, + extras, intentExtras); + + // Finally, set this as the details of the call. + Field detailsField = call.getClass().getDeclaredField("mDetails"); + detailsField.setAccessible(true); + detailsField.set(call, details); + + return new TestTelecomCall(call); + } catch (NoSuchMethodException nsm) { + return null; + } catch (ClassNotFoundException cnf) { + return null; + } catch (IllegalAccessException e) { + return null; + } catch (InstantiationException e) { + return null; + } catch (InvocationTargetException e) { + return null; + } catch (NoSuchFieldException e) { + return null; + } + } + + private TestTelecomCall(android.telecom.Call call) { + mCall = call; + } + + public android.telecom.Call getCall() { + return mCall; + } + + public void forceDetailsUpdate() { + Preconditions.checkNotNull(mCall); + + try { + Method method = mCall.getClass().getDeclaredMethod("fireDetailsChanged", + android.telecom.Call.Details.class); + method.setAccessible(true); + method.invoke(mCall, mCall.getDetails()); + } catch (NoSuchMethodException e) { + Log.e(this, "forceDetailsUpdate", e); + } catch (InvocationTargetException e) { + Log.e(this, "forceDetailsUpdate", e); + } catch (IllegalAccessException e) { + Log.e(this, "forceDetailsUpdate", e); + } + } + + public void setCapabilities(int capabilities) { + Preconditions.checkNotNull(mCall); + try { + Field field = mCall.getDetails().getClass().getDeclaredField("mCallCapabilities"); + field.setAccessible(true); + field.set(mCall.getDetails(), capabilities); + } catch (IllegalAccessException e) { + Log.e(this, "setProperties", e); + } catch (NoSuchFieldException e) { + Log.e(this, "setProperties", e); + } + } + + public void setCall(android.telecom.Call call) { + mCall = call; + } +} -- cgit v1.2.3