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