blob: 439b37790d9b88b862bf2092645fd37501631231 [file] [log] [blame]
/*
* Copyright (C) 2015 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 android.telecom.cts;
import static android.telecom.cts.TestUtils.PACKAGE;
import static android.telecom.cts.TestUtils.TAG;
import static android.telecom.cts.TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.not;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThat;
import android.app.AppOpsManager;
import android.app.UiAutomation;
import android.app.UiModeManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.database.Cursor;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.RemoteException;
import android.os.Process;
import android.os.UserHandle;
import android.provider.CallLog;
import android.telecom.Call;
import android.telecom.CallAudioState;
import android.telecom.Conference;
import android.telecom.Connection;
import android.telecom.ConnectionRequest;
import android.telecom.InCallService;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
import android.telecom.cts.MockInCallService.InCallServiceCallbacks;
import android.telecom.cts.carmodetestapp.ICtsCarModeInCallServiceControl;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyCallback;
import android.telephony.TelephonyManager;
import android.telephony.emergency.EmergencyNumber;
import android.test.InstrumentationTestCase;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import androidx.test.InstrumentationRegistry;
import com.android.compatibility.common.util.ShellIdentityUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* Base class for Telecom CTS tests that require a {@link CtsConnectionService} and
* {@link MockInCallService} to verify Telecom functionality.
*/
public class BaseTelecomTestWithMockServices extends InstrumentationTestCase {
public static final int FLAG_REGISTER = 0x1;
public static final int FLAG_ENABLE = 0x2;
public static final int FLAG_SET_DEFAULT = 0x4;
// Don't accidently use emergency number.
private static int sCounter = 5553638;
public static final String TEST_EMERGENCY_NUMBER = "5553637";
public static final Uri TEST_EMERGENCY_URI = Uri.fromParts("tel", TEST_EMERGENCY_NUMBER, null);
public static final String PKG_NAME = "android.telecom.cts";
public static final String PERMISSION_PROCESS_OUTGOING_CALLS =
"android.permission.PROCESS_OUTGOING_CALLS";
Context mContext;
TelecomManager mTelecomManager;
TelephonyManager mTelephonyManager;
UiModeManager mUiModeManager;
TestUtils.InvokeCounter mOnBringToForegroundCounter;
TestUtils.InvokeCounter mOnCallAudioStateChangedCounter;
TestUtils.InvokeCounter mOnPostDialWaitCounter;
TestUtils.InvokeCounter mOnCannedTextResponsesLoadedCounter;
TestUtils.InvokeCounter mOnSilenceRingerCounter;
TestUtils.InvokeCounter mOnConnectionEventCounter;
TestUtils.InvokeCounter mOnExtrasChangedCounter;
TestUtils.InvokeCounter mOnPropertiesChangedCounter;
TestUtils.InvokeCounter mOnRttModeChangedCounter;
TestUtils.InvokeCounter mOnRttStatusChangedCounter;
TestUtils.InvokeCounter mOnRttInitiationFailedCounter;
TestUtils.InvokeCounter mOnRttRequestCounter;
TestUtils.InvokeCounter mOnHandoverCompleteCounter;
TestUtils.InvokeCounter mOnHandoverFailedCounter;
TestUtils.InvokeCounter mOnPhoneAccountChangedCounter;
Bundle mPreviousExtras;
int mPreviousProperties = -1;
PhoneAccountHandle mPreviousPhoneAccountHandle = null;
InCallServiceCallbacks mInCallCallbacks;
String mPreviousDefaultDialer = null;
PhoneAccountHandle mPreviousDefaultOutgoingAccount = null;
boolean mShouldRestoreDefaultOutgoingAccount = false;
MockConnectionService connectionService = null;
boolean mIsEmergencyCallingSetup = false;
HandlerThread mTelephonyCallbackThread;
Handler mTelephonyCallbackHandler;
TestTelephonyCallback mTelephonyCallback;
TestCallStateListener mTestCallStateListener;
Handler mHandler;
/**
* Uses the control interface to disable car mode.
* @param expectedUiMode
*/
protected void disableAndVerifyCarMode(ICtsCarModeInCallServiceControl control,
int expectedUiMode) {
if (control == null) {
return;
}
try {
control.disableCarMode();
} catch (RemoteException re) {
fail("Bee-boop; can't control the incall service");
}
assertUiMode(expectedUiMode);
}
protected void disconnectAllCallsAndVerify(ICtsCarModeInCallServiceControl controlBinder) {
if (controlBinder == null) {
return;
}
try {
controlBinder.disconnectCalls();
} catch (RemoteException re) {
fail("Bee-boop; can't control the incall service");
}
assertCarModeCallCount(controlBinder, 0);
}
/**
* Verify the car mode ICS has an expected call count.
* @param expected
*/
protected void assertCarModeCallCount(ICtsCarModeInCallServiceControl control, int expected) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return expected;
}
@Override
public Object actual() {
int callCount = 0;
try {
callCount = control.getCallCount();
} catch (RemoteException re) {
fail("Bee-boop; can't control the incall service");
}
return callCount;
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Expected " + expected + " calls."
);
}
static class TestCallStateListener extends TelephonyCallback
implements TelephonyCallback.CallStateListener {
private CountDownLatch mCountDownLatch = new CountDownLatch(1);
private int mLastState = -1;
@Override
public void onCallStateChanged(int state) {
Log.i(TAG, "onCallStateChanged: state=" + state);
mLastState = state;
mCountDownLatch.countDown();
mCountDownLatch = new CountDownLatch(1);
}
public CountDownLatch getCountDownLatch() {
return mCountDownLatch;
}
public int getLastState() {
return mLastState;
}
}
static class TestTelephonyCallback extends TelephonyCallback implements
TelephonyCallback.CallStateListener,
TelephonyCallback.OutgoingEmergencyCallListener,
TelephonyCallback.EmergencyNumberListListener {
/** Semaphore released for every callback invocation. */
public Semaphore mCallbackSemaphore = new Semaphore(0);
List<Integer> mCallStates = new ArrayList<>();
EmergencyNumber mLastOutgoingEmergencyNumber;
LinkedBlockingQueue<Map<Integer, List<EmergencyNumber>>> mEmergencyNumberListQueue =
new LinkedBlockingQueue<>(2);
@Override
public void onCallStateChanged(int state) {
Log.i(TAG, "onCallStateChanged: state=" + state);
mCallStates.add(state);
mCallbackSemaphore.release();
}
@Override
public void onOutgoingEmergencyCall(EmergencyNumber emergencyNumber, int subscriptionId) {
Log.i(TAG, "onOutgoingEmergencyCall: emergencyNumber=" + emergencyNumber);
mLastOutgoingEmergencyNumber = emergencyNumber;
mCallbackSemaphore.release();
}
@Override
public void onEmergencyNumberListChanged(
Map<Integer, List<EmergencyNumber>> emergencyNumberList) {
Log.i(TAG, "onEmergencyNumberChanged, total size=" + emergencyNumberList.values()
.stream().mapToInt(List::size).sum());
mEmergencyNumberListQueue.offer(emergencyNumberList);
}
public Map<Integer, List<EmergencyNumber>> waitForEmergencyNumberListUpdate(
long timeoutMillis) throws Throwable {
return mEmergencyNumberListQueue.poll(timeoutMillis, TimeUnit.MILLISECONDS);
}
}
boolean mShouldTestTelecom = true;
@Override
protected void setUp() throws Exception {
super.setUp();
mContext = getInstrumentation().getContext();
mHandler = new Handler(Looper.getMainLooper());
mShouldTestTelecom = TestUtils.shouldTestTelecom(mContext);
if (!mShouldTestTelecom) {
return;
}
// Assume we start in normal mode at the start of all Telecom tests; a failure to leave car
// mode in any of the tests would cause subsequent test failures.
// For Watch, UI_MODE shouldn't be normal mode.
mUiModeManager = mContext.getSystemService(UiModeManager.class);
TestUtils.executeShellCommand(getInstrumentation(), "telecom reset-car-mode");
if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) {
assertUiMode(Configuration.UI_MODE_TYPE_WATCH);
} else {
assertUiMode(Configuration.UI_MODE_TYPE_NORMAL);
}
AppOpsManager aom = mContext.getSystemService(AppOpsManager.class);
ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(aom,
(appOpsMan) -> appOpsMan.setUidMode(AppOpsManager.OPSTR_PROCESS_OUTGOING_CALLS,
Process.myUid(), AppOpsManager.MODE_ALLOWED));
mTelecomManager = (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE);
mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
mPreviousDefaultDialer = TestUtils.getDefaultDialer(getInstrumentation());
TestUtils.setDefaultDialer(getInstrumentation(), PACKAGE);
setupCallbacks();
// Register a call state listener.
mTestCallStateListener = new TestCallStateListener();
CountDownLatch latch = mTestCallStateListener.getCountDownLatch();
mTelephonyManager.registerTelephonyCallback(r -> r.run(), mTestCallStateListener);
latch.await(
TestUtils.WAIT_FOR_PHONE_STATE_LISTENER_REGISTERED_TIMEOUT_S, TimeUnit.SECONDS);
// Create a new thread for the telephony callback.
mTelephonyCallbackThread = new HandlerThread("PhoneStateListenerThread");
mTelephonyCallbackThread.start();
mTelephonyCallbackHandler = new Handler(mTelephonyCallbackThread.getLooper());
mTelephonyCallback = new TestTelephonyCallback();
ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(mTelephonyManager,
(tm) -> tm.registerTelephonyCallback(
mTelephonyCallbackHandler::post,
mTelephonyCallback));
UiAutomation uiAutomation =
InstrumentationRegistry.getInstrumentation().getUiAutomation();
uiAutomation.grantRuntimePermissionAsUser(PKG_NAME, PERMISSION_PROCESS_OUTGOING_CALLS,
UserHandle.CURRENT);
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
if (!mShouldTestTelecom) {
return;
}
mTelephonyManager.unregisterTelephonyCallback(mTestCallStateListener);
mTelephonyManager.unregisterTelephonyCallback(mTelephonyCallback);
mTelephonyCallbackThread.quit();
cleanupCalls();
if (!TextUtils.isEmpty(mPreviousDefaultDialer)) {
TestUtils.setDefaultDialer(getInstrumentation(), mPreviousDefaultDialer);
}
tearDownConnectionService(TestUtils.TEST_PHONE_ACCOUNT_HANDLE);
tearDownEmergencyCalling();
try {
assertMockInCallServiceUnbound();
} catch (Throwable t) {
// If we haven't unbound, that means there's some dirty state in Telecom that needs
// cleaning up. Forcibly unbind and clean up Telecom state so that we don't have a
// cascading failure of tests.
TestUtils.executeShellCommand(getInstrumentation(), "telecom cleanup-stuck-calls");
throw t;
}
UiAutomation uiAutomation =
InstrumentationRegistry.getInstrumentation().getUiAutomation();
uiAutomation.revokeRuntimePermissionAsUser(PKG_NAME, PERMISSION_PROCESS_OUTGOING_CALLS,
UserHandle.CURRENT);
}
protected PhoneAccount setupConnectionService(MockConnectionService connectionService,
int flags) throws Exception {
Log.i(TAG, "Setting up mock connection service");
if (connectionService != null) {
this.connectionService = connectionService;
} else {
// Generate a vanilla mock connection service, if not provided.
this.connectionService = new MockConnectionService();
}
CtsConnectionService.setUp(this.connectionService);
if ((flags & FLAG_REGISTER) != 0) {
mTelecomManager.registerPhoneAccount(TestUtils.TEST_PHONE_ACCOUNT);
}
if ((flags & FLAG_ENABLE) != 0) {
TestUtils.enablePhoneAccount(getInstrumentation(), TestUtils.TEST_PHONE_ACCOUNT_HANDLE);
// Wait till the adb commands have executed and account is enabled in Telecom database.
assertPhoneAccountEnabled(TestUtils.TEST_PHONE_ACCOUNT_HANDLE);
}
if ((flags & FLAG_SET_DEFAULT) != 0) {
mPreviousDefaultOutgoingAccount = mTelecomManager.getUserSelectedOutgoingPhoneAccount();
mShouldRestoreDefaultOutgoingAccount = true;
TestUtils.setDefaultOutgoingPhoneAccount(getInstrumentation(),
TestUtils.TEST_PHONE_ACCOUNT_HANDLE);
// Wait till the adb commands have executed and the default has changed.
assertPhoneAccountIsDefault(TestUtils.TEST_PHONE_ACCOUNT_HANDLE);
}
return TestUtils.TEST_PHONE_ACCOUNT;
}
protected void tearDownConnectionService(PhoneAccountHandle accountHandle) throws Exception {
Log.i(TAG, "Tearing down mock connection service");
if (this.connectionService != null) {
assertNumConnections(this.connectionService, 0);
}
mTelecomManager.unregisterPhoneAccount(accountHandle);
CtsConnectionService.tearDown();
assertCtsConnectionServiceUnbound();
if (mShouldRestoreDefaultOutgoingAccount) {
TestUtils.setDefaultOutgoingPhoneAccount(getInstrumentation(),
mPreviousDefaultOutgoingAccount);
}
this.connectionService = null;
mPreviousDefaultOutgoingAccount = null;
mShouldRestoreDefaultOutgoingAccount = false;
}
protected void setupForEmergencyCalling(String testNumber) throws Exception {
TestUtils.setSystemDialerOverride(getInstrumentation());
TestUtils.addTestEmergencyNumber(getInstrumentation(), testNumber);
TestUtils.setTestEmergencyPhoneAccountPackageFilter(getInstrumentation(), mContext);
// Emergency calls require special capabilities.
TestUtils.registerEmergencyPhoneAccount(getInstrumentation(),
TestUtils.TEST_EMERGENCY_PHONE_ACCOUNT_HANDLE,
TestUtils.ACCOUNT_LABEL + "E", "tel:555-EMER");
mIsEmergencyCallingSetup = true;
}
protected void tearDownEmergencyCalling() throws Exception {
if (!mIsEmergencyCallingSetup) return;
TestUtils.clearSystemDialerOverride(getInstrumentation());
TestUtils.clearTestEmergencyNumbers(getInstrumentation());
TestUtils.clearTestEmergencyPhoneAccountPackageFilter(getInstrumentation());
mTelecomManager.unregisterPhoneAccount(TestUtils.TEST_EMERGENCY_PHONE_ACCOUNT_HANDLE);
}
protected void startCallTo(Uri address, PhoneAccountHandle accountHandle) {
final Intent intent = new Intent(Intent.ACTION_CALL, address);
if (accountHandle != null) {
intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, accountHandle);
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
}
void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
}
}
private void setupCallbacks() {
mInCallCallbacks = new InCallServiceCallbacks() {
@Override
public void onCallAdded(Call call, int numCalls) {
Log.i(TAG, "onCallAdded, Call: " + call + ", Num Calls: " + numCalls);
this.lock.release();
mPreviousPhoneAccountHandle = call.getDetails().getAccountHandle();
}
@Override
public void onCallRemoved(Call call, int numCalls) {
Log.i(TAG, "onCallRemoved, Call: " + call + ", Num Calls: " + numCalls);
}
@Override
public void onParentChanged(Call call, Call parent) {
Log.i(TAG, "onParentChanged, Call: " + call + ", Parent: " + parent);
this.lock.release();
}
@Override
public void onChildrenChanged(Call call, List<Call> children) {
Log.i(TAG, "onChildrenChanged, Call: " + call + "Children: " + children);
this.lock.release();
}
@Override
public void onConferenceableCallsChanged(Call call, List<Call> conferenceableCalls) {
Log.i(TAG, "onConferenceableCallsChanged, Call: " + call + ", Conferenceables: " +
conferenceableCalls);
}
@Override
public void onDetailsChanged(Call call, Call.Details details) {
Log.i(TAG, "onDetailsChanged, Call: " + call + ", Details: " + details);
if (!areBundlesEqual(mPreviousExtras, details.getExtras())) {
mOnExtrasChangedCounter.invoke(call, details);
}
mPreviousExtras = details.getExtras();
if (mPreviousProperties != details.getCallProperties()) {
mOnPropertiesChangedCounter.invoke(call, details);
Log.i(TAG, "onDetailsChanged; properties changed from " + Call.Details.propertiesToString(mPreviousProperties) +
" to " + Call.Details.propertiesToString(details.getCallProperties()));
}
mPreviousProperties = details.getCallProperties();
if (details.getAccountHandle() != null &&
!details.getAccountHandle().equals(mPreviousPhoneAccountHandle)) {
mOnPhoneAccountChangedCounter.invoke(call, details.getAccountHandle());
}
mPreviousPhoneAccountHandle = details.getAccountHandle();
}
@Override
public void onCallDestroyed(Call call) {
Log.i(TAG, "onCallDestroyed, Call: " + call);
}
@Override
public void onCallStateChanged(Call call, int newState) {
Log.i(TAG, "onCallStateChanged, Call: " + call + ", New State: " + newState);
}
@Override
public void onBringToForeground(boolean showDialpad) {
mOnBringToForegroundCounter.invoke(showDialpad);
}
@Override
public void onCallAudioStateChanged(CallAudioState audioState) {
Log.i(TAG, "onCallAudioStateChanged, audioState: " + audioState);
mOnCallAudioStateChangedCounter.invoke(audioState);
}
@Override
public void onPostDialWait(Call call, String remainingPostDialSequence) {
mOnPostDialWaitCounter.invoke(call, remainingPostDialSequence);
}
@Override
public void onCannedTextResponsesLoaded(Call call, List<String> cannedTextResponses) {
mOnCannedTextResponsesLoadedCounter.invoke(call, cannedTextResponses);
}
@Override
public void onConnectionEvent(Call call, String event, Bundle extras) {
mOnConnectionEventCounter.invoke(call, event, extras);
}
@Override
public void onSilenceRinger() {
Log.i(TAG, "onSilenceRinger");
mOnSilenceRingerCounter.invoke();
}
@Override
public void onRttModeChanged(Call call, int mode) {
mOnRttModeChangedCounter.invoke(call, mode);
}
@Override
public void onRttStatusChanged(Call call, boolean enabled, Call.RttCall rttCall) {
mOnRttStatusChangedCounter.invoke(call, enabled, rttCall);
}
@Override
public void onRttRequest(Call call, int id) {
mOnRttRequestCounter.invoke(call, id);
}
@Override
public void onRttInitiationFailure(Call call, int reason) {
mOnRttInitiationFailedCounter.invoke(call, reason);
}
@Override
public void onHandoverComplete(Call call) {
mOnHandoverCompleteCounter.invoke(call);
}
@Override
public void onHandoverFailed(Call call, int reason) {
mOnHandoverFailedCounter.invoke(call, reason);
}
};
MockInCallService.setCallbacks(mInCallCallbacks);
// TODO: If more InvokeCounters are added in the future, consider consolidating them into a
// single Collection.
mOnBringToForegroundCounter = new TestUtils.InvokeCounter("OnBringToForeground");
mOnCallAudioStateChangedCounter = new TestUtils.InvokeCounter("OnCallAudioStateChanged");
mOnPostDialWaitCounter = new TestUtils.InvokeCounter("OnPostDialWait");
mOnCannedTextResponsesLoadedCounter = new TestUtils.InvokeCounter("OnCannedTextResponsesLoaded");
mOnSilenceRingerCounter = new TestUtils.InvokeCounter("OnSilenceRinger");
mOnConnectionEventCounter = new TestUtils.InvokeCounter("OnConnectionEvent");
mOnExtrasChangedCounter = new TestUtils.InvokeCounter("OnDetailsChangedCounter");
mOnPropertiesChangedCounter = new TestUtils.InvokeCounter("OnPropertiesChangedCounter");
mOnRttModeChangedCounter = new TestUtils.InvokeCounter("mOnRttModeChangedCounter");
mOnRttStatusChangedCounter = new TestUtils.InvokeCounter("mOnRttStatusChangedCounter");
mOnRttInitiationFailedCounter =
new TestUtils.InvokeCounter("mOnRttInitiationFailedCounter");
mOnRttRequestCounter = new TestUtils.InvokeCounter("mOnRttRequestCounter");
mOnHandoverCompleteCounter = new TestUtils.InvokeCounter("mOnHandoverCompleteCounter");
mOnHandoverFailedCounter = new TestUtils.InvokeCounter("mOnHandoverFailedCounter");
mOnPhoneAccountChangedCounter = new TestUtils.InvokeCounter(
"mOnPhoneAccountChangedCounter");
}
void addAndVerifyNewFailedIncomingCall(Uri incomingHandle, Bundle extras) {
assertEquals("Lock should have no permits!", 0, mInCallCallbacks.lock.availablePermits());
int currentCallCount = 0;
if (mInCallCallbacks.getService() != null) {
currentCallCount = mInCallCallbacks.getService().getCallCount();
}
if (extras == null) {
extras = new Bundle();
}
extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, incomingHandle);
mTelecomManager.addNewIncomingCall(TestUtils.TEST_PHONE_ACCOUNT_HANDLE, extras);
if (!connectionService.waitForEvent(
MockConnectionService.EVENT_CONNECTION_SERVICE_CREATE_CONNECTION_FAILED)) {
fail("Incoming Connection failure indication did not get called.");
}
assertEquals("ConnectionService did not receive failed connection",
1, connectionService.failedConnections.size());
assertEquals("Address is not correct for failed connection",
connectionService.failedConnections.get(0).getAddress(), incomingHandle);
assertEquals("InCallService should contain the same number of calls.",
currentCallCount,
mInCallCallbacks.getService().getCallCount());
}
/**
* Puts Telecom in a state where there is an incoming call provided by the
* {@link CtsConnectionService} which can be tested.
*/
void addAndVerifyNewIncomingCall(Uri incomingHandle, Bundle extras) {
int currentCallCount = addNewIncomingCall(incomingHandle, extras);
verifyNewIncomingCall(currentCallCount);
}
int addNewIncomingCall(Uri incomingHandle, Bundle extras) {
assertEquals("Lock should have no permits!", 0, mInCallCallbacks.lock.availablePermits());
int currentCallCount = 0;
if (mInCallCallbacks.getService() != null) {
currentCallCount = mInCallCallbacks.getService().getCallCount();
}
if (extras == null) {
extras = new Bundle();
}
extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, incomingHandle);
mTelecomManager.addNewIncomingCall(TestUtils.TEST_PHONE_ACCOUNT_HANDLE, extras);
return currentCallCount;
}
void verifyNewIncomingCall(int currentCallCount) {
try {
if (!mInCallCallbacks.lock.tryAcquire(TestUtils.WAIT_FOR_CALL_ADDED_TIMEOUT_S,
TimeUnit.SECONDS)) {
fail("No call added to InCallService.");
}
} catch (InterruptedException e) {
Log.i(TAG, "Test interrupted!");
}
assertEquals("InCallService should contain 1 more call after adding a call.",
currentCallCount + 1,
mInCallCallbacks.getService().getCallCount());
}
/**
* Puts Telecom in a state where there is an active call provided by the
* {@link CtsConnectionService} which can be tested.
*/
void placeAndVerifyCall() {
placeAndVerifyCall(null);
}
void placeAndVerifyCallByRedirection(boolean wasCancelled) {
placeAndVerifyCallByRedirection(null, wasCancelled);
}
/**
* Puts Telecom in a state where there is an active call provided by the
* {@link CtsConnectionService} which can be tested.
*/
void placeAndVerifyCallByRedirection(Bundle extras, boolean wasCancelled) {
int currentCallCount = (getInCallService() == null) ? 0 : getInCallService().getCallCount();
int currentConnections = getNumberOfConnections();
// We expect a new connection if it wasn't cancelled.
if (!wasCancelled) {
currentConnections++;
currentCallCount++;
}
placeAndVerifyCall(extras, VideoProfile.STATE_AUDIO_ONLY, currentCallCount);
// The connectionService.lock is released in
// MockConnectionService#onCreateOutgoingConnection, however the connection will not
// actually be added to the list of connections in the ConnectionService until shortly
// afterwards. So there is still a potential for the lock to be released before it would
// be seen by calls to ConnectionService#getAllConnections().
// We will wait here until the list of connections includes one more connection to ensure
// that placing the call has fully completed.
assertCSConnections(currentConnections);
// Ensure the new outgoing call broadcast fired for the outgoing call.
assertOutgoingCallBroadcastReceived(true);
// CTS test does not have read call log permission so should not get the phone number.
assertNull(NewOutgoingCallBroadcastReceiver.getReceivedNumber());
}
/**
* Puts Telecom in a state where there is an active call provided by the
* {@link CtsConnectionService} which can be tested.
*
* @param videoState the video state of the call.
*/
void placeAndVerifyCall(int videoState) {
placeAndVerifyCall(null, videoState);
}
/**
* Puts Telecom in a state where there is an active call provided by the
* {@link CtsConnectionService} which can be tested.
*/
void placeAndVerifyCall(Bundle extras) {
placeAndVerifyCall(extras, VideoProfile.STATE_AUDIO_ONLY);
}
/**
* Puts Telecom in a state where there is an active call provided by the
* {@link CtsConnectionService} which can be tested.
*/
void placeAndVerifyCall(Bundle extras, int videoState) {
int currentCallCount = (getInCallService() == null) ? 0 : getInCallService().getCallCount();
// We expect placing the call adds a new call/connection.
int expectedConnections = getNumberOfConnections() + 1;
placeAndVerifyCall(extras, videoState, currentCallCount + 1);
// The connectionService.lock is released in
// MockConnectionService#onCreateOutgoingConnection, however the connection will not
// actually be added to the list of connections in the ConnectionService until shortly
// afterwards. So there is still a potential for the lock to be released before it would
// be seen by calls to ConnectionService#getAllConnections().
// We will wait here until the list of connections includes one more connection to ensure
// that placing the call has fully completed.
assertCSConnections(expectedConnections);
assertOutgoingCallBroadcastReceived(true);
// CTS test does not have read call log permission so should not get the phone number.
assertNull(NewOutgoingCallBroadcastReceiver.getReceivedNumber());
}
/**
* Puts Telecom in a state where there is an active call provided by the
* {@link CtsConnectionService} which can be tested.
*/
void placeAndVerifyCall(Bundle extras, int videoState, int expectedCallCount) {
assertEquals("Lock should have no permits!", 0, mInCallCallbacks.lock.availablePermits());
placeNewCallWithPhoneAccount(extras, videoState);
try {
if (!mInCallCallbacks.lock.tryAcquire(TestUtils.WAIT_FOR_CALL_ADDED_TIMEOUT_S,
TimeUnit.SECONDS)) {
fail("No call added to InCallService.");
}
} catch (InterruptedException e) {
Log.i(TAG, "Test interrupted!");
}
// Make sure any procedures to disconnect existing calls (makeRoomForOutgoingCall)
// complete successfully
TestUtils.waitOnLocalMainLooper(WAIT_FOR_STATE_CHANGE_TIMEOUT_MS);
TestUtils.waitOnAllHandlers(getInstrumentation());
assertEquals("InCallService should match the expected count.", expectedCallCount,
mInCallCallbacks.getService().getCallCount());
}
/**
* Place an emergency call and verify that it has been setup properly.
*
* @param supportsHold If telecom supports holding emergency calls, this will expect two
* calls. If telecom does not support holding emergency calls, this will expect only the
* emergency call to be active.
* @return The emergency connection
*/
public Connection placeAndVerifyEmergencyCall(boolean supportsHold) {
Bundle extras = new Bundle();
extras.putParcelable(TestUtils.EXTRA_PHONE_NUMBER, TEST_EMERGENCY_URI);
// We want to request the active connections vs number of connections because in some cases,
// we wait to destroy the underlying connection to prevent race conditions. This will result
// in Connections in the DISCONNECTED state.
int currentConnectionCount = supportsHold ?
getNumberOfActiveConnections() + 1 : getNumberOfActiveConnections();
int currentCallCount = (getInCallService() == null) ? 0 : getInCallService().getCallCount();
currentCallCount = supportsHold ? currentCallCount + 1 : currentCallCount;
// The device only supports a max of two calls active at any one time
currentCallCount = Math.min(currentCallCount, 2);
placeAndVerifyCall(extras, VideoProfile.STATE_AUDIO_ONLY, currentCallCount);
// The connectionService.lock is released in
// MockConnectionService#onCreateOutgoingConnection, however the connection will not
// actually be added to the list of connections in the ConnectionService until shortly
// afterwards. So there is still a potential for the lock to be released before it would
// be seen by calls to ConnectionService#getAllConnections().
// We will wait here until the list of connections includes one more connection to ensure
// that placing the call has fully completed.
assertActiveCSConnections(currentConnectionCount);
assertOutgoingCallBroadcastReceived(true);
Connection connection = verifyConnectionForOutgoingCall(TEST_EMERGENCY_URI);
TestUtils.waitOnAllHandlers(getInstrumentation());
return connection;
}
int getNumberOfConnections() {
return CtsConnectionService.getAllConnectionsFromTelecom().size();
}
int getNumberOfActiveConnections() {
return CtsConnectionService.getAllConnectionsFromTelecom().stream()
.filter(c -> c.getState() != Connection.STATE_DISCONNECTED).collect(
Collectors.toSet()).size();
}
Connection getConnection(Uri address) {
return CtsConnectionService.getAllConnectionsFromTelecom().stream()
.filter(c -> c.getAddress().equals(address)).findFirst().orElse(null);
}
MockConnection verifyConnectionForOutgoingCall() {
// Assuming only 1 connection present
return verifyConnectionForOutgoingCall(0);
}
MockConnection verifyConnectionForOutgoingCall(int connectionIndex) {
try {
if (!connectionService.lock.tryAcquire(TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
TimeUnit.MILLISECONDS)) {
fail("No outgoing call connection requested by Telecom");
}
} catch (InterruptedException e) {
Log.i(TAG, "Test interrupted!");
}
assertThat("Telecom should create outgoing connection for outgoing call",
connectionService.outgoingConnections.size(), not(equalTo(0)));
MockConnection connection = connectionService.outgoingConnections.get(connectionIndex);
return connection;
}
MockConnection verifyConnectionForOutgoingCall(Uri address) {
if (!connectionService.waitForEvent(
MockConnectionService.EVENT_CONNECTION_SERVICE_CREATE_CONNECTION)) {
fail("No outgoing call connection requested by Telecom");
}
assertThat("Telecom should create outgoing connection for outgoing call",
connectionService.outgoingConnections.size(), not(equalTo(0)));
// There is a subtle race condition in ConnectionService. When onCreateIncomingConnection
// or onCreateOutgoingConnection completes, ConnectionService then adds the connection to
// the list of tracked connections. It's very possible for the lock to be released and
// the connection to have not yet been added to the connection list yet.
waitUntilConditionIsTrueOrTimeout(new Condition() {
@Override
public Object expected() {
return true;
}
@Override
public Object actual() {
return getConnection(address) != null;
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Expected call from number " + address);
Connection connection = getConnection(address);
if (connection instanceof MockConnection) {
if (connectionService.outgoingConnections.contains(connection)) {
return (MockConnection) connection;
}
}
return null;
}
MockConnection verifyConnectionForIncomingCall() {
// Assuming only 1 connection present
return verifyConnectionForIncomingCall(0);
}
MockConnection verifyConnectionForIncomingCall(int connectionIndex) {
try {
if (!connectionService.lock.tryAcquire(TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
TimeUnit.MILLISECONDS)) {
fail("No outgoing call connection requested by Telecom");
}
} catch (InterruptedException e) {
Log.i(TAG, "Test interrupted!");
}
assertThat("Telecom should create incoming connections for incoming calls",
connectionService.incomingConnections.size(), not(equalTo(0)));
MockConnection connection = connectionService.incomingConnections.get(connectionIndex);
setAndVerifyConnectionForIncomingCall(connection);
return connection;
}
MockConference verifyConference(int permit) {
try {
if (!connectionService.lock.tryAcquire(permit, WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
TimeUnit.MILLISECONDS)) {
fail("No conference requested by Telecom");
}
} catch (InterruptedException e) {
Log.i(TAG, "Test interrupted!");
}
return connectionService.conferences.get(0);
}
void setAndVerifyConnectionForIncomingCall(MockConnection connection) {
if (connection.getState() == Connection.STATE_ACTIVE) {
// If the connection is already active (like if it got picked up immediately), don't
// bother with setting it back to ringing.
return;
}
connection.setRinging();
assertConnectionState(connection, Connection.STATE_RINGING);
}
void setAndVerifyConferenceablesForOutgoingConnection(int connectionIndex) {
assertEquals("Lock should have no permits!", 0, mInCallCallbacks.lock.availablePermits());
// Make all other outgoing connections as conferenceable with this connection.
MockConnection connection = connectionService.outgoingConnections.get(connectionIndex);
List<Connection> confConnections =
new ArrayList<>(connectionService.outgoingConnections.size());
for (Connection c : connectionService.outgoingConnections) {
if (c != connection) {
confConnections.add(c);
}
}
connection.setConferenceableConnections(confConnections);
assertEquals(connection.getConferenceables(), confConnections);
}
void addConferenceCall(Call call1, Call call2) {
assertEquals("Lock should have no permits!", 0, mInCallCallbacks.lock.availablePermits());
int currentConfCallCount = 0;
if (mInCallCallbacks.getService() != null) {
currentConfCallCount = mInCallCallbacks.getService().getConferenceCallCount();
}
// Verify that the calls have each other on their conferenceable list before proceeding
List<Call> callConfList = new ArrayList<>();
callConfList.add(call2);
assertCallConferenceableList(call1, callConfList);
callConfList.clear();
callConfList.add(call1);
assertCallConferenceableList(call2, callConfList);
call1.conference(call2);
/**
* We should have 1 onCallAdded, 2 onChildrenChanged and 2 onParentChanged invoked, so
* we should have 5 available permits on the incallService lock.
*/
try {
if (!mInCallCallbacks.lock.tryAcquire(5, 3, TimeUnit.SECONDS)) {
fail("Conference addition failed.");
}
} catch (InterruptedException e) {
Log.i(TAG, "Test interrupted!");
}
assertEquals("InCallService should contain 1 more call after adding a conf call.",
currentConfCallCount + 1,
mInCallCallbacks.getService().getConferenceCallCount());
}
void splitFromConferenceCall(Call call1) {
assertEquals("Lock should have no permits!", 0, mInCallCallbacks.lock.availablePermits());
call1.splitFromConference();
/**
* We should have 1 onChildrenChanged and 1 onParentChanged invoked, so
* we should have 2 available permits on the incallService lock.
*/
try {
if (!mInCallCallbacks.lock.tryAcquire(2, 3, TimeUnit.SECONDS)) {
fail("Conference split failed");
}
} catch (InterruptedException e) {
Log.i(TAG, "Test interrupted!");
}
}
MockConference verifyConferenceForOutgoingCall() {
try {
if (!connectionService.lock.tryAcquire(TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
TimeUnit.MILLISECONDS)) {
fail("No outgoing conference requested by Telecom");
}
} catch (InterruptedException e) {
Log.i(TAG, "Test interrupted!");
}
// Return the newly created conference object to the caller
MockConference conference = connectionService.conferences.get(0);
setAndVerifyConferenceForOutgoingCall(conference);
return conference;
}
Pair<Conference, ConnectionRequest> verifyAdhocConferenceCall() {
try {
if (!connectionService.lock.tryAcquire(2, WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
TimeUnit.MILLISECONDS)) {
fail("No conference requested by Telecom");
}
} catch (InterruptedException e) {
Log.i(TAG, "Test interrupted!");
}
return new Pair<>(connectionService.conferences.get(0),
connectionService.connectionRequest);
}
void setAndVerifyConferenceForOutgoingCall(MockConference conference) {
conference.setActive();
assertConferenceState(conference, Connection.STATE_ACTIVE);
}
void verifyCallStateListener(int expectedCallState) throws InterruptedException {
mTestCallStateListener.getCountDownLatch().await(
TestUtils.WAIT_FOR_PHONE_STATE_LISTENER_CALLBACK_TIMEOUT_S, TimeUnit.SECONDS);
assertEquals(expectedCallState, mTestCallStateListener.getLastState());
}
void verifyPhoneStateListenerCallbacksForCall(int expectedCallState, String expectedNumber)
throws Exception {
assertTrue(mTelephonyCallback.mCallbackSemaphore.tryAcquire(
TestUtils.WAIT_FOR_PHONE_STATE_LISTENER_CALLBACK_TIMEOUT_S, TimeUnit.SECONDS));
// At this point we can only be sure that we got AN update, but not necessarily the one we
// are looking for; wait until we see the state we want before verifying further.
waitUntilConditionIsTrueOrTimeout(new Condition() {
@Override
public Object expected() {
return true;
}
@Override
public Object actual() {
return mTelephonyCallback.mCallStates
.stream()
.filter(p -> p == expectedCallState)
.count() > 0;
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Expected call state " + expectedCallState + " and number "
+ expectedNumber);
// Get the most recent callback; it is possible that there was an initial state reported due
// to the fact that TelephonyManager will sometimes give an initial state back to the caller
// when the listener is registered.
int callState = mTelephonyCallback.mCallStates.get(
mTelephonyCallback.mCallStates.size() - 1);
assertEquals(expectedCallState, callState);
// Note: We do NOT check the phone number here. Due to changes in how the phone state
// broadcast is sent, the caller may receive multiple broadcasts, and the number will be
// present in one or the other. We waited for a full matching broadcast above so we can
// be sure the number was reported as expected.
}
void verifyPhoneStateListenerCallbacksForEmergencyCall(String expectedNumber)
throws Exception {
assertTrue(mTelephonyCallback.mCallbackSemaphore.tryAcquire(
TestUtils.WAIT_FOR_PHONE_STATE_LISTENER_CALLBACK_TIMEOUT_S, TimeUnit.SECONDS));
// At this point we can only be sure that we got AN update, but not necessarily the one we
// are looking for; wait until we see the state we want before verifying further.
waitUntilConditionIsTrueOrTimeout(new Condition() {
@Override
public Object expected() {
return true;
}
@Override
public Object actual() {
return mTelephonyCallback
.mLastOutgoingEmergencyNumber != null
&& mTelephonyCallback
.mLastOutgoingEmergencyNumber.getNumber()
.equals(expectedNumber);
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Expected emergency number: " + expectedNumber);
assertEquals(mTelephonyCallback.mLastOutgoingEmergencyNumber.getNumber(),
expectedNumber);
}
/**
* Disconnect the created test call and verify that Telecom has cleared all calls.
*/
void cleanupCalls() {
if (mInCallCallbacks != null && mInCallCallbacks.getService() != null) {
mInCallCallbacks.getService().disconnectAllConferenceCalls();
mInCallCallbacks.getService().disconnectAllCalls();
assertNumConferenceCalls(mInCallCallbacks.getService(), 0);
assertNumCalls(mInCallCallbacks.getService(), 0);
}
}
/**
* Place a new outgoing call via the {@link CtsConnectionService}
*/
private void placeNewCallWithPhoneAccount(Bundle extras, int videoState) {
if (extras == null) {
extras = new Bundle();
}
if (!extras.containsKey(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE)) {
extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
TestUtils.TEST_PHONE_ACCOUNT_HANDLE);
}
if (!VideoProfile.isAudioOnly(videoState)) {
extras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, videoState);
}
Uri number;
if (extras.containsKey(TestUtils.EXTRA_PHONE_NUMBER)) {
number = extras.getParcelable(TestUtils.EXTRA_PHONE_NUMBER);
} else {
number = createTestNumber();
}
mTelecomManager.placeCall(number, extras);
}
/**
* Create a new number each time for a new test. Telecom has special logic to reuse certain
* calls if multiple calls to the same number are placed within a short period of time which
* can cause certain tests to fail.
*/
Uri createTestNumber() {
return Uri.fromParts("tel", String.valueOf(++sCounter), null);
}
/**
* Creates a new random phone number in the range:
* 000-000-0000
* to
* 999-999-9999
* @return Randomized phone number.
*/
Uri createRandomTestNumber() {
return Uri.fromParts("tel", String.format("16%05d", new Random().nextInt(99999))
+ String.format("%04d", new Random().nextInt(9999)), null);
}
public static Uri getTestNumber() {
return Uri.fromParts("tel", String.valueOf(sCounter), null);
}
public boolean isLoggedCall(PhoneAccountHandle handle) {
PhoneAccount phoneAccount = mTelecomManager.getPhoneAccount(handle);
Bundle extras = phoneAccount.getExtras();
if (extras == null) {
extras = new Bundle();
}
boolean isSelfManaged = (phoneAccount.getCapabilities()
& PhoneAccount.CAPABILITY_SELF_MANAGED) == PhoneAccount.CAPABILITY_SELF_MANAGED;
// Calls are logged if:
// 1. They're not self-managed
// 2. They're self-managed and are configured to request logging.
return (!isSelfManaged
|| (isSelfManaged
&& extras.getBoolean(PhoneAccount.EXTRA_LOG_SELF_MANAGED_CALLS)
&& (phoneAccount.getSupportedUriSchemes().contains(PhoneAccount.SCHEME_TEL)
|| phoneAccount.getSupportedUriSchemes().contains(PhoneAccount.SCHEME_SIP))));
}
public CountDownLatch getCallLogEntryLatch() {
CountDownLatch changeLatch = new CountDownLatch(1);
mContext.getContentResolver().registerContentObserver(
CallLog.Calls.CONTENT_URI, true,
new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange, Uri uri) {
mContext.getContentResolver().unregisterContentObserver(this);
changeLatch.countDown();
super.onChange(selfChange);
}
});
return changeLatch;
}
public void verifyCallLogging(CountDownLatch logLatch, boolean isCallLogged, Uri testNumber) {
Cursor logCursor = getLatestCallLogCursorIfMatchesUri(logLatch, isCallLogged, testNumber);
if (isCallLogged) {
assertNotNull("Call log entry not found for test number", logCursor);
}
}
public void verifyCallLogging(Uri testNumber, int expectedLogType) {
CountDownLatch logLatch = getCallLogEntryLatch();
Cursor logCursor = getLatestCallLogCursorIfMatchesUri(logLatch, true /*isCallLogged*/,
testNumber);
assertNotNull("Call log entry not found for test number", logCursor);
int typeIndex = logCursor.getColumnIndex(CallLog.Calls.TYPE);
int type = logCursor.getInt(typeIndex);
assertEquals("recorded type does not match expected", expectedLogType, type);
}
public Cursor getLatestCallLogCursorIfMatchesUri(CountDownLatch latch, boolean newLogExpected,
Uri testNumber) {
if (newLogExpected) {
// Wait for the content observer to report that we have gotten a new call log entry.
try {
latch.await(WAIT_FOR_STATE_CHANGE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
} catch (InterruptedException ie) {
fail("Expected log latch");
}
}
// Query the latest entry into the call log.
Cursor callsCursor = mContext.getContentResolver().query(CallLog.Calls.CONTENT_URI, null,
null, null, CallLog.Calls._ID + " DESC limit 1;");
int numberIndex = callsCursor.getColumnIndex(CallLog.Calls.NUMBER);
if (callsCursor.moveToNext()) {
String number = callsCursor.getString(numberIndex);
if (testNumber.getSchemeSpecificPart().equals(number)) {
return callsCursor;
} else {
// Last call log entry doesnt match expected number.
return null;
}
}
// No Calls
return null;
}
void assertNumCalls(final MockInCallService inCallService, final int numCalls) {
waitUntilConditionIsTrueOrTimeout(new Condition() {
@Override
public Object expected() {
return numCalls;
}
@Override
public Object actual() {
return inCallService.getCallCount();
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"InCallService should contain " + numCalls + " calls."
);
}
void assertNumConferenceCalls(final MockInCallService inCallService, final int numCalls) {
waitUntilConditionIsTrueOrTimeout(new Condition() {
@Override
public Object expected() {
return numCalls;
}
@Override
public Object actual() {
return inCallService.getConferenceCallCount();
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"InCallService should contain " + numCalls + " conference calls."
);
}
void assertActiveCSConnections(final int numConnections) {
waitUntilConditionIsTrueOrTimeout(new Condition() {
@Override
public Object expected() {
return numConnections;
}
@Override
public Object actual() {
return getNumberOfActiveConnections();
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"ConnectionService should contain " + numConnections + " connections."
);
}
void assertCSConnections(final int numConnections) {
waitUntilConditionIsTrueOrTimeout(new Condition() {
@Override
public Object expected() {
return numConnections;
}
@Override
public Object actual() {
return CtsConnectionService
.getAllConnectionsFromTelecom()
.size();
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"ConnectionService should contain " + numConnections + " connections."
);
}
void assertNumConnections(final MockConnectionService connService, final int numConnections) {
waitUntilConditionIsTrueOrTimeout(new Condition() {
@Override
public Object expected() {
return numConnections;
}
@Override
public Object actual() {
return connService.getAllConnections().size();
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"ConnectionService should contain " + numConnections + " connections."
);
}
void assertMuteState(final InCallService incallService, final boolean isMuted) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return isMuted;
}
@Override
public Object actual() {
final CallAudioState state = incallService.getCallAudioState();
return state == null ? null : state.isMuted();
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Phone's mute state should be: " + isMuted
);
}
void assertMuteState(final MockConnection connection, final boolean isMuted) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return isMuted;
}
@Override
public Object actual() {
final CallAudioState state = connection.getCallAudioState();
return state == null ? null : state.isMuted();
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Connection's mute state should be: " + isMuted
);
}
void assertAudioRoute(final InCallService incallService, final int route) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return route;
}
@Override
public Object actual() {
final CallAudioState state = incallService.getCallAudioState();
return state == null ? null : state.getRoute();
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Phone's audio route should be: " + route
);
}
void assertNotAudioRoute(final InCallService incallService, final int route) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return new Boolean(true);
}
@Override
public Object actual() {
final CallAudioState state = incallService.getCallAudioState();
return route != state.getRoute();
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Phone's audio route should not be: " + route
);
}
void assertAudioRoute(final MockConnection connection, final int route) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return route;
}
@Override
public Object actual() {
final CallAudioState state = ((Connection) connection).getCallAudioState();
return state == null ? null : state.getRoute();
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Connection's audio route should be: " + route
);
}
void assertConnectionState(final Connection connection, final int state) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return state;
}
@Override
public Object actual() {
return connection.getState();
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Connection should be in state " + state
);
}
void assertCallState(final Call call, final int state) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return true;
}
@Override
public Object actual() {
return call.getState() == state && call.getDetails().getState() == state;
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Expected state: " + state + ", callState=" + call.getState() + ", detailState="
+ call.getDetails().getState()
);
}
void assertCallConferenceableList(final Call call, final List<Call> conferenceableList) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return conferenceableList;
}
@Override
public Object actual() {
return call.getConferenceableCalls();
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Call: " + call + " does not have the correct conferenceable call list."
);
}
void assertDtmfString(final MockConnection connection, final String dtmfString) {
waitUntilConditionIsTrueOrTimeout(new Condition() {
@Override
public Object expected() {
return dtmfString;
}
@Override
public Object actual() {
return connection.getDtmfString();
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"DTMF string should be equivalent to entered DTMF characters: " + dtmfString
);
}
void assertDtmfString(final MockConference conference, final String dtmfString) {
waitUntilConditionIsTrueOrTimeout(new Condition() {
@Override
public Object expected() {
return dtmfString;
}
@Override
public Object actual() {
return conference.getDtmfString();
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"DTMF string should be equivalent to entered DTMF characters: " + dtmfString
);
}
void assertCallDisplayName(final Call call, final String name) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return name;
}
@Override
public Object actual() {
return call.getDetails().getCallerDisplayName();
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Call should have display name: " + name
);
}
void assertCallHandle(final Call call, final Uri expectedHandle) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return expectedHandle;
}
@Override
public Object actual() {
return call.getDetails().getHandle();
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Call should have handle name: " + expectedHandle
);
}
void assertCallConnectTimeChanged(final Call call, final long time) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return true;
}
@Override
public Object actual() {
return call.getDetails().getConnectTimeMillis() != time;
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Call have connect time: " + time
);
}
void assertConnectionCallDisplayName(final Connection connection, final String name) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return name;
}
@Override
public Object actual() {
return connection.getCallerDisplayName();
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Connection should have display name: " + name
);
}
void assertDisconnectReason(final Connection connection, final String disconnectReason) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return disconnectReason;
}
@Override
public Object actual() {
return connection.getDisconnectCause().getReason();
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Connection should have been disconnected with reason: " + disconnectReason
);
}
void assertConferenceState(final Conference conference, final int state) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return state;
}
@Override
public Object actual() {
return conference.getState();
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Conference should be in state " + state
);
}
void assertOutgoingCallBroadcastReceived(boolean received) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return received;
}
@Override
public Object actual() {
return NewOutgoingCallBroadcastReceiver
.isNewOutgoingCallBroadcastReceived();
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
received ? "Outgoing Call Broadcast should be received"
: "Outgoing Call Broadcast should not be received"
);
}
void assertCallDetailsConstructed(Call mCall, boolean constructed) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return constructed;
}
@Override
public Object actual() {
return mCall != null && mCall.getDetails() != null;
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
constructed ? "Call Details should be constructed"
: "Call Details should not be constructed"
);
}
void assertCallGatewayConstructed(Call mCall, boolean constructed) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return constructed;
}
@Override
public Object actual() {
return mCall != null && mCall.getDetails() != null
&& mCall.getDetails().getGatewayInfo() != null;
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
constructed ? "Call Gateway should be constructed"
: "Call Gateway should not be constructed"
);
}
void assertCallNotNull(Call mCall, boolean notNull) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return notNull;
}
@Override
public Object actual() {
return mCall != null;
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
notNull ? "Call should not be null" : "Call should be null"
);
}
/**
* Checks all fields of two PhoneAccounts for equality, with the exception of the enabled state.
* Should only be called after assertPhoneAccountRegistered when it can be guaranteed
* that the PhoneAccount is registered.
* @param expected The expected PhoneAccount.
* @param actual The actual PhoneAccount.
*/
void assertPhoneAccountEquals(final PhoneAccount expected,
final PhoneAccount actual) {
assertEquals(expected.getAddress(), actual.getAddress());
assertEquals(expected.getAccountHandle(), actual.getAccountHandle());
assertEquals(expected.getCapabilities(), actual.getCapabilities());
assertTrue(areBundlesEqual(expected.getExtras(), actual.getExtras()));
assertEquals(expected.getHighlightColor(), actual.getHighlightColor());
assertEquals(expected.getIcon(), actual.getIcon());
assertEquals(expected.getLabel(), actual.getLabel());
assertEquals(expected.getShortDescription(), actual.getShortDescription());
assertEquals(expected.getSubscriptionAddress(), actual.getSubscriptionAddress());
assertEquals(expected.getSupportedUriSchemes(), actual.getSupportedUriSchemes());
}
void assertPhoneAccountRegistered(final PhoneAccountHandle handle) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return true;
}
@Override
public Object actual() {
return mTelecomManager.getPhoneAccount(handle) != null;
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Phone account registration failed for " + handle
);
}
void assertPhoneAccountEnabled(final PhoneAccountHandle handle) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return true;
}
@Override
public Object actual() {
PhoneAccount phoneAccount = mTelecomManager.getPhoneAccount(handle);
return (phoneAccount != null && phoneAccount.isEnabled());
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Phone account enable failed for " + handle
);
}
void assertPhoneAccountIsDefault(final PhoneAccountHandle handle) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return true;
}
@Override
public Object actual() {
PhoneAccountHandle phoneAccountHandle =
mTelecomManager.getUserSelectedOutgoingPhoneAccount();
return (phoneAccountHandle != null && phoneAccountHandle.equals(handle));
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Failed to set default phone account to " + handle
);
}
void assertCtsConnectionServiceUnbound() {
if (CtsConnectionService.isBound()) {
assertTrue("CtsConnectionService not yet unbound!",
CtsConnectionService.waitForUnBinding());
}
}
void assertMockInCallServiceUnbound() {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return false;
}
@Override
public Object actual() {
return MockInCallService.isServiceBound();
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"MockInCallService not yet unbound!"
);
}
void assertIsOutgoingCallPermitted(boolean isPermitted, PhoneAccountHandle handle) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return isPermitted;
}
@Override
public Object actual() {
return mTelecomManager.isOutgoingCallPermitted(handle);
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Expected isOutgoingCallPermitted to be " + isPermitted
);
}
void assertIsIncomingCallPermitted(boolean isPermitted, PhoneAccountHandle handle) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return isPermitted;
}
@Override
public Object actual() {
return mTelecomManager.isIncomingCallPermitted(handle);
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Expected isIncomingCallPermitted to be " + isPermitted
);
}
void assertIsInCall(boolean isIncall) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return isIncall;
}
@Override
public Object actual() {
return mTelecomManager.isInCall();
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Expected isInCall to be " + isIncall
);
}
void assertIsInManagedCall(boolean isIncall) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return isIncall;
}
@Override
public Object actual() {
return mTelecomManager.isInManagedCall();
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Expected isInManagedCall to be " + isIncall
);
}
/**
* Asserts that a call's properties are as expected.
*
* @param call The call.
* @param properties The expected properties.
*/
public void assertCallProperties(final Call call, final int properties) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return true;
}
@Override
public Object actual() {
return call.getDetails().hasProperty(properties);
}
},
TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Call should have properties " + properties
);
}
/**
* Asserts that a call does not have any of the specified call capability bits specified.
*
* @param call The call.
* @param capabilities The capability or capabilities which are not expected.
*/
public void assertDoesNotHaveCallCapabilities(final Call call, final int capabilities) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return true;
}
@Override
public Object actual() {
int callCapabilities = call.getDetails().getCallCapabilities();
return !Call.Details.hasProperty(callCapabilities, capabilities);
}
},
TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Call should not have capabilities " + capabilities
);
}
/**
* Asserts that a call does not have any of the specified call property bits specified.
*
* @param call The call.
* @param properties The property or properties which are not expected.
*/
public void assertDoesNotHaveCallProperties(final Call call, final int properties) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return true;
}
@Override
public Object actual() {
return !call.getDetails().hasProperty(properties);
}
},
TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Call should not have properties " + properties
);
}
/**
* Asserts that the audio manager reports the specified audio mode.
*
* @param audioManager The audio manager to check.
* @param expectedMode The expected audio mode.
*/
public void assertAudioMode(final AudioManager audioManager, final int expectedMode) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return true;
}
@Override
public Object actual() {
return audioManager.getMode() == expectedMode;
}
},
TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Audio mode was expected to be " + expectedMode
);
}
/**
* Asserts that a call's capabilities are as expected.
*
* @param call The call.
* @param capabilities The expected capabiltiies.
*/
public void assertCallCapabilities(final Call call, final int capabilities) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return true;
}
@Override
public Object actual() {
return (call.getDetails().getCallCapabilities() & capabilities) ==
capabilities;
}
},
TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Call should have properties " + capabilities
);
}
MockInCallService getInCallService() {
return (mInCallCallbacks == null) ? null : mInCallCallbacks.getService();
}
/**
* Asserts that the {@link UiModeManager} mode matches the specified mode.
*
* @param uiMode The expected ui mode.
*/
public void assertUiMode(final int uiMode) {
waitUntilConditionIsTrueOrTimeout(
new Condition() {
@Override
public Object expected() {
return uiMode;
}
@Override
public Object actual() {
return mUiModeManager.getCurrentModeType();
}
},
TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Expected ui mode " + uiMode
);
}
void waitUntilConditionIsTrueOrTimeout(Condition condition, long timeout,
String description) {
final long start = System.currentTimeMillis();
while (!Objects.equals(condition.expected(), condition.actual())
&& System.currentTimeMillis() - start < timeout) {
sleep(50);
}
assertEquals(description, condition.expected(), condition.actual());
}
/**
* Performs some work, and waits for the condition to be met. If the condition is not met in
* each step of the loop, the work is performed again.
*
* @param work The work to perform.
* @param condition The condition.
* @param timeout The timeout.
* @param description Description of the work being performed.
*/
void doWorkAndWaitUntilConditionIsTrueOrTimeout(Work work, Condition condition, long timeout,
String description) {
final long start = System.currentTimeMillis();
work.doWork();
while (!condition.expected().equals(condition.actual())
&& System.currentTimeMillis() - start < timeout) {
sleep(50);
work.doWork();
}
assertEquals(description, condition.expected(), condition.actual());
}
protected interface Condition {
Object expected();
Object actual();
}
protected interface Work {
void doWork();
}
public static boolean areBundlesEqual(Bundle extras, Bundle newExtras) {
if (extras == null || newExtras == null) {
return extras == newExtras;
}
if (extras.size() != newExtras.size()) {
return false;
}
for (String key : extras.keySet()) {
if (key != null) {
final Object value = extras.get(key);
final Object newValue = newExtras.get(key);
if (!Objects.equals(value, newValue)) {
return false;
}
}
}
return true;
}
}