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