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