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.PhoneCapabilities; 31 import android.telecom.ConnectionRequest; 32 import android.telecom.ConnectionService; 33 import android.telecom.PhoneAccountHandle; 34 import android.telecom.RemoteConnection; 35 import android.telecom.StatusHints; 36 import android.telecom.TelecomManager; 37 import android.telecom.VideoProfile; 38 import android.util.Log; 39 40 import com.android.server.telecom.tests.R; 41 42 import java.lang.String; 43 import java.util.ArrayList; 44 import java.util.List; 45 import java.util.Random; 46 47 /** 48 * Service which provides fake calls to test the ConnectionService interface. 49 * TODO: Rename all classes in the directory to Dummy* (e.g., DummyConnectionService). 50 */ 51 public class TestConnectionService extends ConnectionService { 52 /** 53 * Intent extra used to pass along whether a call is video or audio based on the user's choice 54 * in the notification. 55 */ 56 public static final String EXTRA_IS_VIDEO_CALL = "extra_is_video_call"; 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 81 addConnection(a); 82 addConnection(b); 83 84 a.addConnectionListener(mConnectionListener); 85 b.addConnectionListener(mConnectionListener); 86 87 a.setConference(this); 88 b.setConference(this); 89 90 setActive(); 91 } 92 93 @Override onDisconnect()94 public void onDisconnect() { 95 for (Connection c : getConnections()) { 96 c.setDisconnected(new DisconnectCause(DisconnectCause.REMOTE)); 97 c.destroy(); 98 } 99 } 100 101 @Override onSeparate(Connection connection)102 public void onSeparate(Connection connection) { 103 if (getConnections().contains(connection)) { 104 connection.setConference(null); 105 removeConnection(connection); 106 connection.removeConnectionListener(mConnectionListener); 107 } 108 } 109 110 @Override onHold()111 public void onHold() { 112 for (Connection c : getConnections()) { 113 c.setOnHold(); 114 } 115 setOnHold(); 116 } 117 118 @Override onUnhold()119 public void onUnhold() { 120 for (Connection c : getConnections()) { 121 c.setActive(); 122 } 123 setActive(); 124 } 125 } 126 127 private final class TestConnection extends Connection { 128 private final boolean mIsIncoming; 129 130 /** Used to cleanup camera and media when done with connection. */ 131 private TestVideoProvider mTestVideoCallProvider; 132 TestConnection(boolean isIncoming)133 TestConnection(boolean isIncoming) { 134 mIsIncoming = isIncoming; 135 // Assume all calls are video capable. 136 int capabilities = getCallCapabilities(); 137 capabilities |= PhoneCapabilities.SUPPORTS_VT_LOCAL; 138 capabilities |= PhoneCapabilities.ADD_CALL; 139 capabilities |= PhoneCapabilities.MUTE; 140 capabilities |= PhoneCapabilities.SUPPORT_HOLD; 141 capabilities |= PhoneCapabilities.HOLD; 142 setCallCapabilities(capabilities); 143 } 144 startOutgoing()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 onAbort()158 public void onAbort() { 159 destroyCall(this); 160 destroy(); 161 } 162 163 /** ${inheritDoc} */ 164 @Override onAnswer(int videoState)165 public void onAnswer(int videoState) { 166 setVideoState(videoState); 167 activateCall(this); 168 setActive(); 169 updateConferenceable(); 170 } 171 172 /** ${inheritDoc} */ 173 @Override onPlayDtmfTone(char c)174 public void onPlayDtmfTone(char c) { 175 if (c == '1') { 176 setDialing(); 177 } 178 } 179 180 /** ${inheritDoc} */ 181 @Override onStopDtmfTone()182 public void onStopDtmfTone() { } 183 184 /** ${inheritDoc} */ 185 @Override onDisconnect()186 public void onDisconnect() { 187 setDisconnected(new DisconnectCause(DisconnectCause.REMOTE)); 188 destroyCall(this); 189 destroy(); 190 } 191 192 /** ${inheritDoc} */ 193 @Override onHold()194 public void onHold() { 195 setOnHold(); 196 } 197 198 /** ${inheritDoc} */ 199 @Override onReject()200 public void onReject() { 201 setDisconnected(new DisconnectCause(DisconnectCause.REJECTED)); 202 destroyCall(this); 203 destroy(); 204 } 205 206 /** ${inheritDoc} */ 207 @Override onUnhold()208 public void onUnhold() { 209 setActive(); 210 } 211 212 @Override onAudioStateChanged(AudioState state)213 public void onAudioStateChanged(AudioState state) { } 214 setTestVideoCallProvider(TestVideoProvider testVideoCallProvider)215 public void setTestVideoCallProvider(TestVideoProvider testVideoCallProvider) { 216 mTestVideoCallProvider = testVideoCallProvider; 217 } 218 219 /** 220 * Stops playback of test videos. 221 */ stopAndCleanupMedia()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 onUnbind(Intent intent)237 public boolean onUnbind(Intent intent) { 238 log("onUnbind"); 239 mMediaPlayer = null; 240 return super.onUnbind(intent); 241 } 242 243 @Override onConference(Connection a, Connection b)244 public void onConference(Connection a, Connection b) { 245 addConference(new TestConference(a, b)); 246 } 247 248 @Override onCreateOutgoingConnection( PhoneAccountHandle connectionManagerAccount, final ConnectionRequest originalRequest)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 onCreateIncomingConnection( PhoneAccountHandle connectionManagerAccount, final ConnectionRequest request)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 onCreateUnknownConnection(PhoneAccountHandle connectionManagerPhoneAccount, final ConnectionRequest request)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 activateCall(TestConnection connection)370 private void activateCall(TestConnection connection) { 371 if (mMediaPlayer == null) { 372 mMediaPlayer = createMediaPlayer(); 373 } 374 if (!mMediaPlayer.isPlaying()) { 375 mMediaPlayer.start(); 376 } 377 } 378 destroyCall(TestConnection connection)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 addCall(TestConnection connection)395 private void addCall(TestConnection connection) { 396 mCalls.add(connection); 397 updateConferenceable(); 398 } 399 updateConferenceable()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 createMediaPlayer()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 log(String msg)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 */ getDummyNumber(boolean isVideo)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