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.Objects;
49 import java.util.concurrent.TimeUnit;
50 
51 /**
52  * Base class for Telecom CTS tests that require a {@link CtsConnectionService} and
53  * {@link MockInCallService} to verify Telecom functionality.
54  */
55 public class BaseTelecomTestWithMockServices extends InstrumentationTestCase {
56 
57     public static final int FLAG_REGISTER = 0x1;
58     public static final int FLAG_ENABLE = 0x2;
59 
60     public static final PhoneAccountHandle TEST_PHONE_ACCOUNT_HANDLE =
61             new PhoneAccountHandle(new ComponentName(PACKAGE, COMPONENT), ACCOUNT_ID);
62 
63     public static final PhoneAccount TEST_PHONE_ACCOUNT = PhoneAccount.builder(
64             TEST_PHONE_ACCOUNT_HANDLE, ACCOUNT_LABEL)
65             .setAddress(Uri.parse("tel:555-TEST"))
66             .setSubscriptionAddress(Uri.parse("tel:555-TEST"))
67             .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER |
68                     PhoneAccount.CAPABILITY_VIDEO_CALLING |
69                     PhoneAccount.CAPABILITY_CONNECTION_MANAGER)
70             .setHighlightColor(Color.RED)
71             .setShortDescription(ACCOUNT_LABEL)
72             .addSupportedUriScheme(PhoneAccount.SCHEME_TEL)
73             .addSupportedUriScheme(PhoneAccount.SCHEME_VOICEMAIL)
74             .build();
75 
76     private static int sCounter = 9999;
77 
78     Context mContext;
79     TelecomManager mTelecomManager;
80 
81     InvokeCounter mOnBringToForegroundCounter;
82     InvokeCounter mOnCallAudioStateChangedCounter;
83     InvokeCounter mOnPostDialWaitCounter;
84     InvokeCounter mOnCannedTextResponsesLoadedCounter;
85     InvokeCounter mOnSilenceRingerCounter;
86     InvokeCounter mOnConnectionEventCounter;
87     InvokeCounter mOnExtrasChangedCounter;
88     InvokeCounter mOnPropertiesChangedCounter;
89     Bundle mPreviousExtras;
90     int mPreviousProperties = -1;
91 
92     InCallServiceCallbacks mInCallCallbacks;
93     String mPreviousDefaultDialer = null;
94     MockConnectionService connectionService = null;
95 
96     boolean mShouldTestTelecom = true;
97 
98     @Override
setUp()99     protected void setUp() throws Exception {
100         super.setUp();
101         mContext = getInstrumentation().getContext();
102         mTelecomManager = (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE);
103 
104         mShouldTestTelecom = shouldTestTelecom(mContext);
105         if (mShouldTestTelecom) {
106             mPreviousDefaultDialer = TestUtils.getDefaultDialer(getInstrumentation());
107             TestUtils.setDefaultDialer(getInstrumentation(), PACKAGE);
108             setupCallbacks();
109         }
110     }
111 
112     @Override
tearDown()113     protected void tearDown() throws Exception {
114         if (mShouldTestTelecom) {
115             cleanupCalls();
116             if (!TextUtils.isEmpty(mPreviousDefaultDialer)) {
117                 TestUtils.setDefaultDialer(getInstrumentation(), mPreviousDefaultDialer);
118             }
119             tearDownConnectionService(TEST_PHONE_ACCOUNT_HANDLE);
120             assertMockInCallServiceUnbound();
121         }
122         super.tearDown();
123     }
124 
setupConnectionService(MockConnectionService connectionService, int flags)125     protected PhoneAccount setupConnectionService(MockConnectionService connectionService,
126             int flags) throws Exception {
127         if (connectionService != null) {
128             this.connectionService = connectionService;
129         } else {
130             // Generate a vanilla mock connection service, if not provided.
131             this.connectionService = new MockConnectionService();
132         }
133         CtsConnectionService.setUp(TEST_PHONE_ACCOUNT_HANDLE, this.connectionService);
134 
135         if ((flags & FLAG_REGISTER) != 0) {
136             mTelecomManager.registerPhoneAccount(TEST_PHONE_ACCOUNT);
137         }
138         if ((flags & FLAG_ENABLE) != 0) {
139             TestUtils.enablePhoneAccount(getInstrumentation(), TEST_PHONE_ACCOUNT_HANDLE);
140             // Wait till the adb commands have executed and account is enabled in Telecom database.
141             assertPhoneAccountEnabled(TEST_PHONE_ACCOUNT_HANDLE);
142         }
143 
144         return TEST_PHONE_ACCOUNT;
145     }
146 
tearDownConnectionService(PhoneAccountHandle accountHandle)147     protected void tearDownConnectionService(PhoneAccountHandle accountHandle) throws Exception {
148         if (this.connectionService != null) {
149             assertNumConnections(this.connectionService, 0);
150         }
151         mTelecomManager.unregisterPhoneAccount(accountHandle);
152         CtsConnectionService.tearDown();
153         assertCtsConnectionServiceUnbound();
154         this.connectionService = null;
155     }
156 
startCallTo(Uri address, PhoneAccountHandle accountHandle)157     protected void startCallTo(Uri address, PhoneAccountHandle accountHandle) {
158         final Intent intent = new Intent(Intent.ACTION_CALL, address);
159         if (accountHandle != null) {
160             intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, accountHandle);
161         }
162         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
163         mContext.startActivity(intent);
164     }
165 
sleep(long ms)166     private void sleep(long ms) {
167         try {
168             Thread.sleep(ms);
169         } catch (InterruptedException e) {
170         }
171     }
172 
setupCallbacks()173     private void setupCallbacks() {
174         mInCallCallbacks = new InCallServiceCallbacks() {
175             @Override
176             public void onCallAdded(Call call, int numCalls) {
177                 Log.i(TAG, "onCallAdded, Call: " + call + ", Num Calls: " + numCalls);
178                 this.lock.release();
179             }
180             @Override
181             public void onCallRemoved(Call call, int numCalls) {
182                 Log.i(TAG, "onCallRemoved, Call: " + call + ", Num Calls: " + numCalls);
183             }
184             @Override
185             public void onParentChanged(Call call, Call parent) {
186                 Log.i(TAG, "onParentChanged, Call: " + call + ", Parent: " + parent);
187                 this.lock.release();
188             }
189             @Override
190             public void onChildrenChanged(Call call, List<Call> children) {
191                 Log.i(TAG, "onChildrenChanged, Call: " + call + "Children: " + children);
192                 this.lock.release();
193             }
194             @Override
195             public void onConferenceableCallsChanged(Call call, List<Call> conferenceableCalls) {
196                 Log.i(TAG, "onConferenceableCallsChanged, Call: " + call + ", Conferenceables: " +
197                         conferenceableCalls);
198             }
199             @Override
200             public void onDetailsChanged(Call call, Call.Details details) {
201                 Log.i(TAG, "onDetailsChanged, Call: " + call + ", Details: " + details);
202                 if (!areBundlesEqual(mPreviousExtras, details.getExtras())) {
203                     mOnExtrasChangedCounter.invoke(call, details);
204                 }
205                 mPreviousExtras = details.getExtras();
206 
207                 if (mPreviousProperties != details.getCallProperties()) {
208                     mOnPropertiesChangedCounter.invoke(call, details);
209                     Log.i(TAG, "onDetailsChanged; properties changed from " + Call.Details.propertiesToString(mPreviousProperties) +
210                             " to " + Call.Details.propertiesToString(details.getCallProperties()));
211                 }
212                 mPreviousProperties = details.getCallProperties();
213             }
214             @Override
215             public void onCallDestroyed(Call call) {
216                 Log.i(TAG, "onCallDestroyed, Call: " + call);
217             }
218             @Override
219             public void onCallStateChanged(Call call, int newState) {
220                 Log.i(TAG, "onCallStateChanged, Call: " + call + ", New State: " + newState);
221             }
222             @Override
223             public void onBringToForeground(boolean showDialpad) {
224                 mOnBringToForegroundCounter.invoke(showDialpad);
225             }
226             @Override
227             public void onCallAudioStateChanged(CallAudioState audioState) {
228                 Log.i(TAG, "onCallAudioStateChanged, audioState: " + audioState);
229                 mOnCallAudioStateChangedCounter.invoke(audioState);
230             }
231             @Override
232             public void onPostDialWait(Call call, String remainingPostDialSequence) {
233                 mOnPostDialWaitCounter.invoke(call, remainingPostDialSequence);
234             }
235             @Override
236             public void onCannedTextResponsesLoaded(Call call, List<String> cannedTextResponses) {
237                 mOnCannedTextResponsesLoadedCounter.invoke(call, cannedTextResponses);
238             }
239             @Override
240             public void onConnectionEvent(Call call, String event, Bundle extras) {
241                 mOnConnectionEventCounter.invoke(call, event, extras);
242             }
243 
244             @Override
245             public void onSilenceRinger() {
246                 Log.i(TAG, "onSilenceRinger");
247                 mOnSilenceRingerCounter.invoke();
248             }
249         };
250 
251         MockInCallService.setCallbacks(mInCallCallbacks);
252 
253         // TODO: If more InvokeCounters are added in the future, consider consolidating them into a
254         // single Collection.
255         mOnBringToForegroundCounter = new InvokeCounter("OnBringToForeground");
256         mOnCallAudioStateChangedCounter = new InvokeCounter("OnCallAudioStateChanged");
257         mOnPostDialWaitCounter = new InvokeCounter("OnPostDialWait");
258         mOnCannedTextResponsesLoadedCounter = new InvokeCounter("OnCannedTextResponsesLoaded");
259         mOnSilenceRingerCounter = new InvokeCounter("OnSilenceRinger");
260         mOnConnectionEventCounter = new InvokeCounter("OnConnectionEvent");
261         mOnExtrasChangedCounter = new InvokeCounter("OnDetailsChangedCounter");
262         mOnPropertiesChangedCounter = new InvokeCounter("OnPropertiesChangedCounter");
263     }
264 
265     /**
266      * Puts Telecom in a state where there is an incoming call provided by the
267      * {@link CtsConnectionService} which can be tested.
268      */
addAndVerifyNewIncomingCall(Uri incomingHandle, Bundle extras)269     void addAndVerifyNewIncomingCall(Uri incomingHandle, Bundle extras) {
270         assertEquals("Lock should have no permits!", 0, mInCallCallbacks.lock.availablePermits());
271         int currentCallCount = 0;
272         if (mInCallCallbacks.getService() != null) {
273             currentCallCount = mInCallCallbacks.getService().getCallCount();
274         }
275 
276         if (extras == null) {
277             extras = new Bundle();
278         }
279         extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, incomingHandle);
280         mTelecomManager.addNewIncomingCall(TEST_PHONE_ACCOUNT_HANDLE, extras);
281 
282         try {
283             if (!mInCallCallbacks.lock.tryAcquire(TestUtils.WAIT_FOR_CALL_ADDED_TIMEOUT_S,
284                         TimeUnit.SECONDS)) {
285                 fail("No call added to InCallService.");
286             }
287         } catch (InterruptedException e) {
288             Log.i(TAG, "Test interrupted!");
289         }
290 
291         assertEquals("InCallService should contain 1 more call after adding a call.",
292                 currentCallCount + 1,
293                 mInCallCallbacks.getService().getCallCount());
294     }
295 
296     /**
297      *  Puts Telecom in a state where there is an active call provided by the
298      *  {@link CtsConnectionService} which can be tested.
299      */
placeAndVerifyCall()300     void placeAndVerifyCall() {
301         placeAndVerifyCall(null);
302     }
303 
304     /**
305      *  Puts Telecom in a state where there is an active call provided by the
306      *  {@link CtsConnectionService} which can be tested.
307      *
308      *  @param videoState the video state of the call.
309      */
placeAndVerifyCall(int videoState)310     void placeAndVerifyCall(int videoState) {
311         placeAndVerifyCall(null, videoState);
312     }
313 
314     /**
315      *  Puts Telecom in a state where there is an active call provided by the
316      *  {@link CtsConnectionService} which can be tested.
317      */
placeAndVerifyCall(Bundle extras)318     void placeAndVerifyCall(Bundle extras) {
319         placeAndVerifyCall(extras, VideoProfile.STATE_AUDIO_ONLY);
320     }
321 
322     /**
323      *  Puts Telecom in a state where there is an active call provided by the
324      *  {@link CtsConnectionService} which can be tested.
325      */
placeAndVerifyCall(Bundle extras, int videoState)326     void placeAndVerifyCall(Bundle extras, int videoState) {
327         assertEquals("Lock should have no permits!", 0, mInCallCallbacks.lock.availablePermits());
328         int currentCallCount = 0;
329         if (mInCallCallbacks.getService() != null) {
330             currentCallCount = mInCallCallbacks.getService().getCallCount();
331         }
332         int currentConnectionCount = getNumberOfConnections();
333         placeNewCallWithPhoneAccount(extras, videoState);
334 
335         try {
336             if (!mInCallCallbacks.lock.tryAcquire(TestUtils.WAIT_FOR_CALL_ADDED_TIMEOUT_S,
337                         TimeUnit.SECONDS)) {
338                 fail("No call added to InCallService.");
339             }
340         } catch (InterruptedException e) {
341             Log.i(TAG, "Test interrupted!");
342         }
343 
344         assertEquals("InCallService should contain 1 more call after adding a call.",
345                 currentCallCount + 1,
346                 mInCallCallbacks.getService().getCallCount());
347 
348         // The connectionService.lock is released in
349         // MockConnectionService#onCreateOutgoingConnection, however the connection will not
350         // actually be added to the list of connections in the ConnectionService until shortly
351         // afterwards.  So there is still a potential for the lock to be released before it would
352         // be seen by calls to ConnectionService#getAllConnections().
353         // We will wait here until the list of connections includes one more connection to ensure
354         // that placing the call has fully completed.
355         final int expectedConnectionCount = currentConnectionCount + 1;
356         assertCSConnections(expectedConnectionCount);
357     }
358 
getNumberOfConnections()359     int getNumberOfConnections() {
360         return CtsConnectionService.getAllConnectionsFromTelecom().size();
361     }
362 
verifyConnectionForOutgoingCall()363     MockConnection verifyConnectionForOutgoingCall() {
364         // Assuming only 1 connection present
365         return verifyConnectionForOutgoingCall(0);
366     }
367 
verifyConnectionForOutgoingCall(int connectionIndex)368     MockConnection verifyConnectionForOutgoingCall(int connectionIndex) {
369         try {
370             if (!connectionService.lock.tryAcquire(TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
371                     TimeUnit.MILLISECONDS)) {
372                 fail("No outgoing call connection requested by Telecom");
373             }
374         } catch (InterruptedException e) {
375             Log.i(TAG, "Test interrupted!");
376         }
377 
378         assertThat("Telecom should create outgoing connection for outgoing call",
379                 connectionService.outgoingConnections.size(), not(equalTo(0)));
380         MockConnection connection = connectionService.outgoingConnections.get(connectionIndex);
381         return connection;
382     }
383 
verifyConnectionForIncomingCall()384     MockConnection verifyConnectionForIncomingCall() {
385         // Assuming only 1 connection present
386         return verifyConnectionForIncomingCall(0);
387     }
388 
verifyConnectionForIncomingCall(int connectionIndex)389     MockConnection verifyConnectionForIncomingCall(int connectionIndex) {
390         try {
391             if (!connectionService.lock.tryAcquire(TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
392                     TimeUnit.MILLISECONDS)) {
393                 fail("No outgoing call connection requested by Telecom");
394             }
395         } catch (InterruptedException e) {
396             Log.i(TAG, "Test interrupted!");
397         }
398 
399         assertThat("Telecom should create incoming connections for incoming calls",
400                 connectionService.incomingConnections.size(), not(equalTo(0)));
401         MockConnection connection = connectionService.incomingConnections.get(connectionIndex);
402         setAndVerifyConnectionForIncomingCall(connection);
403         return connection;
404     }
405 
setAndVerifyConnectionForIncomingCall(MockConnection connection)406     void setAndVerifyConnectionForIncomingCall(MockConnection connection) {
407         connection.setRinging();
408         assertConnectionState(connection, Connection.STATE_RINGING);
409     }
410 
setAndVerifyConferenceablesForOutgoingConnection(int connectionIndex)411     void setAndVerifyConferenceablesForOutgoingConnection(int connectionIndex) {
412         assertEquals("Lock should have no permits!", 0, mInCallCallbacks.lock.availablePermits());
413         // Make all other outgoing connections as conferenceable with this connection.
414         MockConnection connection = connectionService.outgoingConnections.get(connectionIndex);
415         List<Connection> confConnections =
416                 new ArrayList<>(connectionService.outgoingConnections.size());
417         for (Connection c : connectionService.outgoingConnections) {
418             if (c != connection) {
419                 confConnections.add(c);
420             }
421         }
422         connection.setConferenceableConnections(confConnections);
423         assertEquals(connection.getConferenceables(), confConnections);
424     }
425 
addConferenceCall(Call call1, Call call2)426     void addConferenceCall(Call call1, Call call2) {
427         assertEquals("Lock should have no permits!", 0, mInCallCallbacks.lock.availablePermits());
428         int currentConfCallCount = 0;
429         if (mInCallCallbacks.getService() != null) {
430             currentConfCallCount = mInCallCallbacks.getService().getConferenceCallCount();
431         }
432         // Verify that the calls have each other on their conferenceable list before proceeding
433         List<Call> callConfList = new ArrayList<>();
434         callConfList.add(call2);
435         assertCallConferenceableList(call1, callConfList);
436 
437         callConfList.clear();
438         callConfList.add(call1);
439         assertCallConferenceableList(call2, callConfList);
440 
441         call1.conference(call2);
442 
443         /**
444          * We should have 1 onCallAdded, 2 onChildrenChanged and 2 onParentChanged invoked, so
445          * we should have 5 available permits on the incallService lock.
446          */
447         try {
448             if (!mInCallCallbacks.lock.tryAcquire(5, 3, TimeUnit.SECONDS)) {
449                 fail("Conference addition failed.");
450             }
451         } catch (InterruptedException e) {
452             Log.i(TAG, "Test interrupted!");
453         }
454 
455         assertEquals("InCallService should contain 1 more call after adding a conf call.",
456                 currentConfCallCount + 1,
457                 mInCallCallbacks.getService().getConferenceCallCount());
458     }
459 
splitFromConferenceCall(Call call1)460     void splitFromConferenceCall(Call call1) {
461         assertEquals("Lock should have no permits!", 0, mInCallCallbacks.lock.availablePermits());
462 
463         call1.splitFromConference();
464         /**
465          * We should have 1 onChildrenChanged and 1 onParentChanged invoked, so
466          * we should have 2 available permits on the incallService lock.
467          */
468         try {
469             if (!mInCallCallbacks.lock.tryAcquire(2, 3, TimeUnit.SECONDS)) {
470                 fail("Conference split failed");
471             }
472         } catch (InterruptedException e) {
473             Log.i(TAG, "Test interrupted!");
474         }
475     }
476 
verifyConferenceForOutgoingCall()477     MockConference verifyConferenceForOutgoingCall() {
478         try {
479             if (!connectionService.lock.tryAcquire(TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
480                     TimeUnit.MILLISECONDS)) {
481                 fail("No outgoing conference requested by Telecom");
482             }
483         } catch (InterruptedException e) {
484             Log.i(TAG, "Test interrupted!");
485         }
486         // Return the newly created conference object to the caller
487         MockConference conference = connectionService.conferences.get(0);
488         setAndVerifyConferenceForOutgoingCall(conference);
489         return conference;
490     }
491 
setAndVerifyConferenceForOutgoingCall(MockConference conference)492     void setAndVerifyConferenceForOutgoingCall(MockConference conference) {
493         conference.setActive();
494         assertConferenceState(conference, Connection.STATE_ACTIVE);
495     }
496 
497     /**
498      * Disconnect the created test call and verify that Telecom has cleared all calls.
499      */
cleanupCalls()500     void cleanupCalls() {
501         if (mInCallCallbacks != null && mInCallCallbacks.getService() != null) {
502             mInCallCallbacks.getService().disconnectAllConferenceCalls();
503             mInCallCallbacks.getService().disconnectAllCalls();
504             assertNumConferenceCalls(mInCallCallbacks.getService(), 0);
505             assertNumCalls(mInCallCallbacks.getService(), 0);
506         }
507     }
508 
509     /**
510      * Place a new outgoing call via the {@link CtsConnectionService}
511      */
placeNewCallWithPhoneAccount(Bundle extras, int videoState)512     private void placeNewCallWithPhoneAccount(Bundle extras, int videoState) {
513         if (extras == null) {
514             extras = new Bundle();
515         }
516         extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, TEST_PHONE_ACCOUNT_HANDLE);
517 
518         if (!VideoProfile.isAudioOnly(videoState)) {
519             extras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, videoState);
520         }
521 
522         mTelecomManager.placeCall(createTestNumber(), extras);
523     }
524 
525     /**
526      * Create a new number each time for a new test. Telecom has special logic to reuse certain
527      * calls if multiple calls to the same number are placed within a short period of time which
528      * can cause certain tests to fail.
529      */
createTestNumber()530     Uri createTestNumber() {
531         return Uri.fromParts("tel", String.valueOf(++sCounter), null);
532     }
533 
getTestNumber()534     public static Uri getTestNumber() {
535         return Uri.fromParts("tel", String.valueOf(sCounter), null);
536     }
537 
assertNumCalls(final MockInCallService inCallService, final int numCalls)538     void assertNumCalls(final MockInCallService inCallService, final int numCalls) {
539         waitUntilConditionIsTrueOrTimeout(new Condition() {
540             @Override
541             public Object expected() {
542                 return numCalls;
543             }
544             @Override
545             public Object actual() {
546                 return inCallService.getCallCount();
547             }
548         },
549         WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
550         "InCallService should contain " + numCalls + " calls."
551     );
552     }
553 
assertNumConferenceCalls(final MockInCallService inCallService, final int numCalls)554     void assertNumConferenceCalls(final MockInCallService inCallService, final int numCalls) {
555         waitUntilConditionIsTrueOrTimeout(new Condition() {
556             @Override
557             public Object expected() {
558                 return numCalls;
559             }
560             @Override
561             public Object actual() {
562                 return inCallService.getConferenceCallCount();
563             }
564         },
565         WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
566         "InCallService should contain " + numCalls + " conference calls."
567     );
568     }
569 
assertCSConnections(final int numConnections)570     void assertCSConnections(final int numConnections) {
571         waitUntilConditionIsTrueOrTimeout(new Condition() {
572                                               @Override
573                                               public Object expected() {
574                                                   return numConnections;
575                                               }
576 
577                                               @Override
578                                               public Object actual() {
579                                                   return CtsConnectionService
580                                                           .getAllConnectionsFromTelecom()
581                                                           .size();
582                                               }
583                                           },
584                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
585                 "ConnectionService should contain " + numConnections + " connections."
586         );
587     }
588 
assertNumConnections(final MockConnectionService connService, final int numConnections)589     void assertNumConnections(final MockConnectionService connService, final int numConnections) {
590         waitUntilConditionIsTrueOrTimeout(new Condition() {
591                                               @Override
592                                               public Object expected() {
593                                                   return numConnections;
594                                               }
595                                               @Override
596                                               public Object actual() {
597                                                   return connService.getAllConnections().size();
598                                               }
599                                           },
600                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
601                 "ConnectionService should contain " + numConnections + " connections."
602         );
603     }
604 
assertMuteState(final InCallService incallService, final boolean isMuted)605     void assertMuteState(final InCallService incallService, final boolean isMuted) {
606         waitUntilConditionIsTrueOrTimeout(
607                 new Condition() {
608                     @Override
609                     public Object expected() {
610                         return isMuted;
611                     }
612 
613                     @Override
614                     public Object actual() {
615                         final CallAudioState state = incallService.getCallAudioState();
616                         return state == null ? null : state.isMuted();
617                     }
618                 },
619                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
620                 "Phone's mute state should be: " + isMuted
621         );
622     }
623 
assertMuteState(final MockConnection connection, final boolean isMuted)624     void assertMuteState(final MockConnection connection, final boolean isMuted) {
625         waitUntilConditionIsTrueOrTimeout(
626                 new Condition() {
627                     @Override
628                     public Object expected() {
629                         return isMuted;
630                     }
631 
632                     @Override
633                     public Object actual() {
634                         final CallAudioState state = connection.getCallAudioState();
635                         return state == null ? null : state.isMuted();
636                     }
637                 },
638                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
639                 "Connection's mute state should be: " + isMuted
640         );
641     }
642 
assertAudioRoute(final InCallService incallService, final int route)643     void assertAudioRoute(final InCallService incallService, final int route) {
644         waitUntilConditionIsTrueOrTimeout(
645                 new Condition() {
646                     @Override
647                     public Object expected() {
648                         return route;
649                     }
650 
651                     @Override
652                     public Object actual() {
653                         final CallAudioState state = incallService.getCallAudioState();
654                         return state == null ? null : state.getRoute();
655                     }
656                 },
657                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
658                 "Phone's audio route should be: " + route
659         );
660     }
661 
assertNotAudioRoute(final InCallService incallService, final int route)662     void assertNotAudioRoute(final InCallService incallService, final int route) {
663         waitUntilConditionIsTrueOrTimeout(
664                 new Condition() {
665                     @Override
666                     public Object expected() {
667                         return new Boolean(true);
668                     }
669 
670                     @Override
671                     public Object actual() {
672                         final CallAudioState state = incallService.getCallAudioState();
673                         return route != state.getRoute();
674                     }
675                 },
676                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
677                 "Phone's audio route should not be: " + route
678         );
679     }
680 
assertAudioRoute(final MockConnection connection, final int route)681     void assertAudioRoute(final MockConnection connection, final int route) {
682         waitUntilConditionIsTrueOrTimeout(
683                 new Condition() {
684                     @Override
685                     public Object expected() {
686                         return route;
687                     }
688 
689                     @Override
690                     public Object actual() {
691                         final CallAudioState state = ((Connection) connection).getCallAudioState();
692                         return state == null ? null : state.getRoute();
693                     }
694                 },
695                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
696                 "Connection's audio route should be: " + route
697         );
698     }
699 
assertConnectionState(final Connection connection, final int state)700     void assertConnectionState(final Connection connection, final int state) {
701         waitUntilConditionIsTrueOrTimeout(
702                 new Condition() {
703                     @Override
704                     public Object expected() {
705                         return state;
706                     }
707 
708                     @Override
709                     public Object actual() {
710                         return connection.getState();
711                     }
712                 },
713                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
714                 "Connection should be in state " + state
715         );
716     }
717 
assertCallState(final Call call, final int state)718     void assertCallState(final Call call, final int state) {
719         waitUntilConditionIsTrueOrTimeout(
720                 new Condition() {
721                     @Override
722                     public Object expected() {
723                         return state;
724                     }
725 
726                     @Override
727                     public Object actual() {
728                         return call.getState();
729                     }
730                 },
731                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
732                 "Call: " + call + " should be in state " + state
733         );
734     }
735 
assertCallConferenceableList(final Call call, final List<Call> conferenceableList)736     void assertCallConferenceableList(final Call call, final List<Call> conferenceableList) {
737         waitUntilConditionIsTrueOrTimeout(
738                 new Condition() {
739                     @Override
740                     public Object expected() {
741                         return conferenceableList;
742                     }
743 
744                     @Override
745                     public Object actual() {
746                         return call.getConferenceableCalls();
747                     }
748                 },
749                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
750                 "Call: " + call + " does not have the correct conferenceable call list."
751         );
752     }
753 
assertDtmfString(final MockConnection connection, final String dtmfString)754     void assertDtmfString(final MockConnection connection, final String dtmfString) {
755         waitUntilConditionIsTrueOrTimeout(new Condition() {
756                 @Override
757                 public Object expected() {
758                     return dtmfString;
759                 }
760 
761                 @Override
762                 public Object actual() {
763                     return connection.getDtmfString();
764                 }
765             },
766             WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
767             "DTMF string should be equivalent to entered DTMF characters: " + dtmfString
768         );
769     }
770 
assertDtmfString(final MockConference conference, final String dtmfString)771     void assertDtmfString(final MockConference conference, final String dtmfString) {
772         waitUntilConditionIsTrueOrTimeout(new Condition() {
773                 @Override
774                 public Object expected() {
775                     return dtmfString;
776                 }
777 
778                 @Override
779                 public Object actual() {
780                     return conference.getDtmfString();
781                 }
782             },
783             WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
784             "DTMF string should be equivalent to entered DTMF characters: " + dtmfString
785         );
786     }
787 
assertCallDisplayName(final Call call, final String name)788     void assertCallDisplayName(final Call call, final String name) {
789         waitUntilConditionIsTrueOrTimeout(
790                 new Condition() {
791                     @Override
792                     public Object expected() {
793                         return name;
794                     }
795 
796                     @Override
797                     public Object actual() {
798                         return call.getDetails().getCallerDisplayName();
799                     }
800                 },
801                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
802                 "Call should have display name: " + name
803         );
804     }
805 
assertConnectionCallDisplayName(final Connection connection, final String name)806     void assertConnectionCallDisplayName(final Connection connection, final String name) {
807         waitUntilConditionIsTrueOrTimeout(
808                 new Condition() {
809                     @Override
810                     public Object expected() {
811                         return name;
812                     }
813 
814                     @Override
815                     public Object actual() {
816                         return connection.getCallerDisplayName();
817                     }
818                 },
819                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
820                 "Connection should have display name: " + name
821         );
822     }
823 
assertDisconnectReason(final Connection connection, final String disconnectReason)824     void assertDisconnectReason(final Connection connection, final String disconnectReason) {
825         waitUntilConditionIsTrueOrTimeout(
826                 new Condition() {
827                     @Override
828                     public Object expected() {
829                         return disconnectReason;
830                     }
831 
832                     @Override
833                     public Object actual() {
834                         return connection.getDisconnectCause().getReason();
835                     }
836                 },
837                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
838                 "Connection should have been disconnected with reason: " + disconnectReason
839         );
840     }
841 
assertConferenceState(final Conference conference, final int state)842     void assertConferenceState(final Conference conference, final int state) {
843         waitUntilConditionIsTrueOrTimeout(
844                 new Condition() {
845                     @Override
846                     public Object expected() {
847                         return state;
848                     }
849 
850                     @Override
851                     public Object actual() {
852                         return conference.getState();
853                     }
854                 },
855                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
856                 "Conference should be in state " + state
857         );
858     }
859 
assertPhoneAccountRegistered(final PhoneAccountHandle handle)860     void assertPhoneAccountRegistered(final PhoneAccountHandle handle) {
861         waitUntilConditionIsTrueOrTimeout(
862                 new Condition() {
863                     @Override
864                     public Object expected() {
865                         return true;
866                     }
867 
868                     @Override
869                     public Object actual() {
870                         return mTelecomManager.getPhoneAccount(handle) != null;
871                     }
872                 },
873                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
874                 "Phone account registration failed for " + handle
875         );
876     }
877 
assertPhoneAccountEnabled(final PhoneAccountHandle handle)878     void assertPhoneAccountEnabled(final PhoneAccountHandle handle) {
879         waitUntilConditionIsTrueOrTimeout(
880                 new Condition() {
881                     @Override
882                     public Object expected() {
883                         return true;
884                     }
885 
886                     @Override
887                     public Object actual() {
888                         PhoneAccount phoneAccount = mTelecomManager.getPhoneAccount(handle);
889                         return (phoneAccount != null && phoneAccount.isEnabled());
890                     }
891                 },
892                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
893                 "Phone account enable failed for " + handle
894         );
895     }
896 
assertCtsConnectionServiceUnbound()897     void assertCtsConnectionServiceUnbound() {
898         waitUntilConditionIsTrueOrTimeout(
899                 new Condition() {
900                     @Override
901                     public Object expected() {
902                         return false;
903                     }
904 
905                     @Override
906                     public Object actual() {
907                         return CtsConnectionService.isServiceBound();
908                     }
909                 },
910                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
911                 "CtsConnectionService not yet unbound!"
912         );
913     }
914 
assertMockInCallServiceUnbound()915     void assertMockInCallServiceUnbound() {
916         waitUntilConditionIsTrueOrTimeout(
917                 new Condition() {
918                     @Override
919                     public Object expected() {
920                         return false;
921                     }
922 
923                     @Override
924                     public Object actual() {
925                         return MockInCallService.isServiceBound();
926                     }
927                 },
928                 WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
929                 "MockInCallService not yet unbound!"
930         );
931     }
932 
933     /**
934      * Asserts that a call's properties are as expected.
935      *
936      * @param call The call.
937      * @param properties The expected properties.
938      */
assertCallProperties(final Call call, final int properties)939     public void assertCallProperties(final Call call, final int properties) {
940         waitUntilConditionIsTrueOrTimeout(
941                 new Condition() {
942                     @Override
943                     public Object expected() {
944                         return true;
945                     }
946 
947                     @Override
948                     public Object actual() {
949                         return call.getDetails().hasProperty(properties);
950                     }
951                 },
952                 TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
953                 "Call should have properties " + properties
954         );
955     }
956 
957     /**
958      * Asserts that a call's capabilities are as expected.
959      *
960      * @param call The call.
961      * @param capabilities The expected capabiltiies.
962      */
assertCallCapabilities(final Call call, final int capabilities)963     public void assertCallCapabilities(final Call call, final int capabilities) {
964         waitUntilConditionIsTrueOrTimeout(
965                 new Condition() {
966                     @Override
967                     public Object expected() {
968                         return true;
969                     }
970 
971                     @Override
972                     public Object actual() {
973                         return (call.getDetails().getCallCapabilities() & capabilities) ==
974                                 capabilities;
975                     }
976                 },
977                 TestUtils.WAIT_FOR_STATE_CHANGE_TIMEOUT_MS,
978                 "Call should have properties " + capabilities
979         );
980     }
981 
waitUntilConditionIsTrueOrTimeout(Condition condition, long timeout, String description)982     void waitUntilConditionIsTrueOrTimeout(Condition condition, long timeout,
983             String description) {
984         final long start = System.currentTimeMillis();
985         while (!condition.expected().equals(condition.actual())
986                 && System.currentTimeMillis() - start < timeout) {
987             sleep(50);
988         }
989         assertEquals(description, condition.expected(), condition.actual());
990     }
991 
992     /**
993      * Performs some work, and waits for the condition to be met.  If the condition is not met in
994      * each step of the loop, the work is performed again.
995      *
996      * @param work The work to perform.
997      * @param condition The condition.
998      * @param timeout The timeout.
999      * @param description Description of the work being performed.
1000      */
doWorkAndWaitUntilConditionIsTrueOrTimeout(Work work, Condition condition, long timeout, String description)1001     void doWorkAndWaitUntilConditionIsTrueOrTimeout(Work work, Condition condition, long timeout,
1002             String description) {
1003         final long start = System.currentTimeMillis();
1004         work.doWork();
1005         while (!condition.expected().equals(condition.actual())
1006                 && System.currentTimeMillis() - start < timeout) {
1007             sleep(50);
1008             work.doWork();
1009         }
1010         assertEquals(description, condition.expected(), condition.actual());
1011     }
1012 
1013     protected interface Condition {
expected()1014         Object expected();
actual()1015         Object actual();
1016     }
1017 
1018     protected interface Work {
doWork()1019         void doWork();
1020     }
1021 
1022     /**
1023      * Utility class used to track the number of times a callback was invoked, and the arguments it
1024      * was invoked with. This class is prefixed Invoke rather than the more typical Call for
1025      * disambiguation purposes.
1026      */
1027     public static final class InvokeCounter {
1028         private final String mName;
1029         private final Object mLock = new Object();
1030         private final ArrayList<Object[]> mInvokeArgs = new ArrayList<>();
1031 
1032         private int mInvokeCount;
1033 
InvokeCounter(String callbackName)1034         public InvokeCounter(String callbackName) {
1035             mName = callbackName;
1036         }
1037 
invoke(Object... args)1038         public void invoke(Object... args) {
1039             synchronized (mLock) {
1040                 mInvokeCount++;
1041                 mInvokeArgs.add(args);
1042                 mLock.notifyAll();
1043             }
1044         }
1045 
getArgs(int index)1046         public Object[] getArgs(int index) {
1047             synchronized (mLock) {
1048                 return mInvokeArgs.get(index);
1049             }
1050         }
1051 
getInvokeCount()1052         public int getInvokeCount() {
1053             synchronized (mLock) {
1054                 return mInvokeCount;
1055             }
1056         }
1057 
waitForCount(int count)1058         public void waitForCount(int count) {
1059             waitForCount(count, WAIT_FOR_STATE_CHANGE_TIMEOUT_MS);
1060         }
1061 
waitForCount(int count, long timeoutMillis)1062         public void waitForCount(int count, long timeoutMillis) {
1063             waitForCount(count, timeoutMillis, null);
1064         }
1065 
waitForCount(long timeoutMillis)1066         public void waitForCount(long timeoutMillis) {
1067              synchronized (mLock) {
1068              try {
1069                   mLock.wait(timeoutMillis);
1070              }catch (InterruptedException ex) {
1071                   ex.printStackTrace();
1072              }
1073            }
1074         }
1075 
waitForCount(int count, long timeoutMillis, String message)1076         public void waitForCount(int count, long timeoutMillis, String message) {
1077             synchronized (mLock) {
1078                 final long startTimeMillis = SystemClock.uptimeMillis();
1079                 while (mInvokeCount < count) {
1080                     try {
1081                         final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
1082                         final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis;
1083                         if (remainingTimeMillis <= 0) {
1084                             if (message != null) {
1085                                 fail(message);
1086                             } else {
1087                                 fail(String.format("Expected %s to be called %d times.", mName,
1088                                         count));
1089                             }
1090                         }
1091                         mLock.wait(timeoutMillis);
1092                     } catch (InterruptedException ie) {
1093                         /* ignore */
1094                     }
1095                 }
1096             }
1097         }
1098     }
1099 
areBundlesEqual(Bundle extras, Bundle newExtras)1100     public static boolean areBundlesEqual(Bundle extras, Bundle newExtras) {
1101         if (extras == null || newExtras == null) {
1102             return extras == newExtras;
1103         }
1104 
1105         if (extras.size() != newExtras.size()) {
1106             return false;
1107         }
1108 
1109         for (String key : extras.keySet()) {
1110             if (key != null) {
1111                 final Object value = extras.get(key);
1112                 final Object newValue = newExtras.get(key);
1113                 if (!Objects.equals(value, newValue)) {
1114                     return false;
1115                 }
1116             }
1117         }
1118         return true;
1119     }
1120 }
1121