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