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.util.Log; 39 import android.widget.Toast; 40 41 import com.android.server.telecom.testapps.R; 42 43 import java.lang.String; 44 import java.util.ArrayList; 45 import java.util.List; 46 import java.util.Random; 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 /** 61 * Random number generator used to generate phone numbers. 62 */ 63 private Random mRandom = new Random(); 64 65 private final class TestConference extends Conference { 66 67 private final Connection.Listener mConnectionListener = new Connection.Listener() { 68 @Override 69 public void onDestroyed(Connection c) { 70 removeConnection(c); 71 if (getConnections().size() == 0) { 72 setDisconnected(new DisconnectCause(DisconnectCause.REMOTE)); 73 destroy(); 74 } 75 } 76 }; 77 TestConference(Connection a, Connection b)78 public TestConference(Connection a, Connection b) { 79 super(null); 80 setConnectionCapabilities( 81 Connection.CAPABILITY_SUPPORT_HOLD | 82 Connection.CAPABILITY_HOLD | 83 Connection.CAPABILITY_MUTE | 84 Connection.CAPABILITY_MANAGE_CONFERENCE); 85 addConnection(a); 86 addConnection(b); 87 88 a.addConnectionListener(mConnectionListener); 89 b.addConnectionListener(mConnectionListener); 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 connection.removeConnectionListener(mConnectionListener); 111 } 112 } 113 114 @Override onHold()115 public void onHold() { 116 for (Connection c : getConnections()) { 117 c.setOnHold(); 118 } 119 setOnHold(); 120 } 121 122 @Override onUnhold()123 public void onUnhold() { 124 for (Connection c : getConnections()) { 125 c.setActive(); 126 } 127 setActive(); 128 } 129 } 130 131 final class TestConnection extends Connection { 132 private final boolean mIsIncoming; 133 134 /** Used to cleanup camera and media when done with connection. */ 135 private TestVideoProvider mTestVideoCallProvider; 136 137 private BroadcastReceiver mHangupReceiver = new BroadcastReceiver() { 138 @Override 139 public void onReceive(Context context, Intent intent) { 140 setDisconnected(new DisconnectCause(DisconnectCause.MISSED)); 141 destroyCall(TestConnection.this); 142 destroy(); 143 } 144 }; 145 146 private BroadcastReceiver mUpgradeRequestReceiver = new BroadcastReceiver() { 147 @Override 148 public void onReceive(Context context, Intent intent) { 149 final int request = Integer.parseInt(intent.getData().getSchemeSpecificPart()); 150 final VideoProfile videoProfile = new VideoProfile(request); 151 mTestVideoCallProvider.receiveSessionModifyRequest(videoProfile); 152 } 153 }; 154 TestConnection(boolean isIncoming)155 TestConnection(boolean isIncoming) { 156 mIsIncoming = isIncoming; 157 // Assume all calls are video capable. 158 int capabilities = getConnectionCapabilities(); 159 capabilities |= CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL; 160 capabilities |= CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL; 161 capabilities |= CAPABILITY_CAN_UPGRADE_TO_VIDEO; 162 capabilities |= CAPABILITY_MUTE; 163 capabilities |= CAPABILITY_SUPPORT_HOLD; 164 capabilities |= CAPABILITY_HOLD; 165 capabilities |= CAPABILITY_RESPOND_VIA_TEXT; 166 setConnectionCapabilities(capabilities); 167 168 LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver( 169 mHangupReceiver, new IntentFilter(TestCallActivity.ACTION_HANGUP_CALLS)); 170 final IntentFilter filter = 171 new IntentFilter(TestCallActivity.ACTION_SEND_UPGRADE_REQUEST); 172 filter.addDataScheme("int"); 173 LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver( 174 mUpgradeRequestReceiver, filter); 175 } 176 startOutgoing()177 void startOutgoing() { 178 setDialing(); 179 mHandler.postDelayed(new Runnable() { 180 @Override 181 public void run() { 182 setActive(); 183 activateCall(TestConnection.this); 184 } 185 }, 4000); 186 } 187 188 /** ${inheritDoc} */ 189 @Override onAbort()190 public void onAbort() { 191 destroyCall(this); 192 destroy(); 193 } 194 195 /** ${inheritDoc} */ 196 @Override onAnswer(int videoState)197 public void onAnswer(int videoState) { 198 setVideoState(videoState); 199 activateCall(this); 200 setActive(); 201 updateConferenceable(); 202 } 203 204 /** ${inheritDoc} */ 205 @Override onPlayDtmfTone(char c)206 public void onPlayDtmfTone(char c) { 207 if (c == '1') { 208 setDialing(); 209 } 210 } 211 212 /** ${inheritDoc} */ 213 @Override onStopDtmfTone()214 public void onStopDtmfTone() { } 215 216 /** ${inheritDoc} */ 217 @Override onDisconnect()218 public void onDisconnect() { 219 setDisconnected(new DisconnectCause(DisconnectCause.REMOTE)); 220 destroyCall(this); 221 destroy(); 222 } 223 224 /** ${inheritDoc} */ 225 @Override onHold()226 public void onHold() { 227 setOnHold(); 228 } 229 230 /** ${inheritDoc} */ 231 @Override onReject()232 public void onReject() { 233 setDisconnected(new DisconnectCause(DisconnectCause.REJECTED)); 234 destroyCall(this); 235 destroy(); 236 } 237 238 /** ${inheritDoc} */ 239 @Override onUnhold()240 public void onUnhold() { 241 setActive(); 242 } 243 setTestVideoCallProvider(TestVideoProvider testVideoCallProvider)244 public void setTestVideoCallProvider(TestVideoProvider testVideoCallProvider) { 245 mTestVideoCallProvider = testVideoCallProvider; 246 } 247 cleanup()248 public void cleanup() { 249 LocalBroadcastManager.getInstance(getApplicationContext()).unregisterReceiver( 250 mHangupReceiver); 251 LocalBroadcastManager.getInstance(getApplicationContext()).unregisterReceiver( 252 mUpgradeRequestReceiver); 253 } 254 255 /** 256 * Stops playback of test videos. 257 */ stopAndCleanupMedia()258 private void stopAndCleanupMedia() { 259 if (mTestVideoCallProvider != null) { 260 mTestVideoCallProvider.stopAndCleanupMedia(); 261 mTestVideoCallProvider.stopCamera(); 262 } 263 } 264 } 265 266 private final List<TestConnection> mCalls = new ArrayList<>(); 267 private final Handler mHandler = new Handler(); 268 269 /** Used to play an audio tone during a call. */ 270 private MediaPlayer mMediaPlayer; 271 272 @Override onUnbind(Intent intent)273 public boolean onUnbind(Intent intent) { 274 log("onUnbind"); 275 mMediaPlayer = null; 276 return super.onUnbind(intent); 277 } 278 279 @Override onConference(Connection a, Connection b)280 public void onConference(Connection a, Connection b) { 281 addConference(new TestConference(a, b)); 282 } 283 284 @Override onCreateOutgoingConnection( PhoneAccountHandle connectionManagerAccount, final ConnectionRequest originalRequest)285 public Connection onCreateOutgoingConnection( 286 PhoneAccountHandle connectionManagerAccount, 287 final ConnectionRequest originalRequest) { 288 289 final Uri handle = originalRequest.getAddress(); 290 String number = originalRequest.getAddress().getSchemeSpecificPart(); 291 log("call, number: " + number); 292 293 // Crash on 555-DEAD to test call service crashing. 294 if ("5550340".equals(number)) { 295 throw new RuntimeException("Goodbye, cruel world."); 296 } 297 298 Bundle extras = originalRequest.getExtras(); 299 String gatewayPackage = extras.getString(TelecomManager.GATEWAY_PROVIDER_PACKAGE); 300 Uri originalHandle = extras.getParcelable(TelecomManager.GATEWAY_ORIGINAL_ADDRESS); 301 302 if (extras.containsKey(TelecomManager.EXTRA_CALL_SUBJECT)) { 303 String callSubject = extras.getString(TelecomManager.EXTRA_CALL_SUBJECT); 304 log("Got subject: " + callSubject); 305 Toast.makeText(getApplicationContext(), "Got subject :" + callSubject, 306 Toast.LENGTH_SHORT).show(); 307 } 308 309 log("gateway package [" + gatewayPackage + "], original handle [" + 310 originalHandle + "]"); 311 312 final TestConnection connection = new TestConnection(false /* isIncoming */); 313 setAddress(connection, handle); 314 315 // If the number starts with 555, then we handle it ourselves. If not, then we 316 // use a remote connection service. 317 // TODO: Have a special phone number to test the account-picker dialog flow. 318 if (number != null && number.startsWith("555")) { 319 // Normally we would use the original request as is, but for testing purposes, we are 320 // adding ".." to the end of the number to follow its path more easily through the logs. 321 final ConnectionRequest request = new ConnectionRequest( 322 originalRequest.getAccountHandle(), 323 Uri.fromParts(handle.getScheme(), 324 handle.getSchemeSpecificPart() + "..", ""), 325 originalRequest.getExtras(), 326 originalRequest.getVideoState()); 327 connection.setVideoState(originalRequest.getVideoState()); 328 addVideoProvider(connection); 329 addCall(connection); 330 connection.startOutgoing(); 331 332 for (Connection c : getAllConnections()) { 333 c.setOnHold(); 334 } 335 } else { 336 log("Not a test number"); 337 } 338 return connection; 339 } 340 341 @Override onCreateIncomingConnection( PhoneAccountHandle connectionManagerAccount, final ConnectionRequest request)342 public Connection onCreateIncomingConnection( 343 PhoneAccountHandle connectionManagerAccount, 344 final ConnectionRequest request) { 345 PhoneAccountHandle accountHandle = request.getAccountHandle(); 346 ComponentName componentName = new ComponentName(this, TestConnectionService.class); 347 348 if (accountHandle != null && componentName.equals(accountHandle.getComponentName())) { 349 final TestConnection connection = new TestConnection(true); 350 // Get the stashed intent extra that determines if this is a video call or audio call. 351 Bundle extras = request.getExtras(); 352 int videoState = extras.getInt(EXTRA_START_VIDEO_STATE, VideoProfile.STATE_AUDIO_ONLY); 353 Uri providedHandle = extras.getParcelable(EXTRA_HANDLE); 354 355 // Use dummy number for testing incoming calls. 356 Uri address = providedHandle == null ? 357 Uri.fromParts(PhoneAccount.SCHEME_TEL, getDummyNumber( 358 VideoProfile.isVideo(videoState)), null) 359 : providedHandle; 360 connection.setVideoState(videoState); 361 362 Bundle connectionExtras = connection.getExtras(); 363 if (connectionExtras == null) { 364 connectionExtras = new Bundle(); 365 } 366 367 // Randomly choose a varying length call subject. 368 int subjectFormat = mRandom.nextInt(3); 369 if (subjectFormat == 0) { 370 connectionExtras.putString(Connection.EXTRA_CALL_SUBJECT, 371 "This is a test of call subject lines. Subjects for a call can be long " + 372 " and can go even longer."); 373 } else if (subjectFormat == 1) { 374 connectionExtras.putString(Connection.EXTRA_CALL_SUBJECT, 375 "This is a test of call subject lines."); 376 } 377 connection.putExtras(connectionExtras); 378 379 setAddress(connection, address); 380 381 addVideoProvider(connection); 382 383 addCall(connection); 384 385 ConnectionRequest newRequest = new ConnectionRequest( 386 request.getAccountHandle(), 387 address, 388 request.getExtras(), 389 videoState); 390 connection.setVideoState(videoState); 391 return connection; 392 } else { 393 return Connection.createFailedConnection(new DisconnectCause(DisconnectCause.ERROR, 394 "Invalid inputs: " + accountHandle + " " + componentName)); 395 } 396 } 397 398 @Override onCreateUnknownConnection(PhoneAccountHandle connectionManagerPhoneAccount, final ConnectionRequest request)399 public Connection onCreateUnknownConnection(PhoneAccountHandle connectionManagerPhoneAccount, 400 final ConnectionRequest request) { 401 PhoneAccountHandle accountHandle = request.getAccountHandle(); 402 ComponentName componentName = new ComponentName(this, TestConnectionService.class); 403 if (accountHandle != null && componentName.equals(accountHandle.getComponentName())) { 404 final TestConnection connection = new TestConnection(false); 405 final Bundle extras = request.getExtras(); 406 final Uri providedHandle = extras.getParcelable(EXTRA_HANDLE); 407 408 Uri handle = providedHandle == null ? 409 Uri.fromParts(PhoneAccount.SCHEME_TEL, getDummyNumber(false), null) 410 : providedHandle; 411 412 connection.setAddress(handle, TelecomManager.PRESENTATION_ALLOWED); 413 connection.setDialing(); 414 415 addCall(connection); 416 return connection; 417 } else { 418 return Connection.createFailedConnection(new DisconnectCause(DisconnectCause.ERROR, 419 "Invalid inputs: " + accountHandle + " " + componentName)); 420 } 421 } 422 addVideoProvider(TestConnection connection)423 private void addVideoProvider(TestConnection connection) { 424 TestVideoProvider testVideoCallProvider = 425 new TestVideoProvider(getApplicationContext(), connection); 426 connection.setVideoProvider(testVideoCallProvider); 427 428 // Keep reference to original so we can clean up the media players later. 429 connection.setTestVideoCallProvider(testVideoCallProvider); 430 } 431 activateCall(TestConnection connection)432 private void activateCall(TestConnection connection) { 433 if (mMediaPlayer == null) { 434 mMediaPlayer = createMediaPlayer(); 435 } 436 if (!mMediaPlayer.isPlaying()) { 437 mMediaPlayer.start(); 438 } 439 } 440 destroyCall(TestConnection connection)441 private void destroyCall(TestConnection connection) { 442 connection.cleanup(); 443 mCalls.remove(connection); 444 445 // Ensure any playing media and camera resources are released. 446 connection.stopAndCleanupMedia(); 447 448 // Stops audio if there are no more calls. 449 if (mCalls.isEmpty() && mMediaPlayer != null && mMediaPlayer.isPlaying()) { 450 mMediaPlayer.stop(); 451 mMediaPlayer.release(); 452 mMediaPlayer = createMediaPlayer(); 453 } 454 455 updateConferenceable(); 456 } 457 addCall(TestConnection connection)458 private void addCall(TestConnection connection) { 459 mCalls.add(connection); 460 updateConferenceable(); 461 } 462 updateConferenceable()463 private void updateConferenceable() { 464 List<Connection> freeConnections = new ArrayList<>(); 465 freeConnections.addAll(mCalls); 466 for (int i = 0; i < freeConnections.size(); i++) { 467 if (freeConnections.get(i).getConference() != null) { 468 freeConnections.remove(i); 469 } 470 } 471 for (int i = 0; i < freeConnections.size(); i++) { 472 Connection c = freeConnections.remove(i); 473 c.setConferenceableConnections(freeConnections); 474 freeConnections.add(i, c); 475 } 476 } 477 setAddress(Connection connection, Uri address)478 private void setAddress(Connection connection, Uri address) { 479 connection.setAddress(address, TelecomManager.PRESENTATION_ALLOWED); 480 if ("5551234".equals(address.getSchemeSpecificPart())) { 481 connection.setCallerDisplayName("Hello World", TelecomManager.PRESENTATION_ALLOWED); 482 } 483 } 484 createMediaPlayer()485 private MediaPlayer createMediaPlayer() { 486 // Prepare the media player to play a tone when there is a call. 487 MediaPlayer mediaPlayer = MediaPlayer.create(getApplicationContext(), R.raw.beep_boop); 488 mediaPlayer.setLooping(true); 489 return mediaPlayer; 490 } 491 log(String msg)492 private static void log(String msg) { 493 Log.w("telecomtestcs", "[TestConnectionService] " + msg); 494 } 495 496 /** 497 * Generates a random phone number of format 555YXXX. Where Y will be {@code 1} if the 498 * phone number is for a video call and {@code 0} for an audio call. XXX is a randomly 499 * generated phone number. 500 * 501 * @param isVideo {@code True} if the call is a video call. 502 * @return The phone number. 503 */ getDummyNumber(boolean isVideo)504 private String getDummyNumber(boolean isVideo) { 505 int videoDigit = isVideo ? 1 : 0; 506 int number = mRandom.nextInt(999); 507 return String.format("555%s%03d", videoDigit, number); 508 } 509 } 510 511