1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.telecom.cts;
18 
19 import static android.telecom.cts.TestUtils.*;
20 
21 import static org.hamcrest.CoreMatchers.not;
22 import static org.hamcrest.CoreMatchers.equalTo;
23 import static org.junit.Assert.assertThat;
24 
25 import android.content.ComponentName;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.graphics.Color;
29 import android.net.Uri;
30 import android.os.Bundle;
31 import android.os.SystemClock;
32 import android.telecom.Call;
33 import android.telecom.CallAudioState;
34 import android.telecom.Conference;
35 import android.telecom.Connection;
36 import android.telecom.InCallService;
37 import android.telecom.PhoneAccount;
38 import android.telecom.PhoneAccountHandle;
39 import android.telecom.TelecomManager;
40 import android.telecom.VideoProfile;
41 import android.telecom.cts.MockInCallService.InCallServiceCallbacks;
42 import android.test.InstrumentationTestCase;
43 import android.text.TextUtils;
44 import android.util.Log;
45 
46 import java.util.ArrayList;
47 import java.util.List;
48 import java.util.concurrent.TimeUnit;
49 
50 /**
51  * Base class for Telecom CTS tests that require a {@link CtsConnectionService} and
52  * {@link MockInCallService} to verify Telecom functionality.
53  */
54 public class BaseTelecomTestWithMockServices extends InstrumentationTestCase {
55 
56     public static final int FLAG_REGISTER = 0x1;
57     public static final int FLAG_ENABLE = 0x2;
58 
59     public static final PhoneAccountHandle TEST_PHONE_ACCOUNT_HANDLE =
60             new PhoneAccountHandle(new ComponentName(PACKAGE, COMPONENT), ACCOUNT_ID);
61 
62     public static final PhoneAccount TEST_PHONE_ACCOUNT = PhoneAccount.builder(
63             TEST_PHONE_ACCOUNT_HANDLE, ACCOUNT_LABEL)
64             .setAddress(Uri.parse("tel:555-TEST"))
65             .setSubscriptionAddress(Uri.parse("tel:555-TEST"))
66             .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER |
67                     PhoneAccount.CAPABILITY_VIDEO_CALLING |
68                     PhoneAccount.CAPABILITY_CONNECTION_MANAGER)
69             .setHighlightColor(Color.RED)
70             .setShortDescription(ACCOUNT_LABEL)
71             .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
72             .addSupportedUriScheme(PhoneAccount.SCHEME_VOICEMAIL)
73             .build();
74 
75     private static int sCounter = 9999;
76 
77     Context mContext;
78     TelecomManager mTelecomManager;
79 
80     InvokeCounter mOnBringToForegroundCounter;
81     InvokeCounter mOnCallAudioStateChangedCounter;
82     InvokeCounter mOnPostDialWaitCounter;
83     InvokeCounter mOnCannedTextResponsesLoadedCounter;
84 
85     InCallServiceCallbacks mInCallCallbacks;
86     String mPreviousDefaultDialer = null;
87     MockConnectionService connectionService = null;
88 
89     boolean mShouldTestTelecom = true;
90 
91     @Override
setUp()92     protected void setUp() throws Exception {
93         super.setUp();
94         mContext = getInstrumentation().getContext();
95         mTelecomManager = (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE);
96 
97         mShouldTestTelecom = shouldTestTelecom(mContext);
98         if (mShouldTestTelecom) {
99             mPreviousDefaultDialer = TestUtils.getDefaultDialer(getInstrumentation());
100             TestUtils.setDefaultDialer(getInstrumentation(), PACKAGE);
101             setupCallbacks();
102         }
103     }
104 
105     @Override
tearDown()106     protected void tearDown() throws Exception {
107         if (mShouldTestTelecom) {
108             cleanupCalls();
109             if (!TextUtils.isEmpty(mPreviousDefaultDialer)) {
110                 TestUtils.setDefaultDialer(getInstrumentation(), mPreviousDefaultDialer);
111             }
112             tearDownConnectionService(TEST_PHONE_ACCOUNT_HANDLE);
113             assertMockInCallServiceUnbound();
114         }
115         super.tearDown();
116     }
117 
setupConnectionService(MockConnectionService connectionService, int flags)118     protected PhoneAccount setupConnectionService(MockConnectionService connectionService,
119             int flags) throws Exception {
120         if (connectionService != null) {
121             this.connectionService = connectionService;
122         } else {
123             // Generate a vanilla mock connection service, if not provided.
124             this.connectionService = new MockConnectionService();
125         }
126         CtsConnectionService.setUp(TEST_PHONE_ACCOUNT_HANDLE, this.connectionService);
127 
128         if ((flags & FLAG_REGISTER) != 0) {
129             mTelecomManager.registerPhoneAccount(TEST_PHONE_ACCOUNT);
130         }
131         if ((flags & FLAG_ENABLE) != 0) {
132             TestUtils.enablePhoneAccount(getInstrumentation(), TEST_PHONE_ACCOUNT_HANDLE);
133             // Wait till the adb commands have executed and account is enabled in Telecom database.
134             assertPhoneAccountEnabled(TEST_PHONE_ACCOUNT_HANDLE);
135         }
136 
137         return TEST_PHONE_ACCOUNT;
138     }
139 
tearDownConnectionService(PhoneAccountHandle accountHandle)140     protected void tearDownConnectionService(PhoneAccountHandle accountHandle) throws Exception {
141         assertNumConnections(this.connectionService, 0);
142         mTelecomManager.unregisterPhoneAccount(accountHandle);
143         CtsConnectionService.tearDown();
144         assertCtsConnectionServiceUnbound();
145         this.connectionService = null;
146     }
147 
startCallTo(Uri address, PhoneAccountHandle accountHandle)148     protected void startCallTo(Uri address, PhoneAccountHandle accountHandle) {
149         final Intent intent = new Intent(Intent.ACTION_CALL, address);
150         if (accountHandle != null) {
151             intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, accountHandle);
152         }
153         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
154         mContext.startActivity(intent);
155     }
156 
sleep(long ms)157     private void sleep(long ms) {
158         try {
159             Thread.sleep(ms);
160         } catch (InterruptedException e) {
161         }
162     }
163 
setupCallbacks()164     private void setupCallbacks() {
165         mInCallCallbacks = new InCallServiceCallbacks() {
166             @Override
167             public void onCallAdded(Call call, int numCalls) {
168                 Log.i(TAG, "onCallAdded, Call: " + call + ", Num Calls: " + numCalls);
169                 this.lock.release();
170             }
171             @Override
172             public void onCallRemoved(Call call, int numCalls) {
173                 Log.i(TAG, "onCallRemoved, Call: " + call + ", Num Calls: " + numCalls);
174             }
175             @Override
176             public void onParentChanged(Call call, Call parent) {
177                 Log.i(TAG, "onParentChanged, Call: " + call + ", Parent: " + parent);
178                 this.lock.release();
179             }
180             @Override
181             public void onChildrenChanged(Call call, List<Call> children) {
182                 Log.i(TAG, "onChildrenChanged, Call: " + call + "Children: " + children);
183                 this.lock.release();
184             }
185             @Override
186             public void onConferenceableCallsChanged(Call call, List<Call> conferenceableCalls) {
187                 Log.i(TAG, "onConferenceableCallsChanged, Call: " + call + ", Conferenceables: " +
188                         conferenceableCalls);
189             }
190             @Override
191             public void onDetailsChanged(Call call, Call.Details details) {
192                 Log.i(TAG, "onDetailsChanged, Call: " + call + ", Details: " + details);
193             }
194             @Override
195             public void onCallDestroyed(Call call) {
196                 Log.i(TAG, "onCallDestroyed, Call: " + call);
197             }
198             @Override
199             public void onCallStateChanged(Call call, int newState) {
200                 Log.i(TAG, "onCallStateChanged, Call: " + call + ", New State: " + newState);
201             }
202             @Override
203             public void onBringToForeground(boolean showDialpad) {
204                 mOnBringToForegroundCounter.invoke(showDialpad);
205             }
206             @Override
207             public void onCallAudioStateChanged(CallAudioState audioState) {
208                 Log.i(TAG, "onCallAudioStateChanged, audioState: " + audioState);
209                 mOnCallAudioStateChangedCounter.invoke(audioState);
210             }
211             @Override
212             public void onPostDialWait(Call call, String remainingPostDialSequence) {
213                 mOnPostDialWaitCounter.invoke(call, remainingPostDialSequence);
214             }
215             @Override
216             public void onCannedTextResponsesLoaded(Call call, List<String> cannedTextResponses) {
217                 mOnCannedTextResponsesLoadedCounter.invoke(call, cannedTextResponses);
218             }
219         };
220 
221         MockInCallService.setCallbacks(mInCallCallbacks);
222 
223         // TODO: If more InvokeCounters are added in the future, consider consolidating them into a
224         // single Collection.
225         mOnBringToForegroundCounter = new InvokeCounter("OnBringToForeground");
226         mOnCallAudioStateChangedCounter = new InvokeCounter("OnCallAudioStateChanged");
227         mOnPostDialWaitCounter = new InvokeCounter("OnPostDialWait");
228         mOnCannedTextResponsesLoadedCounter = new InvokeCounter("OnCannedTextResponsesLoaded");
229     }
230 
231     /**
232      * Puts Telecom in a state where there is an incoming call provided by the
233      * {@link CtsConnectionService} which can be tested.
234      */
addAndVerifyNewIncomingCall(Uri incomingHandle, Bundle extras)235     void addAndVerifyNewIncomingCall(Uri incomingHandle, Bundle extras) {
236         assertEquals("Lock should have no permits!", 0, mInCallCallbacks.lock.availablePermits());
237         int currentCallCount = 0;
238         if (mInCallCallbacks.getService() != null) {
239             currentCallCount = mInCallCallbacks.getService().getCallCount();
240         }
241 
242         if (extras == null) {
243             extras = new Bundle();
244         }
245         extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, incomingHandle);
246         mTelecomManager.addNewIncomingCall(TEST_PHONE_ACCOUNT_HANDLE, extras);
247 
248         try {
249             if (!mInCallCallbacks.lock.tryAcquire(3, TimeUnit.SECONDS)) {
250                 fail("No call added to InCallService.");
251             }
252         } catch (InterruptedException e) {
253             Log.i(TAG, "Test interrupted!");
254         }
255 
256         assertEquals("InCallService should contain 1 more call after adding a call.",
257                 currentCallCount + 1,
258                 mInCallCallbacks.getService().getCallCount());
259     }
260 
261     /**
262      *  Puts Telecom in a state where there is an active call provided by the
263      *  {@link CtsConnectionService} which can be tested.
264      */
placeAndVerifyCall()265     void placeAndVerifyCall() {
266         placeAndVerifyCall(null);
267     }
268 
269     /**
270      *  Puts Telecom in a state where there is an active call provided by the
271      *  {@link CtsConnectionService} which can be tested.
272      *
273      *  @param videoState the video state of the call.
274      */
placeAndVerifyCall(int videoState)275     void placeAndVerifyCall(int videoState) {
276         placeAndVerifyCall(null, videoState);
277     }
278 
279     /**
280      *  Puts Telecom in a state where there is an active call provided by the
281      *  {@link CtsConnectionService} which can be tested.
282      */
placeAndVerifyCall(Bundle extras)283     void placeAndVerifyCall(Bundle extras) {
284         placeAndVerifyCall(extras, VideoProfile.STATE_AUDIO_ONLY);
285     }
286 
287     /**
288      *  Puts Telecom in a state where there is an active call provided by the
289      *  {@link CtsConnectionService} which can be tested.
290      */
placeAndVerifyCall(Bundle extras, int videoState)291     void placeAndVerifyCall(Bundle extras, int videoState) {
292         assertEquals("Lock should have no permits!", 0, mInCallCallbacks.lock.availablePermits());
293         int currentCallCount = 0;
294         if (mInCallCallbacks.getService() != null) {
295             currentCallCount = mInCallCallbacks.getService().getCallCount();
296         }
297         placeNewCallWithPhoneAccount(extras, videoState);
298 
299         try {
300             if (!mInCallCallbacks.lock.tryAcquire(3, TimeUnit.SECONDS)) {
301                 fail("No call added to InCallService.");
302             }
303         } catch (InterruptedException e) {
304             Log.i(TAG, "Test interrupted!");
305         }
306 
307         assertEquals("InCallService should contain 1 more call after adding a call.",
308                 currentCallCount + 1,
309                 mInCallCallbacks.getService().getCallCount());
310     }
311 
verifyConnectionForOutgoingCall()312     MockConnection verifyConnectionForOutgoingCall() {
313         // Assuming only 1 connection present
314         return verifyConnectionForOutgoingCall(0);
315     }
316 
verifyConnectionForOutgoingCall(int connectionIndex)317     MockConnection verifyConnectionForOutgoingCall(int connectionIndex) {
318         try {
319             if (!connectionService.lock.tryAcquire(TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
320                     TimeUnit.MILLISECONDS)) {
321                 fail("No outgoing call connection requested by Telecom");
322             }
323         } catch (InterruptedException e) {
324             Log.i(TAG, "Test interrupted!");
325         }
326 
327         assertThat("Telecom should create outgoing connection for outgoing call",
328                 connectionService.outgoingConnections.size(), not(equalTo(0)));
329         MockConnection connection = connectionService.outgoingConnections.get(connectionIndex);
330         return connection;
331     }
332 
verifyConnectionForIncomingCall()333     MockConnection verifyConnectionForIncomingCall() {
334         // Assuming only 1 connection present
335         return verifyConnectionForIncomingCall(0);
336     }
337 
verifyConnectionForIncomingCall(int connectionIndex)338     MockConnection verifyConnectionForIncomingCall(int connectionIndex) {
339         try {
340             if (!connectionService.lock.tryAcquire(TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
341                     TimeUnit.MILLISECONDS)) {
342                 fail("No outgoing call connection requested by Telecom");
343             }
344         } catch (InterruptedException e) {
345             Log.i(TAG, "Test interrupted!");
346         }
347 
348         assertThat("Telecom should create incoming connections for incoming calls",
349                 connectionService.incomingConnections.size(), not(equalTo(0)));
350         MockConnection connection = connectionService.incomingConnections.get(connectionIndex);
351         setAndVerifyConnectionForIncomingCall(connection);
352         return connection;
353     }
354 
setAndVerifyConnectionForIncomingCall(MockConnection connection)355     void setAndVerifyConnectionForIncomingCall(MockConnection connection) {
356         connection.setRinging();
357         assertConnectionState(connection, Connection.STATE_RINGING);
358     }
359 
setAndVerifyConferenceablesForOutgoingConnection(int connectionIndex)360     void setAndVerifyConferenceablesForOutgoingConnection(int connectionIndex) {
361         assertEquals("Lock should have no permits!", 0, mInCallCallbacks.lock.availablePermits());
362         // Make all other outgoing connections as conferenceable with this connection.
363         MockConnection connection = connectionService.outgoingConnections.get(connectionIndex);
364         List<Connection> confConnections =
365                 new ArrayList<>(connectionService.outgoingConnections.size());
366         for (Connection c : connectionService.outgoingConnections) {
367             if (c != connection) {
368                 confConnections.add(c);
369             }
370         }
371         connection.setConferenceableConnections(confConnections);
372         assertEquals(connection.getConferenceables(), confConnections);
373     }
374 
addConferenceCall(Call call1, Call call2)375     void addConferenceCall(Call call1, Call call2) {
376         assertEquals("Lock should have no permits!", 0, mInCallCallbacks.lock.availablePermits());
377         int currentConfCallCount = 0;
378         if (mInCallCallbacks.getService() != null) {
379             currentConfCallCount = mInCallCallbacks.getService().getConferenceCallCount();
380         }
381         // Verify that the calls have each other on their conferenceable list before proceeding
382         List<Call> callConfList = new ArrayList<>();
383         callConfList.add(call2);
384         assertCallConferenceableList(call1, callConfList);
385 
386         callConfList.clear();
387         callConfList.add(call1);
388         assertCallConferenceableList(call2, callConfList);
389 
390         call1.conference(call2);
391 
392         /**
393          * We should have 1 onCallAdded, 2 onChildrenChanged and 2 onParentChanged invoked, so
394          * we should have 5 available permits on the incallService lock.
395          */
396         try {
397             if (!mInCallCallbacks.lock.tryAcquire(5, 3, TimeUnit.SECONDS)) {
398                 fail("Conference addition failed.");
399             }
400         } catch (InterruptedException e) {
401             Log.i(TAG, "Test interrupted!");
402         }
403 
404         assertEquals("InCallService should contain 1 more call after adding a conf call.",
405                 currentConfCallCount + 1,
406                 mInCallCallbacks.getService().getConferenceCallCount());
407     }
408 
splitFromConferenceCall(Call call1)409     void splitFromConferenceCall(Call call1) {
410         assertEquals("Lock should have no permits!", 0, mInCallCallbacks.lock.availablePermits());
411 
412         call1.splitFromConference();
413         /**
414          * We should have 1 onChildrenChanged and 1 onParentChanged invoked, so
415          * we should have 2 available permits on the incallService lock.
416          */
417         try {
418             if (!mInCallCallbacks.lock.tryAcquire(2, 3, TimeUnit.SECONDS)) {
419                 fail("Conference split failed");
420             }
421         } catch (InterruptedException e) {
422             Log.i(TAG, "Test interrupted!");
423         }
424     }
425 
verifyConferenceForOutgoingCall()426     MockConference verifyConferenceForOutgoingCall() {
427         try {
428             if (!connectionService.lock.tryAcquire(TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
429                     TimeUnit.MILLISECONDS)) {
430                 fail("No outgoing conference requested by Telecom");
431             }
432         } catch (InterruptedException e) {
433             Log.i(TAG, "Test interrupted!");
434         }
435         // Return the newly created conference object to the caller
436         MockConference conference = connectionService.conferences.get(0);
437         setAndVerifyConferenceForOutgoingCall(conference);
438         return conference;
439     }
440 
setAndVerifyConferenceForOutgoingCall(MockConference conference)441     void setAndVerifyConferenceForOutgoingCall(MockConference conference) {
442         conference.setActive();
443         assertConferenceState(conference, Connection.STATE_ACTIVE);
444     }
445 
446     /**
447      * Disconnect the created test call and verify that Telecom has cleared all calls.
448      */
cleanupCalls()449     void cleanupCalls() {
450         if (mInCallCallbacks != null && mInCallCallbacks.getService() != null) {
451             mInCallCallbacks.getService().disconnectAllConferenceCalls();
452             mInCallCallbacks.getService().disconnectAllCalls();
453             assertNumConferenceCalls(mInCallCallbacks.getService(), 0);
454             assertNumCalls(mInCallCallbacks.getService(), 0);
455         }
456     }
457 
458     /**
459      * Place a new outgoing call via the {@link CtsConnectionService}
460      */
placeNewCallWithPhoneAccount(Bundle extras, int videoState)461     private void placeNewCallWithPhoneAccount(Bundle extras, int videoState) {
462         if (extras == null) {
463             extras = new Bundle();
464         }
465         extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, TEST_PHONE_ACCOUNT_HANDLE);
466 
467         if (!VideoProfile.isAudioOnly(videoState)) {
468             extras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, videoState);
469         }
470 
471         mTelecomManager.placeCall(createTestNumber(), extras);
472     }
473 
474     /**
475      * Create a new number each time for a new test. Telecom has special logic to reuse certain
476      * calls if multiple calls to the same number are placed within a short period of time which
477      * can cause certain tests to fail.
478      */
createTestNumber()479     Uri createTestNumber() {
480         return Uri.fromParts("tel", String.valueOf(++sCounter), null);
481     }
482 
getTestNumber()483     public static Uri getTestNumber() {
484         return Uri.fromParts("tel", String.valueOf(sCounter), null);
485     }
486 
assertNumCalls(final MockInCallService inCallService, final int numCalls)487     void assertNumCalls(final MockInCallService inCallService, final int numCalls) {
488         waitUntilConditionIsTrueOrTimeout(new Condition() {
489             @Override
490             public Object expected() {
491                 return numCalls;
492             }
493             @Override
494             public Object actual() {
495                 return inCallService.getCallCount();
496             }
497         },
498         WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
499         "InCallService should contain " + numCalls + " calls."
500     );
501     }
502 
assertNumConferenceCalls(final MockInCallService inCallService, final int numCalls)503     void assertNumConferenceCalls(final MockInCallService inCallService, final int numCalls) {
504         waitUntilConditionIsTrueOrTimeout(new Condition() {
505             @Override
506             public Object expected() {
507                 return numCalls;
508             }
509             @Override
510             public Object actual() {
511                 return inCallService.getConferenceCallCount();
512             }
513         },
514         WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
515         "InCallService should contain " + numCalls + " conference calls."
516     );
517     }
518 
519 
assertNumConnections(final MockConnectionService connService, final int numConnections)520     void assertNumConnections(final MockConnectionService connService, final int numConnections) {
521         waitUntilConditionIsTrueOrTimeout(new Condition() {
522                                               @Override
523                                               public Object expected() {
524                                                   return numConnections;
525                                               }
526                                               @Override
527                                               public Object actual() {
528                                                   return connService.getAllConnections().size();
529                                               }
530                                           },
531                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
532                 "ConnectionService should contain " + numConnections + " connections."
533         );
534     }
535 
assertMuteState(final InCallService incallService, final boolean isMuted)536     void assertMuteState(final InCallService incallService, final boolean isMuted) {
537         waitUntilConditionIsTrueOrTimeout(
538                 new Condition() {
539                     @Override
540                     public Object expected() {
541                         return isMuted;
542                     }
543 
544                     @Override
545                     public Object actual() {
546                         final CallAudioState state = incallService.getCallAudioState();
547                         return state == null ? null : state.isMuted();
548                     }
549                 },
550                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
551                 "Phone's mute state should be: " + isMuted
552         );
553     }
554 
assertMuteState(final MockConnection connection, final boolean isMuted)555     void assertMuteState(final MockConnection connection, final boolean isMuted) {
556         waitUntilConditionIsTrueOrTimeout(
557                 new Condition() {
558                     @Override
559                     public Object expected() {
560                         return isMuted;
561                     }
562 
563                     @Override
564                     public Object actual() {
565                         final CallAudioState state = connection.getCallAudioState();
566                         return state == null ? null : state.isMuted();
567                     }
568                 },
569                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
570                 "Connection's mute state should be: " + isMuted
571         );
572     }
573 
assertAudioRoute(final InCallService incallService, final int route)574     void assertAudioRoute(final InCallService incallService, final int route) {
575         waitUntilConditionIsTrueOrTimeout(
576                 new Condition() {
577                     @Override
578                     public Object expected() {
579                         return route;
580                     }
581 
582                     @Override
583                     public Object actual() {
584                         final CallAudioState state = incallService.getCallAudioState();
585                         return state == null ? null : state.getRoute();
586                     }
587                 },
588                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
589                 "Phone's audio route should be: " + route
590         );
591     }
592 
assertAudioRoute(final MockConnection connection, final int route)593     void assertAudioRoute(final MockConnection connection, final int route) {
594         waitUntilConditionIsTrueOrTimeout(
595                 new Condition() {
596                     @Override
597                     public Object expected() {
598                         return route;
599                     }
600 
601                     @Override
602                     public Object actual() {
603                         final CallAudioState state = ((Connection) connection).getCallAudioState();
604                         return state == null ? null : state.getRoute();
605                     }
606                 },
607                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
608                 "Connection's audio route should be: " + route
609         );
610     }
611 
assertConnectionState(final Connection connection, final int state)612     void assertConnectionState(final Connection connection, final int state) {
613         waitUntilConditionIsTrueOrTimeout(
614                 new Condition() {
615                     @Override
616                     public Object expected() {
617                         return state;
618                     }
619 
620                     @Override
621                     public Object actual() {
622                         return connection.getState();
623                     }
624                 },
625                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
626                 "Connection should be in state " + state
627         );
628     }
629 
assertCallState(final Call call, final int state)630     void assertCallState(final Call call, final int state) {
631         waitUntilConditionIsTrueOrTimeout(
632                 new Condition() {
633                     @Override
634                     public Object expected() {
635                         return state;
636                     }
637 
638                     @Override
639                     public Object actual() {
640                         return call.getState();
641                     }
642                 },
643                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
644                 "Call: " + call + " should be in state " + state
645         );
646     }
647 
assertCallConferenceableList(final Call call, final List<Call> conferenceableList)648     void assertCallConferenceableList(final Call call, final List<Call> conferenceableList) {
649         waitUntilConditionIsTrueOrTimeout(
650                 new Condition() {
651                     @Override
652                     public Object expected() {
653                         return conferenceableList;
654                     }
655 
656                     @Override
657                     public Object actual() {
658                         return call.getConferenceableCalls();
659                     }
660                 },
661                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
662                 "Call: " + call + " does not have the correct conferenceable call list."
663         );
664     }
665 
assertDtmfString(final MockConnection connection, final String dtmfString)666     void assertDtmfString(final MockConnection connection, final String dtmfString) {
667         waitUntilConditionIsTrueOrTimeout(new Condition() {
668                 @Override
669                 public Object expected() {
670                     return dtmfString;
671                 }
672 
673                 @Override
674                 public Object actual() {
675                     return connection.getDtmfString();
676                 }
677             },
678             WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
679             "DTMF string should be equivalent to entered DTMF characters: " + dtmfString
680         );
681     }
682 
assertDtmfString(final MockConference conference, final String dtmfString)683     void assertDtmfString(final MockConference conference, final String dtmfString) {
684         waitUntilConditionIsTrueOrTimeout(new Condition() {
685                 @Override
686                 public Object expected() {
687                     return dtmfString;
688                 }
689 
690                 @Override
691                 public Object actual() {
692                     return conference.getDtmfString();
693                 }
694             },
695             WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
696             "DTMF string should be equivalent to entered DTMF characters: " + dtmfString
697         );
698     }
699 
assertCallDisplayName(final Call call, final String name)700     void assertCallDisplayName(final Call call, final String name) {
701         waitUntilConditionIsTrueOrTimeout(
702                 new Condition() {
703                     @Override
704                     public Object expected() {
705                         return name;
706                     }
707 
708                     @Override
709                     public Object actual() {
710                         return call.getDetails().getCallerDisplayName();
711                     }
712                 },
713                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
714                 "Call should have display name: " + name
715         );
716     }
717 
assertConnectionCallDisplayName(final Connection connection, final String name)718     void assertConnectionCallDisplayName(final Connection connection, final String name) {
719         waitUntilConditionIsTrueOrTimeout(
720                 new Condition() {
721                     @Override
722                     public Object expected() {
723                         return name;
724                     }
725 
726                     @Override
727                     public Object actual() {
728                         return connection.getCallerDisplayName();
729                     }
730                 },
731                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
732                 "Connection should have display name: " + name
733         );
734     }
735 
assertConferenceState(final Conference conference, final int state)736     void assertConferenceState(final Conference conference, final int state) {
737         waitUntilConditionIsTrueOrTimeout(
738                 new Condition() {
739                     @Override
740                     public Object expected() {
741                         return state;
742                     }
743 
744                     @Override
745                     public Object actual() {
746                         return conference.getState();
747                     }
748                 },
749                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
750                 "Conference should be in state " + state
751         );
752     }
753 
assertPhoneAccountRegistered(final PhoneAccountHandle handle)754     void assertPhoneAccountRegistered(final PhoneAccountHandle handle) {
755         waitUntilConditionIsTrueOrTimeout(
756                 new Condition() {
757                     @Override
758                     public Object expected() {
759                         return true;
760                     }
761 
762                     @Override
763                     public Object actual() {
764                         return mTelecomManager.getPhoneAccount(handle) != null;
765                     }
766                 },
767                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
768                 "Phone account registration failed for " + handle
769         );
770     }
771 
assertPhoneAccountEnabled(final PhoneAccountHandle handle)772     void assertPhoneAccountEnabled(final PhoneAccountHandle handle) {
773         waitUntilConditionIsTrueOrTimeout(
774                 new Condition() {
775                     @Override
776                     public Object expected() {
777                         return true;
778                     }
779 
780                     @Override
781                     public Object actual() {
782                         PhoneAccount phoneAccount = mTelecomManager.getPhoneAccount(handle);
783                         return (phoneAccount != null && phoneAccount.isEnabled());
784                     }
785                 },
786                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
787                 "Phone account enable failed for " + handle
788         );
789     }
790 
assertCtsConnectionServiceUnbound()791     void assertCtsConnectionServiceUnbound() {
792         waitUntilConditionIsTrueOrTimeout(
793                 new Condition() {
794                     @Override
795                     public Object expected() {
796                         return true;
797                     }
798 
799                     @Override
800                     public Object actual() {
801                         return CtsConnectionService.isServiceUnbound();
802                     }
803                 },
804                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
805                 "CtsConnectionService not yet unbound!"
806         );
807     }
808 
assertMockInCallServiceUnbound()809     void assertMockInCallServiceUnbound() {
810         waitUntilConditionIsTrueOrTimeout(
811                 new Condition() {
812                     @Override
813                     public Object expected() {
814                         return true;
815                     }
816 
817                     @Override
818                     public Object actual() {
819                         return MockInCallService.isServiceUnbound();
820                     }
821                 },
822                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
823                 "MockInCallService not yet unbound!"
824         );
825     }
826 
waitUntilConditionIsTrueOrTimeout(Condition condition, long timeout, String description)827     void waitUntilConditionIsTrueOrTimeout(Condition condition, long timeout,
828             String description) {
829         final long start = System.currentTimeMillis();
830         while (!condition.expected().equals(condition.actual())
831                 && System.currentTimeMillis() - start < timeout) {
832             sleep(50);
833         }
834         assertEquals(description, condition.expected(), condition.actual());
835     }
836 
837     /**
838      * Performs some work, and waits for the condition to be met.  If the condition is not met in
839      * each step of the loop, the work is performed again.
840      *
841      * @param work The work to perform.
842      * @param condition The condition.
843      * @param timeout The timeout.
844      * @param description Description of the work being performed.
845      */
doWorkAndWaitUntilConditionIsTrueOrTimeout(Work work, Condition condition, long timeout, String description)846     void doWorkAndWaitUntilConditionIsTrueOrTimeout(Work work, Condition condition, long timeout,
847             String description) {
848         final long start = System.currentTimeMillis();
849         work.doWork();
850         while (!condition.expected().equals(condition.actual())
851                 && System.currentTimeMillis() - start < timeout) {
852             sleep(50);
853             work.doWork();
854         }
855         assertEquals(description, condition.expected(), condition.actual());
856     }
857 
858     protected interface Condition {
expected()859         Object expected();
actual()860         Object actual();
861     }
862 
863     protected interface Work {
doWork()864         void doWork();
865     }
866 
867     /**
868      * Utility class used to track the number of times a callback was invoked, and the arguments it
869      * was invoked with. This class is prefixed Invoke rather than the more typical Call for
870      * disambiguation purposes.
871      */
872     protected static final class InvokeCounter {
873         private final String mName;
874         private final Object mLock = new Object();
875         private final ArrayList<Object[]> mInvokeArgs = new ArrayList<>();
876 
877         private int mInvokeCount;
878 
InvokeCounter(String callbackName)879         public InvokeCounter(String callbackName) {
880             mName = callbackName;
881         }
882 
invoke(Object... args)883         public void invoke(Object... args) {
884             synchronized (mLock) {
885                 mInvokeCount++;
886                 mInvokeArgs.add(args);
887                 mLock.notifyAll();
888             }
889         }
890 
getArgs(int index)891         public Object[] getArgs(int index) {
892             synchronized (mLock) {
893                 return mInvokeArgs.get(index);
894             }
895         }
896 
getInvokeCount()897         public int getInvokeCount() {
898             synchronized (mLock) {
899                 return mInvokeCount;
900             }
901         }
902 
waitForCount(int count)903         public void waitForCount(int count) {
904             waitForCount(count, WAIT_FOR_STATE_CHANGE_TIMEOUT_MS);
905         }
906 
waitForCount(int count, long timeoutMillis)907         public void waitForCount(int count, long timeoutMillis) {
908             waitForCount(count, timeoutMillis, null);
909         }
910 
waitForCount(int count, long timeoutMillis, String message)911         public void waitForCount(int count, long timeoutMillis, String message) {
912             synchronized (mLock) {
913                 final long startTimeMillis = SystemClock.uptimeMillis();
914                 while (mInvokeCount < count) {
915                     try {
916                         final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
917                         final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis;
918                         if (remainingTimeMillis <= 0) {
919                             if (message != null) {
920                                 fail(message);
921                             } else {
922                                 fail(String.format("Expected %s to be called %d times.", mName,
923                                         count));
924                             }
925                         }
926                         mLock.wait(timeoutMillis);
927                     } catch (InterruptedException ie) {
928                         /* ignore */
929                     }
930                 }
931             }
932         }
933     }
934 }
935