blob: 323190c34c5b79600fb88bff427d8e5e61f8c0df [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.*;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertThat;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.os.SystemClock;
import android.telecom.Call;
import android.telecom.CallAudioState;
import android.telecom.Conference;
import android.telecom.Connection;
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.test.InstrumentationTestCase;
import android.text.TextUtils;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 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 PhoneAccountHandle TEST_PHONE_ACCOUNT_HANDLE =
new PhoneAccountHandle(new ComponentName(PACKAGE, COMPONENT), ACCOUNT_ID);
public static final PhoneAccount TEST_PHONE_ACCOUNT = PhoneAccount.builder(
TEST_PHONE_ACCOUNT_HANDLE, ACCOUNT_LABEL)
.setAddress(Uri.parse("tel:555-TEST"))
.setSubscriptionAddress(Uri.parse("tel:555-TEST"))
.setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER |
PhoneAccount.CAPABILITY_VIDEO_CALLING |
PhoneAccount.CAPABILITY_CONNECTION_MANAGER)
.setHighlightColor(Color.RED)
.setShortDescription(ACCOUNT_LABEL)
.addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
.addSupportedUriScheme(PhoneAccount.SCHEME_VOICEMAIL)
.build();
private static int sCounter = 9999;
Context mContext;
TelecomManager mTelecomManager;
InvokeCounter mOnBringToForegroundCounter;
InvokeCounter mOnCallAudioStateChangedCounter;
InvokeCounter mOnPostDialWaitCounter;
InvokeCounter mOnCannedTextResponsesLoadedCounter;
InCallServiceCallbacks mInCallCallbacks;
String mPreviousDefaultDialer = null;
MockConnectionService connectionService = null;
@Override
protected void setUp() throws Exception {
super.setUp();
mContext = getInstrumentation().getContext();
mTelecomManager = (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE);
if (shouldTestTelecom(mContext)) {
mPreviousDefaultDialer = TestUtils.getDefaultDialer(getInstrumentation());
TestUtils.setDefaultDialer(getInstrumentation(), PACKAGE);
setupCallbacks();
}
}
@Override
protected void tearDown() throws Exception {
if (shouldTestTelecom(mContext)) {
cleanupCalls();
if (!TextUtils.isEmpty(mPreviousDefaultDialer)) {
TestUtils.setDefaultDialer(getInstrumentation(), mPreviousDefaultDialer);
}
tearDownConnectionService(TEST_PHONE_ACCOUNT_HANDLE);
}
super.tearDown();
}
protected PhoneAccount setupConnectionService(MockConnectionService connectionService,
int flags)
throws Exception {
if (connectionService != null) {
this.connectionService = connectionService;
} else {
// Generate a vanilla mock connection service, if not provided.
this.connectionService = new MockConnectionService();
}
CtsConnectionService.setUp(TEST_PHONE_ACCOUNT_HANDLE, this.connectionService);
if ((flags & FLAG_REGISTER) != 0) {
mTelecomManager.registerPhoneAccount(TEST_PHONE_ACCOUNT);
}
if ((flags & FLAG_ENABLE) != 0) {
TestUtils.enablePhoneAccount(getInstrumentation(), TEST_PHONE_ACCOUNT_HANDLE);
}
return TEST_PHONE_ACCOUNT;
}
protected void tearDownConnectionService(PhoneAccountHandle accountHandle) throws Exception {
mTelecomManager.unregisterPhoneAccount(accountHandle);
CtsConnectionService.tearDown();
this.connectionService = null;
}
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);
}
private 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();
}
@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);
}
@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);
}
};
MockInCallService.setCallbacks(mInCallCallbacks);
// TODO: If more InvokeCounters are added in the future, consider consolidating them into a
// single Collection.
mOnBringToForegroundCounter = new InvokeCounter("OnBringToForeground");
mOnCallAudioStateChangedCounter = new InvokeCounter("OnCallAudioStateChanged");
mOnPostDialWaitCounter = new InvokeCounter("OnPostDialWait");
mOnCannedTextResponsesLoadedCounter = new InvokeCounter("OnCannedTextResponsesLoaded");
}
/**
* 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) {
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(TEST_PHONE_ACCOUNT_HANDLE, extras);
try {
if (!mInCallCallbacks.lock.tryAcquire(3, 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);
}
/**
* 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) {
assertEquals("Lock should have no permits!", 0, mInCallCallbacks.lock.availablePermits());
int currentCallCount = 0;
if (mInCallCallbacks.getService() != null) {
currentCallCount = mInCallCallbacks.getService().getCallCount();
}
placeNewCallWithPhoneAccount(extras, videoState);
try {
if (!mInCallCallbacks.lock.tryAcquire(3, 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());
}
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)));
assertEquals("Telecom should not create incoming connections for outgoing calls",
0, connectionService.incomingConnections.size());
MockConnection connection = connectionService.outgoingConnections.get(connectionIndex);
return connection;
}
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)));
assertEquals("Telecom should not create outgoing connections for incoming calls",
0, connectionService.outgoingConnections.size());
MockConnection connection = connectionService.incomingConnections.get(connectionIndex);
setAndVerifyConnectionForIncomingCall(connection);
return connection;
}
void setAndVerifyConnectionForIncomingCall(MockConnection connection) {
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;
}
void setAndVerifyConferenceForOutgoingCall(MockConference conference) {
conference.setActive();
assertConferenceState(conference, Connection.STATE_ACTIVE);
}
/**
* 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();
}
extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, TEST_PHONE_ACCOUNT_HANDLE);
if (!VideoProfile.isAudioOnly(videoState)) {
extras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, videoState);
}
mTelecomManager.placeCall(createTestNumber(), 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);
}
public static Uri getTestNumber() {
return Uri.fromParts("tel", String.valueOf(sCounter), 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 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 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 state;
}
@Override
public Object actual() {
return call.getState();
}
},
WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
"Call should be in state " + state
);
}
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 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 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 waitUntilConditionIsTrueOrTimeout(Condition condition, long timeout,
String description) {
final long start = System.currentTimeMillis();
while (!condition.expected().equals(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();
}
/**
* Utility class used to track the number of times a callback was invoked, and the arguments it
* was invoked with. This class is prefixed Invoke rather than the more typical Call for
* disambiguation purposes.
*/
protected static final class InvokeCounter {
private final String mName;
private final Object mLock = new Object();
private final ArrayList<Object[]> mInvokeArgs = new ArrayList<>();
private int mInvokeCount;
public InvokeCounter(String callbackName) {
mName = callbackName;
}
public void invoke(Object... args) {
synchronized (mLock) {
mInvokeCount++;
mInvokeArgs.add(args);
mLock.notifyAll();
}
}
public Object[] getArgs(int index) {
synchronized (mLock) {
return mInvokeArgs.get(index);
}
}
public int getInvokeCount() {
synchronized (mLock) {
return mInvokeCount;
}
}
public void waitForCount(int count) {
waitForCount(count, WAIT_FOR_STATE_CHANGE_TIMEOUT_MS);
}
public void waitForCount(int count, long timeoutMillis) {
waitForCount(count, timeoutMillis, null);
}
public void waitForCount(int count, long timeoutMillis, String message) {
synchronized (mLock) {
final long startTimeMillis = SystemClock.uptimeMillis();
while (mInvokeCount < count) {
try {
final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis;
if (remainingTimeMillis <= 0) {
if (message != null) {
fail(message);
} else {
fail(String.format("Expected %s to be called %d times.", mName,
count));
}
}
mLock.wait(timeoutMillis);
} catch (InterruptedException ie) {
/* ignore */
}
}
}
}
}
}