1 /*
2  * Copyright (C) 2023 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.apps.transactionalvoipappmain;
18 
19 import static android.telecom.Call.STATE_ACTIVE;
20 import static android.telecom.Call.STATE_DISCONNECTED;
21 import static android.telecom.Call.STATE_HOLDING;
22 import static android.telecom.cts.apps.AssertOutcome.assertCountDownLatchWasCalled;
23 import static android.telecom.cts.apps.NotificationUtils.isTargetNotificationPosted;
24 import static android.telecom.cts.apps.StackTraceUtil.appendStackTraceList;
25 import static android.telecom.cts.apps.StackTraceUtil.createStackTraceList;
26 import static android.telecom.cts.apps.TelecomTestApp.TRANSACTIONAL_CLONE_ACCOUNT;
27 import static android.telecom.cts.apps.TelecomTestApp.TRANSACTIONAL_CLONE_PACKAGE_NAME;
28 import static android.telecom.cts.apps.TelecomTestApp.TRANSACTIONAL_MAIN_DEFAULT_ACCOUNT;
29 import static android.telecom.cts.apps.TelecomTestApp.T_CONTROL_INTERFACE_ACTION;
30 import static android.telecom.cts.apps.WaitUntil.waitUntilAvailableEndpointAreSet;
31 import static android.telecom.cts.apps.WaitUntil.waitUntilCurrentCallEndpointIsSet;
32 
33 import android.app.NotificationChannel;
34 import android.app.NotificationManager;
35 import android.app.Service;
36 import android.content.Intent;
37 import android.os.Bundle;
38 import android.os.IBinder;
39 import android.os.OutcomeReceiver;
40 import android.os.Process;
41 import android.os.RemoteException;
42 import android.os.UserHandle;
43 import android.telecom.CallAttributes;
44 import android.telecom.CallControl;
45 import android.telecom.CallEndpoint;
46 import android.telecom.CallException;
47 import android.telecom.DisconnectCause;
48 import android.telecom.PhoneAccount;
49 import android.telecom.PhoneAccountHandle;
50 import android.telecom.TelecomManager;
51 import android.telecom.cts.apps.AvailableEndpointsTransaction;
52 import android.telecom.cts.apps.BooleanTransaction;
53 import android.telecom.cts.apps.CallEndpointTransaction;
54 import android.telecom.cts.apps.CallExceptionTransaction;
55 import android.telecom.cts.apps.CallResources;
56 import android.telecom.cts.apps.IAppControl;
57 import android.telecom.cts.apps.LatchedOutcomeReceiver;
58 import android.telecom.cts.apps.NoDataTransaction;
59 import android.telecom.cts.apps.NotificationUtils;
60 import android.telecom.cts.apps.PhoneAccountTransaction;
61 import android.telecom.cts.apps.TestAppException;
62 import android.telecom.cts.apps.TestAppTransaction;
63 import android.telecom.cts.apps.TransactionalCall;
64 import android.util.Log;
65 
66 import androidx.annotation.NonNull;
67 import androidx.annotation.Nullable;
68 
69 import java.util.HashMap;
70 import java.util.List;
71 import java.util.concurrent.CountDownLatch;
72 
73 public class TransactionalVoipAppControlMain extends Service {
74     private String mTag = TransactionalVoipAppControlMain.class.getSimpleName();
75     private String mPackageName = TransactionalVoipAppControlMain.class.getPackageName();
76     private String mClassName = TransactionalVoipAppControlMain.class.getCanonicalName();
77     private static int sNextNotificationId = 200;
78     private TelecomManager mTelecomManager = null;
79     private NotificationManager mNotificationManager = null;
80     private boolean mIsBound = false;
81     public PhoneAccount mPhoneAccount = TRANSACTIONAL_MAIN_DEFAULT_ACCOUNT;
82     private final String NOTIFICATION_CHANNEL_ID = mTag;
83     private final String NOTIFICATION_CHANNEL_NAME = mPackageName + " Notification Channel";
84     private final HashMap<String, TransactionalCall> mIdToControl = new HashMap<>();
85 
86     private final IBinder mBinder = new IAppControl.Stub() {
87 
88         @Override
89         public boolean isBound() throws RemoteException {
90             Log.i(mTag, String.format("isBound: [%b]", mIsBound));
91             return mIsBound;
92         }
93 
94         @Override
95         public UserHandle getProcessUserHandle(){
96             return Process.myUserHandle();
97         }
98 
99         @Override
100         public int getProcessUid(){
101             return Process.myUid();
102         }
103 
104         @Override
105         public NoDataTransaction addCall(CallAttributes callAttributes) throws RemoteException {
106             Log.i(mTag, String.format("addCall: w/ attributes=[%s]", callAttributes));
107             try {
108                 List<String> stackTrace = createStackTraceList(mClassName
109                         + ".addCall(" + callAttributes + ")");
110                 maybeInitTelecomManager();
111                 final CountDownLatch latch = new CountDownLatch(1);
112                 final TransactionalCall call = new TransactionalCall(getApplicationContext(),
113                         new CallResources(
114                                 getApplicationContext(),
115                                 callAttributes,
116                                 NOTIFICATION_CHANNEL_ID,
117                                 sNextNotificationId++));
118 
119                 mTelecomManager.addCall(callAttributes, Runnable::run, new OutcomeReceiver<>() {
120                     @Override
121                     public void onResult(CallControl callControl) {
122                         Log.i(mTag, "onResult: adding callControl to callObject");
123                         verifyCallControlIsNonNull(callControl, stackTrace);
124                         call.setCallControlAndId(callControl);
125                         mIdToControl.put(call.getId(), call);
126                         latch.countDown();
127                     }
128 
129                     @Override
130                     public void onError(@NonNull CallException exception) {
131                         Log.i(mTag, "onError: exception: " + exception);
132                         throw new TestAppException(mPackageName, stackTrace,
133                                 "expected:<CallControl transaction to complete with onResult()>"
134                                         + "actual:<Failed with CallException=["
135                                         + exception.getMessage() + "]>");
136                     }
137                 }, call.mHandshakes, call.mEvents);
138 
139                 assertCountDownLatchWasCalled(
140                         mPackageName,
141                         stackTrace,
142                         "expected: TelecomManager#addCall to return the CallControl"
143                                 + " object within <time> window  actual: timeout",
144                         latch);
145 
146                 return new NoDataTransaction(TestAppTransaction.Success);
147             } catch (TestAppException e) {
148                 return new NoDataTransaction(TestAppTransaction.Failure, e);
149             }
150         }
151 
152         private void verifyCallControlIsNonNull(CallControl callControl, List<String> stackTrace)
153                 throws TestAppException {
154             if (callControl == null) {
155                 throw new TestAppException(mPackageName, stackTrace,
156                         "expected:<CallControl to be non-null>"
157                                 + "actual:<The CallControl object is Null>");
158             }
159         }
160 
161         @Override
162         public CallExceptionTransaction transitionCallStateTo(String id,
163                 int state,
164                 boolean expectSuccess,
165                 Bundle extras) throws RemoteException {
166             Log.i(mTag, "transitionCallStateTo: attempting to transition callId=" + id);
167             List<String> stackTrace = createStackTraceList(mClassName
168                     + ".transitionCallStateTo(" + (id) + ")");
169             try {
170                 final CountDownLatch latch = new CountDownLatch(1);
171                 final LatchedOutcomeReceiver outcome =
172                         new LatchedOutcomeReceiver(latch);
173 
174                 TransactionalCall call = getCallOrThrowError(id, stackTrace);
175                 CallControl callControl = call.getCallControl();
176 
177                 if (callControl == null) {
178                     throw new TestAppException(mPackageName, stackTrace,
179                             String.format("CalControl is NULL for callId=[%s]", id));
180                 }
181 
182                 switch (state) {
183                     case STATE_ACTIVE -> {
184                         call.setActive(outcome, extras);
185                     }
186                     case STATE_HOLDING -> {
187                         call.setInactive(outcome);
188                     }
189                     case STATE_DISCONNECTED -> {
190                         call.disconnect(outcome, extras);
191                         mIdToControl.remove(id);
192                     }
193                 }
194                 Log.i(mTag, "transitionCallStateTo: done");
195 
196                 // execution should be paused until the OutcomeReceiver#onResult or
197                 // OutcomeReceiver#onError is called for the given transaction. If a timeout occurs,
198                 // bubble up the timeout exception
199                 assertCountDownLatchWasCalled(mPackageName, stackTrace,
200                         "expected:<CountDownLatch to count down within the time window>"
201                                 + "  actual:<Timeout; Inspect the Transactions to determine cause>",
202                         latch);
203 
204                 // if the call control transaction should have resulted in OutcomeReceiver#onResult
205                 // being called instead of the OutcomeReceiver#onError, fail the test and
206                 // bubble up the exception
207                 verifyCallControlTransactionWasSuccessful(expectSuccess, stackTrace, outcome);
208 
209                 // There may be times when it is EXPECTED (from the Test) that the CallControl
210                 // transaction fails via OutcomeReceiver#onError. In these cases, return the
211                 // CallException to the test.
212                 if (testExpectsOnErrorAndShouldReturnCallException(expectSuccess, outcome)) {
213                     return new CallExceptionTransaction(TestAppTransaction.Success,
214                             outcome.getmCallException());
215                 }
216 
217                 // otherwise, the CallControl transaction completed successfully!
218                 return new CallExceptionTransaction(TestAppTransaction.Success);
219             } catch (TestAppException e) {
220                 return new CallExceptionTransaction(TestAppTransaction.Failure, e);
221             }
222         }
223 
224         private void verifyCallControlTransactionWasSuccessful(boolean expectSuccess,
225                 List<String> stackTrace,
226                 LatchedOutcomeReceiver outcome)
227                 throws TestAppException {
228             if (expectSuccess && !outcome.wasSuccessful()) {
229                 throw new TestAppException(mPackageName, stackTrace,
230                         "expected:<CallControl transaction to complete with onResult()>"
231                                 + "actual:<Failed with CallException=["
232                                 + outcome.getmCallException().getMessage() + "]>");
233             }
234         }
235 
236         private boolean testExpectsOnErrorAndShouldReturnCallException(boolean expectSuccess,
237                 LatchedOutcomeReceiver outcome) {
238             return !expectSuccess && !outcome.wasSuccessful();
239         }
240 
241         @Override
242         public BooleanTransaction isMuted(String id) throws RemoteException {
243             Log.i(mTag, String.format("isMuted: id=[%s]", id));
244             try {
245                 TransactionalCall call = getCallOrThrowError(id,
246                         createStackTraceList(mClassName + ".isMuted(" + (id) + ")"));
247 
248                 return new BooleanTransaction(TestAppTransaction.Success,
249                         call.mEvents.getIsMuted());
250             } catch (TestAppException e) {
251                 return new BooleanTransaction(TestAppTransaction.Failure, e);
252             }
253         }
254 
255         @Override
256         public NoDataTransaction setMuteState(String id, boolean isMuted) {
257             Log.i(mTag, String.format("setMuteState: id=[%s], isMuted=[%b]", id, isMuted));
258             // TODO:: b/310669304
259             return new NoDataTransaction(TestAppTransaction.Failure,
260                     new TestAppException(
261                             mPackageName,
262                             createStackTraceList(mClassName + "setMuteState"),
263                             "TransactionalVoipApp* does not implement setMuteState b/c there is"
264                                     + " no existing API in android.telecom.CallControl!")
265             );
266         }
267 
268         @Override
269         public CallEndpointTransaction getCurrentCallEndpoint(String id) throws RemoteException {
270             Log.i(mTag, String.format("getCurrentCallEndpoint: id=[%s]", id));
271             try {
272                 TransactionalCall call = getCallOrThrowError(id,
273                         createStackTraceList(mClassName + ".getCurrentCallEndpoint(" + (id) + ")"));
274 
275                 waitUntilCurrentCallEndpointIsSet(
276                         mPackageName,
277                         createStackTraceList(mClassName
278                                 + ".getCurrentCallEndpoint(id=" + id + ")"),
279                         call.mEvents);
280 
281                 return new CallEndpointTransaction(
282                         TestAppTransaction.Success,
283                         call.mEvents.getCurrentCallEndpoint());
284             } catch (TestAppException e) {
285                 return new CallEndpointTransaction(TestAppTransaction.Failure, e);
286             }
287         }
288 
289         @Override
290         public AvailableEndpointsTransaction getAvailableCallEndpoints(String id)
291                 throws RemoteException {
292             Log.i(mTag, String.format("getAvailableCallEndpoints: id=[%s]", id));
293             try {
294                 TransactionalCall call = getCallOrThrowError(id,
295                         createStackTraceList(mClassName
296                                 + ".getAvailableCallEndpoints(" + (id) + ")"));
297 
298                 waitUntilAvailableEndpointAreSet(
299                         mPackageName,
300                         createStackTraceList(mClassName
301                                 + ".getAvailableCallEndpoints(id=" + id + ")"),
302                         call.mEvents);
303 
304                 return new AvailableEndpointsTransaction(
305                         TestAppTransaction.Success,
306                         call.mEvents.getCallEndpoints());
307             } catch (TestAppException e) {
308                 return new AvailableEndpointsTransaction(TestAppTransaction.Failure, e);
309             }
310         }
311 
312         @Override
313         public NoDataTransaction requestCallEndpointChange(String id, CallEndpoint callEndpoint)
314                 throws RemoteException {
315             Log.i(mTag, String.format("requestCallEndpointChange:"
316                     + " id=[%s], callEndpoint=[%s]", id, callEndpoint));
317             try {
318                 List<String> stackTrace = createStackTraceList(mClassName
319                         + ".requestCallEndpointChange(" + (id) + ")");
320 
321                 TransactionalCall call = getCallOrThrowError(id, stackTrace);
322 
323                 final CountDownLatch latch = new CountDownLatch(1);
324                 final LatchedOutcomeReceiver outcome = new LatchedOutcomeReceiver(latch);
325 
326                 // send the call control request
327                 call.getCallControl().requestCallEndpointChange(callEndpoint, Runnable::run,
328                         outcome);
329                 // await a count down in the CountDownLatch to signify success
330                 assertCountDownLatchWasCalled(
331                         mPackageName,
332                         stackTrace,
333                         "expected:<CallControl#requestCallEndpointChange to complete via"
334                                 + " onResult or onError>  "
335                                 + "actual<Timeout waiting for the CountDownLatch to complete.>",
336                         latch);
337 
338                 return new NoDataTransaction(TestAppTransaction.Success);
339             } catch (TestAppException e) {
340                 return new NoDataTransaction(TestAppTransaction.Failure, e);
341             }
342         }
343 
344         @Override
345         public NoDataTransaction registerDefaultPhoneAccount() {
346             Log.i(mTag, String.format("registerDefaultPhoneAccount: pa=[%s]", mPhoneAccount));
347             mTelecomManager.registerPhoneAccount(mPhoneAccount);
348             return new NoDataTransaction(TestAppTransaction.Success);
349         }
350 
351         @Override
352         public PhoneAccountTransaction getDefaultPhoneAccount() {
353             Log.i(mTag, String.format("getDefaultPhoneAccount: pa=[%s]", mPhoneAccount));
354             return new PhoneAccountTransaction(TestAppTransaction.Success, mPhoneAccount);
355         }
356 
357         @Override
358         public void registerCustomPhoneAccount(PhoneAccount account) {
359             Log.i(mTag, String.format("registerCustomPhoneAccount: account=[%s]", account));
360             mTelecomManager.registerPhoneAccount(account);
361         }
362 
363         @Override
364         public void unregisterPhoneAccountWithHandle(PhoneAccountHandle handle) {
365             Log.i(mTag, String.format("unregisterPhoneAccountWithHandle: handle=[%s]", handle));
366             mTelecomManager.unregisterPhoneAccount(handle);
367         }
368 
369         @Override
370         public List<PhoneAccountHandle> getOwnAccountHandlesForApp() {
371             Log.i(mTag, "getOwnAccountHandlesForApp");
372             return mTelecomManager.getOwnSelfManagedPhoneAccounts();
373         }
374 
375         @Override
376         public List<PhoneAccount> getRegisteredPhoneAccounts() {
377             Log.i(mTag, "getRegisteredPhoneAccounts");
378             return mTelecomManager.getRegisteredPhoneAccounts();
379         }
380 
381         public BooleanTransaction isNotificationPostedForCall(String callId) {
382             List<String> stackTrace = createStackTraceList(mClassName
383                     + ".isNotificationPostedForCall(" + (callId) + ")");
384             try {
385                 int targetNotificationId = getCallOrThrowError(callId,
386                         stackTrace).getCallResources()
387                         .getNotificationId();
388 
389                 return new BooleanTransaction(TestAppTransaction.Success,
390                         isTargetNotificationPosted(getApplicationContext(),
391                                 targetNotificationId));
392             } catch (TestAppException e) {
393                 return new BooleanTransaction(TestAppTransaction.Failure, e);
394             }
395         }
396 
397         @Override
398         public NoDataTransaction removeNotificationForCall(String callId) {
399             List<String> stackTrace = createStackTraceList(mClassName
400                     + ".removeNotificationForCall(" + (callId) + ")");
401             try {
402                 TransactionalCall call = getCallOrThrowError(callId, stackTrace);
403                 CallResources callResources = call.getCallResources();
404                 callResources.clearCallNotification(getApplicationContext());
405                 return new NoDataTransaction(TestAppTransaction.Success);
406             } catch (TestAppException e) {
407                 return new NoDataTransaction(TestAppTransaction.Failure, e);
408             }
409         }
410 
411         private TransactionalCall getCallOrThrowError(String id, List<String> stackTrace) {
412             if (!mIdToControl.containsKey(id)) {
413                 throw new TestAppException(mPackageName,
414                         appendStackTraceList(stackTrace, mClassName + ".getCallOrThrowError"),
415                         "expect:<A TransactionalCall object in the mIdToControl map>"
416                                 + "actual: missing TransactionalCall object for key=" + id);
417             }
418             return mIdToControl.get(id);
419         }
420 
421         @Override
422         public void cleanup() {
423             cleanupImplementation();
424         }
425     };
426 
427     @Nullable
428     @Override
onBind(Intent intent)429     public IBinder onBind(Intent intent) {
430         if (T_CONTROL_INTERFACE_ACTION.equals(intent.getAction())) {
431             Log.i(mTag, String.format("onBind: return control interface w/ intent=[%s]", intent));
432             mIsBound = true;
433             maybeInitTelecomManager();
434             initNotificationChannel();
435             setDefaultPhoneAccountBasedOffIntent(intent);
436             mTelecomManager.registerPhoneAccount(mPhoneAccount);
437             return mBinder;
438         }
439         Log.i(mTag, "onBind: return control interface=" + intent);
440         return null;
441     }
442 
disconnectAndDestroyAllCalls()443     private void disconnectAndDestroyAllCalls() {
444         for (TransactionalCall call : mIdToControl.values()) {
445             call.getCallControl().disconnect(
446                     new DisconnectCause(
447                             DisconnectCause.LOCAL, "onUnbind for TransactionalApp"),
448                     Runnable::run,
449                     result -> {
450                     });
451             call.getCallResources().destroyResources(getApplicationContext());
452         }
453         mIdToControl.clear();
454     }
455 
unregisterAllPhoneAccounts()456     private void unregisterAllPhoneAccounts() {
457         for (PhoneAccountHandle handle : mTelecomManager.getOwnSelfManagedPhoneAccounts()) {
458             mTelecomManager.unregisterPhoneAccount(handle);
459         }
460     }
461 
cleanupImplementation()462     private void cleanupImplementation() {
463         disconnectAndDestroyAllCalls();
464         unregisterAllPhoneAccounts();
465         // delete the call channel
466         NotificationUtils.deleteNotificationChannel(
467                 getApplicationContext(),
468                 NOTIFICATION_CHANNEL_ID);
469     }
470 
471     @Override
onUnbind(Intent intent)472     public boolean onUnbind(Intent intent) {
473         Log.i(mTag, String.format("onUnbind: with intent=[%s]", intent));
474         cleanupImplementation();
475         mIsBound = false;
476         return super.onUnbind(intent);
477     }
478 
maybeInitTelecomManager()479     private void maybeInitTelecomManager() {
480         Log.d(mTag, "maybeInitTelecomManager:");
481         if (mTelecomManager == null) {
482             mTelecomManager = getSystemService(TelecomManager.class);
483         }
484     }
485 
initNotificationChannel()486     private void initNotificationChannel() {
487         Log.d(mTag, "initNotificationChannel:");
488         if (mNotificationManager == null) {
489             mNotificationManager = getSystemService(NotificationManager.class);
490             mNotificationManager.createNotificationChannel(new NotificationChannel(
491                     NOTIFICATION_CHANNEL_ID,
492                     NOTIFICATION_CHANNEL_NAME,
493                     NotificationManager.IMPORTANCE_DEFAULT));
494         }
495     }
496 
setDefaultPhoneAccountBasedOffIntent(Intent intent)497     public void setDefaultPhoneAccountBasedOffIntent(Intent intent) {
498         if (intent.getPackage().equals(TRANSACTIONAL_CLONE_PACKAGE_NAME)) {
499             mPhoneAccount = TRANSACTIONAL_CLONE_ACCOUNT;
500             mTag = "TransactionalVoipAppControlClone";
501             mClassName = TRANSACTIONAL_CLONE_PACKAGE_NAME + "." + mTag;
502             mPackageName = TRANSACTIONAL_CLONE_PACKAGE_NAME;
503         } else {
504             mPhoneAccount = TRANSACTIONAL_MAIN_DEFAULT_ACCOUNT;
505         }
506         Log.i(mTag, String.format("setDefaultPhoneAccountBasedOffIntent:"
507                 + " mPhoneAccount=[%s], intent=[%s]", mPhoneAccount, intent));
508     }
509 }
510