1 /* 2 * Copyright 2014, 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; 18 19 import android.annotation.Nullable; 20 import android.content.Context; 21 import android.media.AudioAttributes; 22 import android.media.AudioManager; 23 import android.media.MediaPlayer; 24 import android.media.ToneGenerator; 25 import android.net.Uri; 26 import android.os.Handler; 27 import android.os.Looper; 28 import android.telecom.Log; 29 import android.telecom.Logging.Runnable; 30 import android.telecom.Logging.Session; 31 32 import com.android.internal.annotations.VisibleForTesting; 33 34 import java.util.concurrent.CountDownLatch; 35 import java.util.concurrent.TimeUnit; 36 37 /** 38 * Play a call-related tone (ringback, busy signal, etc.) either through ToneGenerator, or using a 39 * media resource file. 40 * To use, create an instance using InCallTonePlayer.Factory (passing in the TONE_* constant for 41 * the tone you want) and start() it. Implemented on top of {@link Thread} so that the tone plays in 42 * its own thread. 43 */ 44 public class InCallTonePlayer extends Thread { 45 46 /** 47 * Factory used to create InCallTonePlayers. Exists to aid with testing mocks. 48 */ 49 public static class Factory { 50 private CallAudioManager mCallAudioManager; 51 private final CallAudioRoutePeripheralAdapter mCallAudioRoutePeripheralAdapter; 52 private final TelecomSystem.SyncRoot mLock; 53 private final ToneGeneratorFactory mToneGeneratorFactory; 54 private final MediaPlayerFactory mMediaPlayerFactory; 55 private final AudioManagerAdapter mAudioManagerAdapter; 56 57 public Factory(CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter, 58 TelecomSystem.SyncRoot lock, ToneGeneratorFactory toneGeneratorFactory, 59 MediaPlayerFactory mediaPlayerFactory, AudioManagerAdapter audioManagerAdapter) { 60 mCallAudioRoutePeripheralAdapter = callAudioRoutePeripheralAdapter; 61 mLock = lock; 62 mToneGeneratorFactory = toneGeneratorFactory; 63 mMediaPlayerFactory = mediaPlayerFactory; 64 mAudioManagerAdapter = audioManagerAdapter; 65 } 66 67 public void setCallAudioManager(CallAudioManager callAudioManager) { 68 mCallAudioManager = callAudioManager; 69 } 70 71 public InCallTonePlayer createPlayer(int tone) { 72 return new InCallTonePlayer(tone, mCallAudioManager, 73 mCallAudioRoutePeripheralAdapter, mLock, mToneGeneratorFactory, 74 mMediaPlayerFactory, mAudioManagerAdapter); 75 } 76 } 77 78 public interface ToneGeneratorFactory { 79 ToneGenerator get (int streamType, int volume); 80 } 81 82 public interface MediaPlayerAdapter { 83 void setLooping(boolean isLooping); 84 void setOnCompletionListener(MediaPlayer.OnCompletionListener listener); 85 void start(); 86 void release(); 87 int getDuration(); 88 } 89 90 public static class MediaPlayerAdapterImpl implements MediaPlayerAdapter { 91 private MediaPlayer mMediaPlayer; 92 93 /** 94 * Create new media player adapter backed by a real mediaplayer. 95 * Note: Its possible for the mediaplayer to be null if 96 * {@link MediaPlayer#create(Context, Uri)} fails for some reason; in this case we can 97 * continue but not bother playing the audio. 98 * @param mediaPlayer The media player. 99 */ 100 public MediaPlayerAdapterImpl(@Nullable MediaPlayer mediaPlayer) { 101 mMediaPlayer = mediaPlayer; 102 } 103 104 @Override 105 public void setLooping(boolean isLooping) { 106 if (mMediaPlayer != null) { 107 mMediaPlayer.setLooping(isLooping); 108 } 109 } 110 111 @Override 112 public void setOnCompletionListener(MediaPlayer.OnCompletionListener listener) { 113 if (mMediaPlayer != null) { 114 mMediaPlayer.setOnCompletionListener(listener); 115 } 116 } 117 118 @Override 119 public void start() { 120 if (mMediaPlayer != null) { 121 mMediaPlayer.start(); 122 } 123 } 124 125 @Override 126 public void release() { 127 if (mMediaPlayer != null) { 128 mMediaPlayer.release(); 129 } 130 } 131 132 @Override 133 public int getDuration() { 134 if (mMediaPlayer != null) { 135 return mMediaPlayer.getDuration(); 136 } 137 return 0; 138 } 139 } 140 141 public interface MediaPlayerFactory { 142 MediaPlayerAdapter get (int resourceId, AudioAttributes attributes); 143 } 144 145 public interface AudioManagerAdapter { 146 boolean isVolumeOverZero(); 147 } 148 149 // The possible tones that we can play. 150 public static final int TONE_INVALID = 0; 151 public static final int TONE_BUSY = 1; 152 public static final int TONE_CALL_ENDED = 2; 153 public static final int TONE_OTA_CALL_ENDED = 3; 154 public static final int TONE_CALL_WAITING = 4; 155 public static final int TONE_CDMA_DROP = 5; 156 public static final int TONE_CONGESTION = 6; 157 public static final int TONE_INTERCEPT = 7; 158 public static final int TONE_OUT_OF_SERVICE = 8; 159 public static final int TONE_REDIAL = 9; 160 public static final int TONE_REORDER = 10; 161 public static final int TONE_RING_BACK = 11; 162 public static final int TONE_UNOBTAINABLE_NUMBER = 12; 163 public static final int TONE_VOICE_PRIVACY = 13; 164 public static final int TONE_VIDEO_UPGRADE = 14; 165 public static final int TONE_RTT_REQUEST = 15; 166 public static final int TONE_IN_CALL_QUALITY_NOTIFICATION = 16; 167 168 private static final int TONE_RESOURCE_ID_UNDEFINED = -1; 169 170 private static final int RELATIVE_VOLUME_EMERGENCY = 100; 171 private static final int RELATIVE_VOLUME_HIPRI = 80; 172 private static final int RELATIVE_VOLUME_LOPRI = 50; 173 private static final int RELATIVE_VOLUME_UNDEFINED = -1; 174 175 // Buffer time (in msec) to add on to the tone timeout value. Needed mainly when the timeout 176 // value for a tone is exact duration of the tone itself. 177 private static final int TIMEOUT_BUFFER_MILLIS = 20; 178 179 // The tone state. 180 private static final int STATE_OFF = 0; 181 private static final int STATE_ON = 1; 182 private static final int STATE_STOPPED = 2; 183 184 /** 185 * Keeps count of the number of actively playing tones so that we can notify CallAudioManager 186 * when we need focus and when it can be release. This should only be manipulated from the main 187 * thread. 188 */ 189 private static int sTonesPlaying = 0; 190 191 private final CallAudioManager mCallAudioManager; 192 private final CallAudioRoutePeripheralAdapter mCallAudioRoutePeripheralAdapter; 193 194 private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); 195 196 /** The ID of the tone to play. */ 197 private final int mToneId; 198 199 /** Current state of the tone player. */ 200 private int mState; 201 202 /** For tones which are not generated using ToneGenerator. */ 203 private MediaPlayerAdapter mToneMediaPlayer = null; 204 205 /** Telecom lock object. */ 206 private final TelecomSystem.SyncRoot mLock; 207 208 private Session mSession; 209 private final Object mSessionLock = new Object(); 210 211 private final ToneGeneratorFactory mToneGenerator; 212 private final MediaPlayerFactory mMediaPlayerFactory; 213 private final AudioManagerAdapter mAudioManagerAdapter; 214 215 /** 216 * Initializes the tone player. Private; use the {@link Factory} to create tone players. 217 * 218 * @param toneId ID of the tone to play, see TONE_* constants. 219 */ 220 private InCallTonePlayer( 221 int toneId, 222 CallAudioManager callAudioManager, 223 CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter, 224 TelecomSystem.SyncRoot lock, 225 ToneGeneratorFactory toneGeneratorFactory, 226 MediaPlayerFactory mediaPlayerFactor, 227 AudioManagerAdapter audioManagerAdapter) { 228 mState = STATE_OFF; 229 mToneId = toneId; 230 mCallAudioManager = callAudioManager; 231 mCallAudioRoutePeripheralAdapter = callAudioRoutePeripheralAdapter; 232 mLock = lock; 233 mToneGenerator = toneGeneratorFactory; 234 mMediaPlayerFactory = mediaPlayerFactor; 235 mAudioManagerAdapter = audioManagerAdapter; 236 } 237 238 /** {@inheritDoc} */ 239 @Override 240 public void run() { 241 try { 242 synchronized (mSessionLock) { 243 if (mSession != null) { 244 Log.continueSession(mSession, "ICTP.r"); 245 mSession = null; 246 } 247 } 248 Log.d(this, "run(toneId = %s)", mToneId); 249 250 final int toneType; // Passed to ToneGenerator.startTone. 251 final int toneVolume; // Passed to the ToneGenerator constructor. 252 final int toneLengthMillis; 253 final int mediaResourceId; // The resourceId of the tone to play. Used for media-based 254 // tones. 255 256 switch (mToneId) { 257 case TONE_BUSY: 258 // TODO: CDMA-specific tones 259 toneType = ToneGenerator.TONE_SUP_BUSY; 260 toneVolume = RELATIVE_VOLUME_HIPRI; 261 toneLengthMillis = 4000; 262 mediaResourceId = TONE_RESOURCE_ID_UNDEFINED; 263 break; 264 case TONE_CALL_ENDED: 265 // Don't use tone generator 266 toneType = ToneGenerator.TONE_UNKNOWN; 267 toneVolume = RELATIVE_VOLUME_UNDEFINED; 268 toneLengthMillis = 0; 269 270 // Use a tone resource file for a more rich, full-bodied tone experience. 271 mediaResourceId = R.raw.endcall; 272 break; 273 case TONE_OTA_CALL_ENDED: 274 // TODO: fill in 275 throw new IllegalStateException("OTA Call ended NYI."); 276 case TONE_CALL_WAITING: 277 toneType = ToneGenerator.TONE_SUP_CALL_WAITING; 278 toneVolume = RELATIVE_VOLUME_HIPRI; 279 toneLengthMillis = Integer.MAX_VALUE - TIMEOUT_BUFFER_MILLIS; 280 mediaResourceId = TONE_RESOURCE_ID_UNDEFINED; 281 break; 282 case TONE_CDMA_DROP: 283 toneType = ToneGenerator.TONE_CDMA_CALLDROP_LITE; 284 toneVolume = RELATIVE_VOLUME_LOPRI; 285 toneLengthMillis = 375; 286 mediaResourceId = TONE_RESOURCE_ID_UNDEFINED; 287 break; 288 case TONE_CONGESTION: 289 toneType = ToneGenerator.TONE_SUP_CONGESTION; 290 toneVolume = RELATIVE_VOLUME_HIPRI; 291 toneLengthMillis = 4000; 292 mediaResourceId = TONE_RESOURCE_ID_UNDEFINED; 293 break; 294 case TONE_INTERCEPT: 295 toneType = ToneGenerator.TONE_CDMA_ABBR_INTERCEPT; 296 toneVolume = RELATIVE_VOLUME_LOPRI; 297 toneLengthMillis = 500; 298 mediaResourceId = TONE_RESOURCE_ID_UNDEFINED; 299 break; 300 case TONE_OUT_OF_SERVICE: 301 toneType = ToneGenerator.TONE_CDMA_CALLDROP_LITE; 302 toneVolume = RELATIVE_VOLUME_LOPRI; 303 toneLengthMillis = 375; 304 mediaResourceId = TONE_RESOURCE_ID_UNDEFINED; 305 break; 306 case TONE_REDIAL: 307 toneType = ToneGenerator.TONE_CDMA_ALERT_AUTOREDIAL_LITE; 308 toneVolume = RELATIVE_VOLUME_LOPRI; 309 toneLengthMillis = 5000; 310 mediaResourceId = TONE_RESOURCE_ID_UNDEFINED; 311 break; 312 case TONE_REORDER: 313 toneType = ToneGenerator.TONE_CDMA_REORDER; 314 toneVolume = RELATIVE_VOLUME_HIPRI; 315 toneLengthMillis = 4000; 316 mediaResourceId = TONE_RESOURCE_ID_UNDEFINED; 317 break; 318 case TONE_RING_BACK: 319 toneType = ToneGenerator.TONE_SUP_RINGTONE; 320 toneVolume = RELATIVE_VOLUME_HIPRI; 321 toneLengthMillis = Integer.MAX_VALUE - TIMEOUT_BUFFER_MILLIS; 322 mediaResourceId = TONE_RESOURCE_ID_UNDEFINED; 323 break; 324 case TONE_UNOBTAINABLE_NUMBER: 325 toneType = ToneGenerator.TONE_SUP_ERROR; 326 toneVolume = RELATIVE_VOLUME_HIPRI; 327 toneLengthMillis = 4000; 328 mediaResourceId = TONE_RESOURCE_ID_UNDEFINED; 329 break; 330 case TONE_VOICE_PRIVACY: 331 // TODO: fill in. 332 throw new IllegalStateException("Voice privacy tone NYI."); 333 case TONE_VIDEO_UPGRADE: 334 case TONE_RTT_REQUEST: 335 // Similar to the call waiting tone, but does not repeat. 336 toneType = ToneGenerator.TONE_SUP_CALL_WAITING; 337 toneVolume = RELATIVE_VOLUME_HIPRI; 338 toneLengthMillis = 4000; 339 mediaResourceId = TONE_RESOURCE_ID_UNDEFINED; 340 break; 341 case TONE_IN_CALL_QUALITY_NOTIFICATION: 342 // Don't use tone generator 343 toneType = ToneGenerator.TONE_UNKNOWN; 344 toneVolume = RELATIVE_VOLUME_UNDEFINED; 345 toneLengthMillis = 0; 346 347 // Use a tone resource file for a more rich, full-bodied tone experience. 348 mediaResourceId = R.raw.InCallQualityNotification; 349 break; 350 default: 351 throw new IllegalStateException("Bad toneId: " + mToneId); 352 } 353 354 int stream = AudioManager.STREAM_VOICE_CALL; 355 if (mCallAudioRoutePeripheralAdapter.isBluetoothAudioOn()) { 356 stream = AudioManager.STREAM_BLUETOOTH_SCO; 357 } 358 359 if (toneType != ToneGenerator.TONE_UNKNOWN) { 360 playToneGeneratorTone(stream, toneVolume, toneType, toneLengthMillis); 361 } else if (mediaResourceId != TONE_RESOURCE_ID_UNDEFINED) { 362 playMediaTone(stream, mediaResourceId); 363 } 364 } finally { 365 cleanUpTonePlayer(); 366 Log.endSession(); 367 } 368 } 369 370 /** 371 * Play a tone generated by the {@link ToneGenerator}. 372 * @param stream The stream on which the tone will be played. 373 * @param toneVolume The volume of the tone. 374 * @param toneType The type of tone to play. 375 * @param toneLengthMillis How long to play the tone. 376 */ 377 private void playToneGeneratorTone(int stream, int toneVolume, int toneType, 378 int toneLengthMillis) { 379 ToneGenerator toneGenerator = null; 380 try { 381 // If the ToneGenerator creation fails, just continue without it. It is a local audio 382 // signal, and is not as important. 383 try { 384 toneGenerator = mToneGenerator.get(stream, toneVolume); 385 } catch (RuntimeException e) { 386 Log.w(this, "Failed to create ToneGenerator.", e); 387 return; 388 } 389 390 Log.i(this, "playToneGeneratorTone: toneType=%d", toneType); 391 // TODO: Certain CDMA tones need to check the ringer-volume state before 392 // playing. See CallNotifier.InCallTonePlayer. 393 394 // TODO: Some tones play through the end of a call so we need to inform 395 // CallAudioManager that we want focus the same way that Ringer does. 396 397 synchronized (this) { 398 if (mState != STATE_STOPPED) { 399 mState = STATE_ON; 400 toneGenerator.startTone(toneType); 401 try { 402 Log.v(this, "Starting tone %d...waiting for %d ms.", mToneId, 403 toneLengthMillis + TIMEOUT_BUFFER_MILLIS); 404 wait(toneLengthMillis + TIMEOUT_BUFFER_MILLIS); 405 } catch (InterruptedException e) { 406 Log.w(this, "wait interrupted", e); 407 } 408 } 409 } 410 mState = STATE_OFF; 411 } finally { 412 if (toneGenerator != null) { 413 toneGenerator.release(); 414 } 415 } 416 } 417 418 /** 419 * Plays an audio-file based media tone. 420 * @param stream The audio stream on which to play the tone. 421 * @param toneResourceId The resource ID of the tone to play. 422 */ 423 private void playMediaTone(int stream, int toneResourceId) { 424 synchronized (this) { 425 if (mState != STATE_STOPPED) { 426 mState = STATE_ON; 427 } 428 Log.i(this, "playMediaTone: toneResourceId=%d", toneResourceId); 429 AudioAttributes attributes = new AudioAttributes.Builder() 430 .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) 431 .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) 432 .setLegacyStreamType(stream) 433 .build(); 434 mToneMediaPlayer = mMediaPlayerFactory.get(toneResourceId, attributes); 435 mToneMediaPlayer.setLooping(false); 436 int durationMillis = mToneMediaPlayer.getDuration(); 437 final CountDownLatch toneLatch = new CountDownLatch(1); 438 mToneMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { 439 @Override 440 public void onCompletion(MediaPlayer mp) { 441 Log.i(this, "playMediaTone: toneResourceId=%d completed.", toneResourceId); 442 synchronized (InCallTonePlayer.this) { 443 mState = STATE_OFF; 444 } 445 mToneMediaPlayer.release(); 446 mToneMediaPlayer = null; 447 toneLatch.countDown(); 448 } 449 }); 450 mToneMediaPlayer.start(); 451 try { 452 // Wait for the tone to stop playing; timeout at 2x the length of the file just to 453 // be on the safe side. 454 toneLatch.await(durationMillis * 2, TimeUnit.MILLISECONDS); 455 } catch (InterruptedException ie) { 456 Log.e(this, ie, "playMediaTone: tone playback interrupted."); 457 } 458 } 459 460 } 461 462 @VisibleForTesting 463 public boolean startTone() { 464 // Skip playing the end call tone if the volume is silenced. 465 if (mToneId == TONE_CALL_ENDED && !mAudioManagerAdapter.isVolumeOverZero()) { 466 Log.i(this, "startTone: skip end-call tone as device is silenced."); 467 return false; 468 } 469 470 sTonesPlaying++; 471 if (sTonesPlaying == 1) { 472 mCallAudioManager.setIsTonePlaying(true); 473 } 474 475 synchronized (mSessionLock) { 476 if (mSession != null) { 477 Log.cancelSubsession(mSession); 478 } 479 mSession = Log.createSubsession(); 480 } 481 482 super.start(); 483 return true; 484 } 485 486 @Override 487 public void start() { 488 Log.w(this, "Do not call the start method directly; use startTone instead."); 489 } 490 491 /** 492 * Stops the tone. 493 */ 494 @VisibleForTesting 495 public void stopTone() { 496 synchronized (this) { 497 if (mState == STATE_ON) { 498 Log.d(this, "Stopping the tone %d.", mToneId); 499 notify(); 500 } 501 mState = STATE_STOPPED; 502 } 503 } 504 505 @VisibleForTesting 506 public void cleanup() { 507 sTonesPlaying = 0; 508 } 509 510 private void cleanUpTonePlayer() { 511 // Release focus on the main thread. 512 mMainThreadHandler.post(new Runnable("ICTP.cUTP", mLock) { 513 @Override 514 public void loggedRun() { 515 if (sTonesPlaying == 0) { 516 Log.wtf(this, "Over-releasing focus for tone player."); 517 } else if (--sTonesPlaying == 0 && mCallAudioManager != null) { 518 mCallAudioManager.setIsTonePlaying(false); 519 } 520 } 521 }.prepare()); 522 } 523 } 524