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.ComponentName; 20 import android.content.Intent; 21 import android.media.MediaPlayer; 22 import android.net.Uri; 23 import android.os.Bundle; 24 import android.os.Handler; 25 import android.telecom.AudioState; 26 import android.telecom.Conference; 27 import android.telecom.Connection; 28 import android.telecom.DisconnectCause; 29 import android.telecom.PhoneAccount; 30 import android.telecom.ConnectionRequest; 31 import android.telecom.ConnectionService; 32 import android.telecom.PhoneAccountHandle; 33 import android.telecom.TelecomManager; 34 import android.telecom.VideoProfile; 35 import android.util.Log; 36 37 import com.android.server.telecom.tests.R; 38 39 import java.lang.String; 40 import java.util.ArrayList; 41 import java.util.List; 42 import java.util.Random; 43 44 /** 45 * Service which provides fake calls to test the ConnectionService interface. 46 * TODO: Rename all classes in the directory to Dummy* (e.g., DummyConnectionService). 47 */ 48 public class TestConnectionService extends ConnectionService { 49 /** 50 * Intent extra used to pass along whether a call is video or audio based on the user's choice 51 * in the notification. 52 */ 53 public static final String EXTRA_IS_VIDEO_CALL = "extra_is_video_call"; 54 55 public static final String EXTRA_HANDLE = "extra_handle"; 56 57 /** 58 * Random number generator used to generate phone numbers. 59 */ 60 private Random mRandom = new Random(); 61 62 private final class TestConference extends Conference { 63 64 private final Connection.Listener mConnectionListener = new Connection.Listener() { 65 @Override 66 public void onDestroyed(Connection c) { 67 removeConnection(c); 68 if (getConnections().size() == 0) { 69 setDisconnected(new DisconnectCause(DisconnectCause.REMOTE)); 70 destroy(); 71 } 72 } 73 }; 74 75 public TestConference(Connection a, Connection b) { 76 super(null); 77 setConnectionCapabilities( 78 Connection.CAPABILITY_SUPPORT_HOLD | 79 Connection.CAPABILITY_HOLD | 80 Connection.CAPABILITY_MUTE | 81 Connection.CAPABILITY_MANAGE_CONFERENCE); 82 addConnection(a); 83 addConnection(b); 84 85 a.addConnectionListener(mConnectionListener); 86 b.addConnectionListener(mConnectionListener); 87 88 a.setConference(this); 89 b.setConference(this); 90 91 setActive(); 92 } 93 94 @Override 95 public void onDisconnect() { 96 for (Connection c : getConnections()) { 97 c.setDisconnected(new DisconnectCause(DisconnectCause.REMOTE)); 98 c.destroy(); 99 } 100 } 101 102 @Override 103 public void onSeparate(Connection connection) { 104 if (getConnections().contains(connection)) { 105 connection.setConference(null); 106 removeConnection(connection); 107 connection.removeConnectionListener(mConnectionListener); 108 } 109 } 110 111 @Override 112 public void onHold() { 113 for (Connection c : getConnections()) { 114 c.setOnHold(); 115 } 116 setOnHold(); 117 } 118 119 @Override 120 public void onUnhold() { 121 for (Connection c : getConnections()) { 122 c.setActive(); 123 } 124 setActive(); 125 } 126 } 127 128 private final class TestConnection extends Connection { 129 private final boolean mIsIncoming; 130 131 /** Used to cleanup camera and media when done with connection. */ 132 private TestVideoProvider mTestVideoCallProvider; 133 134 TestConnection(boolean isIncoming) { 135 mIsIncoming = isIncoming; 136 // Assume all calls are video capable. 137 int capabilities = getConnectionCapabilities(); 138 capabilities |= CAPABILITY_SUPPORTS_VT_LOCAL; 139 capabilities |= CAPABILITY_MUTE; 140 capabilities |= CAPABILITY_SUPPORT_HOLD; 141 capabilities |= CAPABILITY_HOLD; 142 setConnectionCapabilities(capabilities); 143 } 144 145 void startOutgoing() { 146 setDialing(); 147 mHandler.postDelayed(new Runnable() { 148 @Override 149 public void run() { 150 setActive(); 151 activateCall(TestConnection.this); 152 } 153 }, 4000); 154 } 155 156 /** ${inheritDoc} */ 157 @Override 158 public void onAbort() { 159 destroyCall(this); 160 destroy(); 161 } 162 163 /** ${inheritDoc} */ 164 @Override 165 public void onAnswer(int videoState) { 166 setVideoState(videoState); 167 activateCall(this); 168 setActive(); 169 updateConferenceable(); 170 } 171 172 /** ${inheritDoc} */ 173 @Override 174 public void onPlayDtmfTone(char c) { 175 if (c == '1') { 176 setDialing(); 177 } 178 } 179 180 /** ${inheritDoc} */ 181 @Override 182 public void onStopDtmfTone() { } 183 184 /** ${inheritDoc} */ 185 @Override 186 public void onDisconnect() { 187 setDisconnected(new DisconnectCause(DisconnectCause.REMOTE)); 188 destroyCall(this); 189 destroy(); 190 } 191 192 /** ${inheritDoc} */ 193 @Override 194 public void onHold() { 195 setOnHold(); 196 } 197 198 /** ${inheritDoc} */ 199 @Override 200 public void onReject() { 201 setDisconnected(new DisconnectCause(DisconnectCause.REJECTED)); 202 destroyCall(this); 203 destroy(); 204 } 205 206 /** ${inheritDoc} */ 207 @Override 208 public void onUnhold() { 209 setActive(); 210 } 211 212 @Override 213 public void onAudioStateChanged(AudioState state) { } 214 215 public void setTestVideoCallProvider(TestVideoProvider testVideoCallProvider) { 216 mTestVideoCallProvider = testVideoCallProvider; 217 } 218 219 /** 220 * Stops playback of test videos. 221 */ 222 private void stopAndCleanupMedia() { 223 if (mTestVideoCallProvider != null) { 224 mTestVideoCallProvider.stopAndCleanupMedia(); 225 mTestVideoCallProvider.stopCamera(); 226 } 227 } 228 } 229 230 private final List<TestConnection> mCalls = new ArrayList<>(); 231 private final Handler mHandler = new Handler(); 232 233 /** Used to play an audio tone during a call. */ 234 private MediaPlayer mMediaPlayer; 235 236 @Override 237 public boolean onUnbind(Intent intent) { 238 log("onUnbind"); 239 mMediaPlayer = null; 240 return super.onUnbind(intent); 241 } 242 243 @Override 244 public void onConference(Connection a, Connection b) { 245 addConference(new TestConference(a, b)); 246 } 247 248 @Override 249 public Connection onCreateOutgoingConnection( 250 PhoneAccountHandle connectionManagerAccount, 251 final ConnectionRequest originalRequest) { 252 253 final Uri handle = originalRequest.getAddress(); 254 String number = originalRequest.getAddress().getSchemeSpecificPart(); 255 log("call, number: " + number); 256 257 // Crash on 555-DEAD to test call service crashing. 258 if ("5550340".equals(number)) { 259 throw new RuntimeException("Goodbye, cruel world."); 260 } 261 262 Bundle extras = originalRequest.getExtras(); 263 String gatewayPackage = extras.getString(TelecomManager.GATEWAY_PROVIDER_PACKAGE); 264 Uri originalHandle = extras.getParcelable(TelecomManager.GATEWAY_ORIGINAL_ADDRESS); 265 266 log("gateway package [" + gatewayPackage + "], original handle [" + 267 originalHandle + "]"); 268 269 final TestConnection connection = new TestConnection(false /* isIncoming */); 270 connection.setAddress(handle, TelecomManager.PRESENTATION_ALLOWED); 271 272 // If the number starts with 555, then we handle it ourselves. If not, then we 273 // use a remote connection service. 274 // TODO: Have a special phone number to test the account-picker dialog flow. 275 if (number != null && number.startsWith("555")) { 276 // Normally we would use the original request as is, but for testing purposes, we are 277 // adding ".." to the end of the number to follow its path more easily through the logs. 278 final ConnectionRequest request = new ConnectionRequest( 279 originalRequest.getAccountHandle(), 280 Uri.fromParts(handle.getScheme(), 281 handle.getSchemeSpecificPart() + "..", ""), 282 originalRequest.getExtras(), 283 originalRequest.getVideoState()); 284 285 addCall(connection); 286 connection.startOutgoing(); 287 288 for (Connection c : getAllConnections()) { 289 c.setOnHold(); 290 } 291 } else { 292 log("Not a test number"); 293 } 294 return connection; 295 } 296 297 @Override 298 public Connection onCreateIncomingConnection( 299 PhoneAccountHandle connectionManagerAccount, 300 final ConnectionRequest request) { 301 PhoneAccountHandle accountHandle = request.getAccountHandle(); 302 ComponentName componentName = new ComponentName(this, TestConnectionService.class); 303 304 if (accountHandle != null && componentName.equals(accountHandle.getComponentName())) { 305 final TestConnection connection = new TestConnection(true); 306 // Get the stashed intent extra that determines if this is a video call or audio call. 307 Bundle extras = request.getExtras(); 308 boolean isVideoCall = extras.getBoolean(EXTRA_IS_VIDEO_CALL); 309 Uri providedHandle = extras.getParcelable(EXTRA_HANDLE); 310 311 // Use dummy number for testing incoming calls. 312 Uri address = providedHandle == null ? 313 Uri.fromParts(PhoneAccount.SCHEME_TEL, getDummyNumber(isVideoCall), null) 314 : providedHandle; 315 if (isVideoCall) { 316 TestVideoProvider testVideoCallProvider = 317 new TestVideoProvider(getApplicationContext()); 318 connection.setVideoProvider(testVideoCallProvider); 319 320 // Keep reference to original so we can clean up the media players later. 321 connection.setTestVideoCallProvider(testVideoCallProvider); 322 } 323 324 int videoState = isVideoCall ? 325 VideoProfile.VideoState.BIDIRECTIONAL : 326 VideoProfile.VideoState.AUDIO_ONLY; 327 connection.setVideoState(videoState); 328 connection.setAddress(address, TelecomManager.PRESENTATION_ALLOWED); 329 330 addCall(connection); 331 332 ConnectionRequest newRequest = new ConnectionRequest( 333 request.getAccountHandle(), 334 address, 335 request.getExtras(), 336 videoState); 337 connection.setVideoState(videoState); 338 return connection; 339 } else { 340 return Connection.createFailedConnection(new DisconnectCause(DisconnectCause.ERROR, 341 "Invalid inputs: " + accountHandle + " " + componentName)); 342 } 343 } 344 345 @Override 346 public Connection onCreateUnknownConnection(PhoneAccountHandle connectionManagerPhoneAccount, 347 final ConnectionRequest request) { 348 PhoneAccountHandle accountHandle = request.getAccountHandle(); 349 ComponentName componentName = new ComponentName(this, TestConnectionService.class); 350 if (accountHandle != null && componentName.equals(accountHandle.getComponentName())) { 351 final TestConnection connection = new TestConnection(false); 352 final Bundle extras = request.getExtras(); 353 final Uri providedHandle = extras.getParcelable(EXTRA_HANDLE); 354 355 Uri handle = providedHandle == null ? 356 Uri.fromParts(PhoneAccount.SCHEME_TEL, getDummyNumber(false), null) 357 : providedHandle; 358 359 connection.setAddress(handle, TelecomManager.PRESENTATION_ALLOWED); 360 connection.setDialing(); 361 362 addCall(connection); 363 return connection; 364 } else { 365 return Connection.createFailedConnection(new DisconnectCause(DisconnectCause.ERROR, 366 "Invalid inputs: " + accountHandle + " " + componentName)); 367 } 368 } 369 370 private void activateCall(TestConnection connection) { 371 if (mMediaPlayer == null) { 372 mMediaPlayer = createMediaPlayer(); 373 } 374 if (!mMediaPlayer.isPlaying()) { 375 mMediaPlayer.start(); 376 } 377 } 378 379 private void destroyCall(TestConnection connection) { 380 mCalls.remove(connection); 381 382 // Ensure any playing media and camera resources are released. 383 connection.stopAndCleanupMedia(); 384 385 // Stops audio if there are no more calls. 386 if (mCalls.isEmpty() && mMediaPlayer != null && mMediaPlayer.isPlaying()) { 387 mMediaPlayer.stop(); 388 mMediaPlayer.release(); 389 mMediaPlayer = createMediaPlayer(); 390 } 391 392 updateConferenceable(); 393 } 394 395 private void addCall(TestConnection connection) { 396 mCalls.add(connection); 397 updateConferenceable(); 398 } 399 400 private void updateConferenceable() { 401 List<Connection> freeConnections = new ArrayList<>(); 402 freeConnections.addAll(mCalls); 403 for (int i = 0; i < freeConnections.size(); i++) { 404 if (freeConnections.get(i).getConference() != null) { 405 freeConnections.remove(i); 406 } 407 } 408 for (int i = 0; i < freeConnections.size(); i++) { 409 Connection c = freeConnections.remove(i); 410 c.setConferenceableConnections(freeConnections); 411 freeConnections.add(i, c); 412 } 413 } 414 415 private MediaPlayer createMediaPlayer() { 416 // Prepare the media player to play a tone when there is a call. 417 MediaPlayer mediaPlayer = MediaPlayer.create(getApplicationContext(), R.raw.beep_boop); 418 mediaPlayer.setLooping(true); 419 return mediaPlayer; 420 } 421 422 private static void log(String msg) { 423 Log.w("telecomtestcs", "[TestConnectionService] " + msg); 424 } 425 426 /** 427 * Generates a random phone number of format 555YXXX. Where Y will be {@code 1} if the 428 * phone number is for a video call and {@code 0} for an audio call. XXX is a randomly 429 * generated phone number. 430 * 431 * @param isVideo {@code True} if the call is a video call. 432 * @return The phone number. 433 */ 434 private String getDummyNumber(boolean isVideo) { 435 int videoDigit = isVideo ? 1 : 0; 436 int number = mRandom.nextInt(999); 437 return String.format("555%s%03d", videoDigit, number); 438 } 439 } 440 441