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