1 /* 2 * Copyright (C) 2013 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 com.android.server.telecom.testapps; 18 19 import static android.media.AudioAttributes.CONTENT_TYPE_SPEECH; 20 import static android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION; 21 22 import android.content.BroadcastReceiver; 23 import android.content.ComponentName; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.IntentFilter; 27 import android.media.AudioAttributes; 28 import android.media.AudioManager; 29 import android.media.MediaPlayer; 30 import android.media.ToneGenerator; 31 import android.net.Uri; 32 import android.os.Bundle; 33 import android.os.Handler; 34 import androidx.localbroadcastmanager.content.LocalBroadcastManager; 35 import android.telecom.Conference; 36 import android.telecom.Connection; 37 import android.telecom.DisconnectCause; 38 import android.telecom.PhoneAccount; 39 import android.telecom.ConnectionRequest; 40 import android.telecom.ConnectionService; 41 import android.telecom.PhoneAccountHandle; 42 import android.telecom.TelecomManager; 43 import android.telecom.VideoProfile; 44 import android.telecom.Log; 45 import android.widget.Toast; 46 47 import java.lang.String; 48 import java.util.ArrayList; 49 import java.util.List; 50 import java.util.Random; 51 52 import static com.android.server.telecom.testapps.CallServiceNotifier.SIM_SUBSCRIPTION_ID2; 53 54 /** 55 * Service which provides fake calls to test the ConnectionService interface. 56 * TODO: Rename all classes in the directory to Dummy* (e.g., DummyConnectionService). 57 */ 58 public class TestConnectionService extends ConnectionService { 59 /** 60 * Intent extra used to pass along the video state for a new test call. 61 */ 62 public static final String EXTRA_START_VIDEO_STATE = "extra_start_video_state"; 63 64 public static final String EXTRA_HANDLE = "extra_handle"; 65 66 /** 67 * If an outgoing call ends with 2879 (BUSY), the test CS will indicate the call is busy. 68 */ 69 public static final String BUSY_SUFFIX = "2879"; 70 71 private static final String LOG_TAG = TestConnectionService.class.getSimpleName(); 72 73 private static TestConnectionService INSTANCE; 74 75 /** 76 * Random number generator used to generate phone numbers. 77 */ 78 private Random mRandom = new Random(); 79 80 private final class TestConference extends Conference { 81 82 private final Connection.Listener mConnectionListener = new Connection.Listener() { 83 @Override 84 public void onDestroyed(Connection c) { 85 removeConnection(c); 86 if (getConnections().size() == 0) { 87 setDisconnected(new DisconnectCause(DisconnectCause.REMOTE)); 88 destroy(); 89 } 90 } 91 }; 92 TestConference(Connection a, Connection b)93 public TestConference(Connection a, Connection b) { 94 super(null); 95 setConnectionCapabilities( 96 Connection.CAPABILITY_SUPPORT_HOLD | 97 Connection.CAPABILITY_HOLD | 98 Connection.CAPABILITY_MUTE | 99 Connection.CAPABILITY_MANAGE_CONFERENCE); 100 addConnection(a); 101 addConnection(b); 102 103 a.addConnectionListener(mConnectionListener); 104 b.addConnectionListener(mConnectionListener); 105 106 a.setConference(this); 107 b.setConference(this); 108 109 setActive(); 110 } 111 112 @Override onDisconnect()113 public void onDisconnect() { 114 for (Connection c : getConnections()) { 115 c.setDisconnected(new DisconnectCause(DisconnectCause.REMOTE)); 116 c.destroy(); 117 } 118 } 119 120 @Override onSeparate(Connection connection)121 public void onSeparate(Connection connection) { 122 if (getConnections().contains(connection)) { 123 connection.setConference(null); 124 removeConnection(connection); 125 connection.removeConnectionListener(mConnectionListener); 126 } 127 } 128 129 @Override onHold()130 public void onHold() { 131 for (Connection c : getConnections()) { 132 c.setOnHold(); 133 } 134 setOnHold(); 135 } 136 137 @Override onUnhold()138 public void onUnhold() { 139 for (Connection c : getConnections()) { 140 c.setActive(); 141 } 142 setActive(); 143 } 144 } 145 146 final class TestConnection extends Connection { 147 private final boolean mIsIncoming; 148 149 /** Used to cleanup camera and media when done with connection. */ 150 private TestVideoProvider mTestVideoCallProvider; 151 private ConnectionRequest mOriginalRequest; 152 private RttChatbot mRttChatbot; 153 154 private BroadcastReceiver mHangupReceiver = new BroadcastReceiver() { 155 @Override 156 public void onReceive(Context context, Intent intent) { 157 setDisconnected(new DisconnectCause(DisconnectCause.MISSED)); 158 destroyCall(TestConnection.this); 159 destroy(); 160 } 161 }; 162 163 private BroadcastReceiver mUpgradeRequestReceiver = new BroadcastReceiver() { 164 @Override 165 public void onReceive(Context context, Intent intent) { 166 final int request = Integer.parseInt(intent.getData().getSchemeSpecificPart()); 167 final VideoProfile videoProfile = new VideoProfile(request); 168 mTestVideoCallProvider.receiveSessionModifyRequest(videoProfile); 169 } 170 }; 171 172 private BroadcastReceiver mRttUpgradeReceiver = new BroadcastReceiver() { 173 @Override 174 public void onReceive(Context context, Intent intent) { 175 sendRemoteRttRequest(); 176 } 177 }; 178 TestConnection(boolean isIncoming, ConnectionRequest request)179 TestConnection(boolean isIncoming, ConnectionRequest request) { 180 mIsIncoming = isIncoming; 181 mOriginalRequest = request; 182 // Assume all calls are video capable. 183 int capabilities = getConnectionCapabilities(); 184 capabilities |= CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL; 185 capabilities |= CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL; 186 capabilities |= CAPABILITY_CAN_UPGRADE_TO_VIDEO; 187 capabilities |= CAPABILITY_MUTE; 188 capabilities |= CAPABILITY_SUPPORT_HOLD; 189 capabilities |= CAPABILITY_HOLD; 190 capabilities |= CAPABILITY_RESPOND_VIA_TEXT; 191 setConnectionCapabilities(capabilities); 192 193 int properties = getConnectionProperties(); 194 if (mOriginalRequest.isRequestingRtt()) { 195 properties |= PROPERTY_IS_RTT; 196 } 197 setConnectionProperties(properties); 198 199 if (isIncoming) { 200 putExtra(Connection.EXTRA_ANSWERING_DROPS_FG_CALL, true); 201 } 202 LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver( 203 mHangupReceiver, new IntentFilter(TestCallActivity.ACTION_HANGUP_CALLS)); 204 final IntentFilter filter = 205 new IntentFilter(TestCallActivity.ACTION_SEND_UPGRADE_REQUEST); 206 filter.addDataScheme("int"); 207 LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver( 208 mUpgradeRequestReceiver, filter); 209 210 LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver( 211 mRttUpgradeReceiver, 212 new IntentFilter(TestCallActivity.ACTION_REMOTE_RTT_UPGRADE)); 213 } 214 startOutgoing()215 void startOutgoing() { 216 setDialing(); 217 mHandler.postDelayed(() -> { 218 if (getAddress().getSchemeSpecificPart().endsWith(BUSY_SUFFIX)) { 219 setDisconnected(new DisconnectCause(DisconnectCause.REMOTE, "Line busy", 220 "Line busy", "Line busy", ToneGenerator.TONE_SUP_BUSY)); 221 destroyCall(this); 222 destroy(); 223 } else { 224 setActive(); 225 activateCall(TestConnection.this); 226 } 227 }, 4000); 228 if (mOriginalRequest.isRequestingRtt()) { 229 Log.i(LOG_TAG, "Is RTT call. Starting chatbot service."); 230 mRttChatbot = new RttChatbot(getApplicationContext(), 231 mOriginalRequest.getRttTextStream()); 232 mRttChatbot.start(); 233 } 234 } 235 236 /** ${inheritDoc} */ 237 @Override onAbort()238 public void onAbort() { 239 destroyCall(this); 240 destroy(); 241 } 242 243 /** ${inheritDoc} */ 244 @Override onAnswer(int videoState)245 public void onAnswer(int videoState) { 246 setVideoState(videoState); 247 activateCall(this); 248 setActive(); 249 updateConferenceable(); 250 if (mOriginalRequest.isRequestingRtt()) { 251 Log.i(LOG_TAG, "Is RTT call. Starting chatbot service."); 252 mRttChatbot = new RttChatbot(getApplicationContext(), 253 mOriginalRequest.getRttTextStream()); 254 mRttChatbot.start(); 255 } 256 } 257 258 /** ${inheritDoc} */ 259 @Override onPlayDtmfTone(char c)260 public void onPlayDtmfTone(char c) { 261 if (c == '1') { 262 setDialing(); 263 } 264 } 265 266 /** ${inheritDoc} */ 267 @Override onStopDtmfTone()268 public void onStopDtmfTone() { } 269 270 /** ${inheritDoc} */ 271 @Override onDisconnect()272 public void onDisconnect() { 273 setDisconnected(new DisconnectCause(DisconnectCause.REMOTE)); 274 destroyCall(this); 275 destroy(); 276 } 277 278 /** ${inheritDoc} */ 279 @Override onHold()280 public void onHold() { 281 setOnHold(); 282 } 283 284 /** ${inheritDoc} */ 285 @Override onReject()286 public void onReject() { 287 setDisconnected(new DisconnectCause(DisconnectCause.REJECTED)); 288 destroyCall(this); 289 destroy(); 290 } 291 292 /** ${inheritDoc} */ 293 @Override onUnhold()294 public void onUnhold() { 295 setActive(); 296 } 297 298 @Override onStopRtt()299 public void onStopRtt() { 300 int newProperties = getConnectionProperties() & ~PROPERTY_IS_RTT; 301 setConnectionProperties(newProperties); 302 mRttChatbot.stop(); 303 mRttChatbot = null; 304 } 305 306 @Override handleRttUpgradeResponse(RttTextStream rttTextStream)307 public void handleRttUpgradeResponse(RttTextStream rttTextStream) { 308 Log.i(this, "RTT request response was %s", rttTextStream == null); 309 if (rttTextStream != null) { 310 mRttChatbot = new RttChatbot(getApplicationContext(), rttTextStream); 311 mRttChatbot.start(); 312 sendRttInitiationSuccess(); 313 } 314 } 315 316 @Override onStartRtt(RttTextStream textStream)317 public void onStartRtt(RttTextStream textStream) { 318 boolean doAccept = Math.random() < 0.5; 319 if (doAccept) { 320 Log.i(this, "Accepting RTT request."); 321 mRttChatbot = new RttChatbot(getApplicationContext(), textStream); 322 mRttChatbot.start(); 323 sendRttInitiationSuccess(); 324 } else { 325 sendRttInitiationFailure(RttModifyStatus.SESSION_MODIFY_REQUEST_FAIL); 326 } 327 } 328 329 public void setTestVideoCallProvider(TestVideoProvider testVideoCallProvider) { 330 mTestVideoCallProvider = testVideoCallProvider; 331 } 332 333 public void cleanup() { 334 LocalBroadcastManager.getInstance(getApplicationContext()).unregisterReceiver( 335 mHangupReceiver); 336 LocalBroadcastManager.getInstance(getApplicationContext()).unregisterReceiver( 337 mUpgradeRequestReceiver); 338 } 339 340 /** 341 * Stops playback of test videos. 342 */ 343 private void stopAndCleanupMedia() { 344 if (mTestVideoCallProvider != null) { 345 mTestVideoCallProvider.stopAndCleanupMedia(); 346 mTestVideoCallProvider.stopCamera(); 347 } 348 } 349 } 350 351 private final List<TestConnection> mCalls = new ArrayList<>(); 352 private final Handler mHandler = new Handler(); 353 354 /** Used to play an audio tone during a call. */ 355 private MediaPlayer mMediaPlayer; 356 357 @Override 358 public void onCreate() { 359 INSTANCE = this; 360 } 361 362 @Override 363 public boolean onUnbind(Intent intent) { 364 log("onUnbind"); 365 mMediaPlayer = null; 366 return super.onUnbind(intent); 367 } 368 369 @Override 370 public void onConference(Connection a, Connection b) { 371 addConference(new TestConference(a, b)); 372 } 373 374 @Override 375 public Connection onCreateOutgoingConnection( 376 PhoneAccountHandle connectionManagerAccount, 377 final ConnectionRequest originalRequest) { 378 379 final Uri handle = originalRequest.getAddress(); 380 String number = originalRequest.getAddress().getSchemeSpecificPart(); 381 log("call, number: " + number); 382 383 // Crash on 555-DEAD to test call service crashing. 384 if ("5550340".equals(number)) { 385 throw new RuntimeException("Goodbye, cruel world."); 386 } 387 388 Bundle extras = originalRequest.getExtras(); 389 String gatewayPackage = extras.getString(TelecomManager.GATEWAY_PROVIDER_PACKAGE); 390 Uri originalHandle = extras.getParcelable(TelecomManager.GATEWAY_ORIGINAL_ADDRESS); 391 392 if (extras.containsKey(TelecomManager.EXTRA_CALL_SUBJECT)) { 393 String callSubject = extras.getString(TelecomManager.EXTRA_CALL_SUBJECT); 394 log("Got subject: " + callSubject); 395 Toast.makeText(getApplicationContext(), "Got subject :" + callSubject, 396 Toast.LENGTH_SHORT).show(); 397 } 398 399 log("gateway package [" + gatewayPackage + "], original handle [" + 400 originalHandle + "]"); 401 402 final TestConnection connection = 403 new TestConnection(false /* isIncoming */, originalRequest); 404 setAddress(connection, handle); 405 406 // If the number starts with 555, then we handle it ourselves. If not, then we 407 // use a remote connection service. 408 // TODO: Have a special phone number to test the account-picker dialog flow. 409 if (number != null && number.startsWith("555")) { 410 // Normally we would use the original request as is, but for testing purposes, we are 411 // adding ".." to the end of the number to follow its path more easily through the logs. 412 final ConnectionRequest request = new ConnectionRequest( 413 originalRequest.getAccountHandle(), 414 Uri.fromParts(handle.getScheme(), 415 handle.getSchemeSpecificPart() + "..", ""), 416 originalRequest.getExtras(), 417 originalRequest.getVideoState()); 418 connection.setVideoState(originalRequest.getVideoState()); 419 addVideoProvider(connection); 420 addCall(connection); 421 connection.startOutgoing(); 422 423 for (Connection c : getAllConnections()) { 424 c.setOnHold(); 425 } 426 } else { 427 log("Not a test number"); 428 } 429 return connection; 430 } 431 432 @Override 433 public Connection onCreateIncomingConnection( 434 PhoneAccountHandle connectionManagerAccount, 435 final ConnectionRequest request) { 436 PhoneAccountHandle accountHandle = request.getAccountHandle(); 437 ComponentName componentName = new ComponentName(this, TestConnectionService.class); 438 439 if (accountHandle != null && componentName.equals(accountHandle.getComponentName())) { 440 final TestConnection connection = new TestConnection(true, request); 441 // Get the stashed intent extra that determines if this is a video call or audio call. 442 Bundle extras = request.getExtras(); 443 int videoState = extras.getInt(EXTRA_START_VIDEO_STATE, VideoProfile.STATE_AUDIO_ONLY); 444 Uri providedHandle = extras.getParcelable(EXTRA_HANDLE); 445 446 // Use dummy number for testing incoming calls. 447 Uri address = providedHandle == null ? 448 Uri.fromParts(PhoneAccount.SCHEME_TEL, getDummyNumber( 449 VideoProfile.isVideo(videoState)), null) 450 : providedHandle; 451 connection.setVideoState(videoState); 452 453 Bundle connectionExtras = connection.getExtras(); 454 if (connectionExtras == null) { 455 connectionExtras = new Bundle(); 456 } 457 458 // Randomly choose a varying length call subject. 459 int subjectFormat = mRandom.nextInt(3); 460 if (subjectFormat == 0) { 461 connectionExtras.putString(Connection.EXTRA_CALL_SUBJECT, 462 "This is a test of call subject lines. Subjects for a call can be long " + 463 " and can go even longer."); 464 } else if (subjectFormat == 1) { 465 connectionExtras.putString(Connection.EXTRA_CALL_SUBJECT, 466 "This is a test of call subject lines."); 467 } 468 469 connection.putExtras(connectionExtras); 470 471 setAddress(connection, address); 472 473 addVideoProvider(connection); 474 475 addCall(connection); 476 477 connection.setVideoState(videoState); 478 return connection; 479 } else { 480 return Connection.createFailedConnection(new DisconnectCause(DisconnectCause.ERROR, 481 "Invalid inputs: " + accountHandle + " " + componentName)); 482 } 483 } 484 485 @Override 486 public Connection onCreateUnknownConnection(PhoneAccountHandle connectionManagerPhoneAccount, 487 final ConnectionRequest request) { 488 PhoneAccountHandle accountHandle = request.getAccountHandle(); 489 ComponentName componentName = new ComponentName(this, TestConnectionService.class); 490 if (accountHandle != null && componentName.equals(accountHandle.getComponentName())) { 491 final TestConnection connection = new TestConnection(false, request); 492 final Bundle extras = request.getExtras(); 493 final Uri providedHandle = extras.getParcelable(EXTRA_HANDLE); 494 495 Uri handle = providedHandle == null ? 496 Uri.fromParts(PhoneAccount.SCHEME_TEL, getDummyNumber(false), null) 497 : providedHandle; 498 499 connection.setAddress(handle, TelecomManager.PRESENTATION_ALLOWED); 500 connection.setDialing(); 501 502 addCall(connection); 503 return connection; 504 } else { 505 return Connection.createFailedConnection(new DisconnectCause(DisconnectCause.ERROR, 506 "Invalid inputs: " + accountHandle + " " + componentName)); 507 } 508 } 509 510 public static TestConnectionService getInstance() { 511 return INSTANCE; 512 } 513 514 public void switchPhoneAccount() { 515 if (!mCalls.isEmpty()) { 516 TestConnection c = mCalls.get(0); 517 c.notifyPhoneAccountChanged(CallServiceNotifier.getInstance() 518 .getPhoneAccountHandle(SIM_SUBSCRIPTION_ID2)); 519 } else { 520 Log.i(this, "Couldn't switch PhoneAccount, call is null!"); 521 } 522 } 523 public void switchPhoneAccountWrong() { 524 PhoneAccountHandle pah = new PhoneAccountHandle( 525 new ComponentName("com.android.phone", 526 "com.android.services.telephony.TelephonyConnectionService"), "TEST"); 527 if (!mCalls.isEmpty()) { 528 TestConnection c = mCalls.get(0); 529 try { 530 c.notifyPhoneAccountChanged(pah); 531 } catch (SecurityException e) { 532 Toast.makeText(getApplicationContext(), "SwitchPhoneAccount: Pass", 533 Toast.LENGTH_SHORT).show(); 534 } 535 } else { 536 Log.i(this, "Couldn't switch PhoneAccount, call is null!"); 537 } 538 } 539 540 private void addVideoProvider(TestConnection connection) { 541 TestVideoProvider testVideoCallProvider = 542 new TestVideoProvider(getApplicationContext(), connection); 543 connection.setVideoProvider(testVideoCallProvider); 544 545 // Keep reference to original so we can clean up the media players later. 546 connection.setTestVideoCallProvider(testVideoCallProvider); 547 } 548 549 private void activateCall(TestConnection connection) { 550 if (mMediaPlayer == null) { 551 mMediaPlayer = createMediaPlayer(); 552 } 553 if (!mMediaPlayer.isPlaying()) { 554 mMediaPlayer.start(); 555 } 556 } 557 558 private void destroyCall(TestConnection connection) { 559 connection.cleanup(); 560 mCalls.remove(connection); 561 562 // Ensure any playing media and camera resources are released. 563 connection.stopAndCleanupMedia(); 564 565 // Stops audio if there are no more calls. 566 if (mCalls.isEmpty() && mMediaPlayer != null && mMediaPlayer.isPlaying()) { 567 mMediaPlayer.stop(); 568 mMediaPlayer.release(); 569 mMediaPlayer = createMediaPlayer(); 570 } 571 572 updateConferenceable(); 573 } 574 575 private void addCall(TestConnection connection) { 576 mCalls.add(connection); 577 updateConferenceable(); 578 } 579 580 private void updateConferenceable() { 581 List<Connection> freeConnections = new ArrayList<>(); 582 freeConnections.addAll(mCalls); 583 for (int i = 0; i < freeConnections.size(); i++) { 584 if (freeConnections.get(i).getConference() != null) { 585 freeConnections.remove(i); 586 } 587 } 588 for (int i = 0; i < freeConnections.size(); i++) { 589 Connection c = freeConnections.remove(i); 590 c.setConferenceableConnections(freeConnections); 591 freeConnections.add(i, c); 592 } 593 } 594 595 private void setAddress(Connection connection, Uri address) { 596 connection.setAddress(address, TelecomManager.PRESENTATION_ALLOWED); 597 if ("5551234".equals(address.getSchemeSpecificPart())) { 598 connection.setCallerDisplayName("Hello World", TelecomManager.PRESENTATION_ALLOWED); 599 } 600 } 601 602 private MediaPlayer createMediaPlayer() { 603 AudioAttributes attributes = new AudioAttributes.Builder() 604 .setUsage(USAGE_VOICE_COMMUNICATION) 605 .setContentType(CONTENT_TYPE_SPEECH) 606 .build(); 607 608 final int audioSessionId = ((AudioManager) getSystemService( 609 Context.AUDIO_SERVICE)).generateAudioSessionId(); 610 // Prepare the media player to play a tone when there is a call. 611 MediaPlayer mediaPlayer = MediaPlayer.create(getApplicationContext(), R.raw.beep_boop, attributes, 612 audioSessionId); 613 mediaPlayer.setLooping(true); 614 return mediaPlayer; 615 } 616 617 private static void log(String msg) { 618 Log.w("telecomtestcs", "[TestConnectionService] " + msg); 619 } 620 621 /** 622 * Generates a random phone number of format 555YXXX. Where Y will be {@code 1} if the 623 * phone number is for a video call and {@code 0} for an audio call. XXX is a randomly 624 * generated phone number. 625 * 626 * @param isVideo {@code True} if the call is a video call. 627 * @return The phone number. 628 */ 629 private String getDummyNumber(boolean isVideo) { 630 int videoDigit = isVideo ? 1 : 0; 631 int number = mRandom.nextInt(999); 632 return String.format("555%s%03d", videoDigit, number); 633 } 634 } 635 636