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