1 package com.android.deskclock; 2 3 import android.annotation.SuppressLint; 4 import android.content.Context; 5 import android.media.AudioAttributes; 6 import android.media.AudioManager; 7 import android.media.MediaPlayer; 8 import android.media.Ringtone; 9 import android.media.RingtoneManager; 10 import android.net.Uri; 11 import android.os.Bundle; 12 import android.os.Handler; 13 import android.os.HandlerThread; 14 import android.os.Looper; 15 import android.os.Message; 16 import android.telephony.TelephonyManager; 17 18 import java.io.IOException; 19 import java.lang.reflect.Method; 20 21 import static android.media.AudioManager.AUDIOFOCUS_GAIN_TRANSIENT; 22 import static android.media.AudioManager.STREAM_ALARM; 23 24 /** 25 * <p>This class controls playback of ringtones. Uses {@link Ringtone} or {@link MediaPlayer} in a 26 * dedicated thread so that this class can be called from the main thread. Consequently, problems 27 * controlling the ringtone do not cause ANRs in the main thread of the application.</p> 28 * 29 * <p>This class also serves a second purpose. It accomplishes alarm ringtone playback using two 30 * different mechanisms depending on the underlying platform.</p> 31 * 32 * <ul> 33 * <li>Prior to the M platform release, ringtone playback is accomplished using 34 * {@link MediaPlayer}. android.permission.READ_EXTERNAL_STORAGE is required to play custom 35 * ringtones located on the SD card using this mechanism. {@link MediaPlayer} allows clients to 36 * adjust the volume of the stream and specify that the stream should be looped.</li> 37 * 38 * <li>Starting with the M platform release, ringtone playback is accomplished using 39 * {@link Ringtone}. android.permission.READ_EXTERNAL_STORAGE is <strong>NOT</strong> required 40 * to play custom ringtones located on the SD card using this mechanism. {@link Ringtone} allows 41 * clients to adjust the volume of the stream and specify that the stream should be looped but 42 * those methods are marked @hide in M and thus invoked using reflection. Consequently, revoking 43 * the android.permission.READ_EXTERNAL_STORAGE permission has no effect on playback in M+.</li> 44 * </ul> 45 * 46 * <p>If either the {@link Ringtone} or {@link MediaPlayer} fails to play the requested audio, an 47 * {@link #getFallbackRingtoneUri in-app fallback} is used because playing <strong>some</strong> 48 * sort of noise is always preferable to remaining silent.</p> 49 */ 50 public final class AsyncRingtonePlayer { 51 52 private static final LogUtils.Logger LOGGER = new LogUtils.Logger("AsyncRingtonePlayer"); 53 54 // Volume suggested by media team for in-call alarms. 55 private static final float IN_CALL_VOLUME = 0.125f; 56 57 // Message codes used with the ringtone thread. 58 private static final int EVENT_PLAY = 1; 59 private static final int EVENT_STOP = 2; 60 private static final int EVENT_VOLUME = 3; 61 private static final String RINGTONE_URI_KEY = "RINGTONE_URI_KEY"; 62 private static final String CRESCENDO_DURATION_KEY = "CRESCENDO_DURATION_KEY"; 63 64 /** Handler running on the ringtone thread. */ 65 private Handler mHandler; 66 67 /** {@link MediaPlayerPlaybackDelegate} on pre M; {@link RingtonePlaybackDelegate} on M+ */ 68 private PlaybackDelegate mPlaybackDelegate; 69 70 /** The context. */ 71 private final Context mContext; 72 AsyncRingtonePlayer(Context context)73 public AsyncRingtonePlayer(Context context) { 74 mContext = context; 75 } 76 77 /** Plays the ringtone. */ play(Uri ringtoneUri, long crescendoDuration)78 public void play(Uri ringtoneUri, long crescendoDuration) { 79 LOGGER.d("Posting play."); 80 postMessage(EVENT_PLAY, ringtoneUri, crescendoDuration, 0); 81 } 82 83 /** Stops playing the ringtone. */ stop()84 public void stop() { 85 LOGGER.d("Posting stop."); 86 postMessage(EVENT_STOP, null, 0, 0); 87 } 88 89 /** Schedules an adjustment of the playback volume 50ms in the future. */ scheduleVolumeAdjustment()90 private void scheduleVolumeAdjustment() { 91 LOGGER.v("Adjusting volume."); 92 93 // Ensure we never have more than one volume adjustment queued. 94 mHandler.removeMessages(EVENT_VOLUME); 95 96 // Queue the next volume adjustment. 97 postMessage(EVENT_VOLUME, null, 0, 50); 98 } 99 100 /** 101 * Posts a message to the ringtone-thread handler. 102 * 103 * @param messageCode the message to post 104 * @param ringtoneUri the ringtone in question, if any 105 * @param crescendoDuration the length of time, in ms, over which to crescendo the ringtone 106 * @param delayMillis the amount of time to delay sending the message, if any 107 */ postMessage(int messageCode, Uri ringtoneUri, long crescendoDuration, long delayMillis)108 private void postMessage(int messageCode, Uri ringtoneUri, long crescendoDuration, 109 long delayMillis) { 110 synchronized (this) { 111 if (mHandler == null) { 112 mHandler = getNewHandler(); 113 } 114 115 final Message message = mHandler.obtainMessage(messageCode); 116 if (ringtoneUri != null) { 117 final Bundle bundle = new Bundle(); 118 bundle.putParcelable(RINGTONE_URI_KEY, ringtoneUri); 119 bundle.putLong(CRESCENDO_DURATION_KEY, crescendoDuration); 120 message.setData(bundle); 121 } 122 123 mHandler.sendMessageDelayed(message, delayMillis); 124 } 125 } 126 127 /** 128 * Creates a new ringtone Handler running in its own thread. 129 */ 130 @SuppressLint("HandlerLeak") getNewHandler()131 private Handler getNewHandler() { 132 final HandlerThread thread = new HandlerThread("ringtone-player"); 133 thread.start(); 134 135 return new Handler(thread.getLooper()) { 136 @Override 137 public void handleMessage(Message msg) { 138 switch (msg.what) { 139 case EVENT_PLAY: 140 final Bundle data = msg.getData(); 141 final Uri ringtoneUri = data.getParcelable(RINGTONE_URI_KEY); 142 final long crescendoDuration = data.getLong(CRESCENDO_DURATION_KEY); 143 if (getPlaybackDelegate().play(mContext, ringtoneUri, crescendoDuration)) { 144 scheduleVolumeAdjustment(); 145 } 146 break; 147 case EVENT_STOP: 148 getPlaybackDelegate().stop(mContext); 149 break; 150 case EVENT_VOLUME: 151 if (getPlaybackDelegate().adjustVolume(mContext)) { 152 scheduleVolumeAdjustment(); 153 } 154 break; 155 } 156 } 157 }; 158 } 159 160 /** 161 * @return <code>true</code> iff the device is currently in a telephone call 162 */ 163 private static boolean isInTelephoneCall(Context context) { 164 final TelephonyManager tm = (TelephonyManager) 165 context.getSystemService(Context.TELEPHONY_SERVICE); 166 return tm.getCallState() != TelephonyManager.CALL_STATE_IDLE; 167 } 168 169 /** 170 * @return Uri of the ringtone to play when the user is in a telephone call 171 */ 172 private static Uri getInCallRingtoneUri(Context context) { 173 return Utils.getResourceUri(context, R.raw.alarm_expire); 174 } 175 176 /** 177 * @return Uri of the ringtone to play when the chosen ringtone fails to play 178 */ 179 private static Uri getFallbackRingtoneUri(Context context) { 180 return Utils.getResourceUri(context, R.raw.alarm_expire); 181 } 182 183 /** 184 * Check if the executing thread is the one dedicated to controlling the ringtone playback. 185 */ 186 private void checkAsyncRingtonePlayerThread() { 187 if (Looper.myLooper() != mHandler.getLooper()) { 188 LOGGER.e("Must be on the AsyncRingtonePlayer thread!", 189 new IllegalStateException()); 190 } 191 } 192 193 /** 194 * @param currentTime current time of the device 195 * @param stopTime time at which the crescendo finishes 196 * @param duration length of time over which the crescendo occurs 197 * @return the scalar volume value that produces a linear increase in volume (in decibels) 198 */ 199 private static float computeVolume(long currentTime, long stopTime, long duration) { 200 // Compute the percentage of the crescendo that has completed. 201 final float elapsedCrescendoTime = stopTime - currentTime; 202 final float fractionComplete = 1 - (elapsedCrescendoTime / duration); 203 204 // Use the fraction to compute a target decibel between -40dB (near silent) and 0dB (max). 205 final float gain = (fractionComplete * 40) - 40; 206 207 // Convert the target gain (in decibels) into the corresponding volume scalar. 208 final float volume = (float) Math.pow(10f, gain/20f); 209 210 LOGGER.v("Ringtone crescendo %,.2f%% complete (scalar: %f, volume: %f dB)", 211 fractionComplete * 100, volume, gain); 212 213 return volume; 214 } 215 216 /** 217 * @return the platform-specific playback delegate to use to play the ringtone 218 */ 219 private PlaybackDelegate getPlaybackDelegate() { 220 checkAsyncRingtonePlayerThread(); 221 222 if (mPlaybackDelegate == null) { 223 if (Utils.isMOrLater()) { 224 // Use the newer Ringtone-based playback delegate because it does not require 225 // any permissions to read from the SD card. (M+) 226 mPlaybackDelegate = new RingtonePlaybackDelegate(); 227 } else { 228 // Fall back to the older MediaPlayer-based playback delegate because it is the only 229 // way to force the looping of the ringtone before M. (pre M) 230 mPlaybackDelegate = new MediaPlayerPlaybackDelegate(); 231 } 232 } 233 234 return mPlaybackDelegate; 235 } 236 237 /** 238 * This interface abstracts away the differences between playing ringtones via {@link Ringtone} 239 * vs {@link MediaPlayer}. 240 */ 241 private interface PlaybackDelegate { 242 /** 243 * @return {@code true} iff a {@link #adjustVolume volume adjustment} should be scheduled 244 */ 245 boolean play(Context context, Uri ringtoneUri, long crescendoDuration); 246 247 /** 248 * Stop any ongoing ringtone playback. 249 */ 250 void stop(Context context); 251 252 /** 253 * @return {@code true} iff another volume adjustment should be scheduled 254 */ 255 boolean adjustVolume(Context context); 256 } 257 258 /** 259 * Loops playback of a ringtone using {@link MediaPlayer}. 260 */ 261 private class MediaPlayerPlaybackDelegate implements PlaybackDelegate { 262 263 /** The audio focus manager. Only used by the ringtone thread. */ 264 private AudioManager mAudioManager; 265 266 /** Non-{@code null} while playing a ringtone; {@code null} otherwise. */ 267 private MediaPlayer mMediaPlayer; 268 269 /** The duration over which to increase the volume. */ 270 private long mCrescendoDuration = 0; 271 272 /** The time at which the crescendo shall cease; 0 if no crescendo is present. */ 273 private long mCrescendoStopTime = 0; 274 275 /** 276 * Starts the actual playback of the ringtone. Executes on ringtone-thread. 277 */ 278 @Override 279 public boolean play(final Context context, Uri ringtoneUri, long crescendoDuration) { 280 checkAsyncRingtonePlayerThread(); 281 mCrescendoDuration = crescendoDuration; 282 283 LOGGER.i("Play ringtone via android.media.MediaPlayer."); 284 285 if (mAudioManager == null) { 286 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 287 } 288 289 final boolean inTelephoneCall = isInTelephoneCall(context); 290 Uri alarmNoise = inTelephoneCall ? getInCallRingtoneUri(context) : ringtoneUri; 291 // Fall back to the system default alarm if the database does not have an alarm stored. 292 if (alarmNoise == null) { 293 alarmNoise = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM); 294 LOGGER.v("Using default alarm: " + alarmNoise.toString()); 295 } 296 297 mMediaPlayer = new MediaPlayer(); 298 mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { 299 @Override 300 public boolean onError(MediaPlayer mp, int what, int extra) { 301 LOGGER.e("Error occurred while playing audio. Stopping AlarmKlaxon."); 302 stop(context); 303 return true; 304 } 305 }); 306 307 try { 308 // If alarmNoise is a custom ringtone on the sd card the app must be granted 309 // android.permission.READ_EXTERNAL_STORAGE. Pre-M this is ensured at app 310 // installation time. M+, this permission can be revoked by the user any time. 311 mMediaPlayer.setDataSource(context, alarmNoise); 312 313 return startPlayback(inTelephoneCall); 314 } catch (Throwable t) { 315 LOGGER.e("Using the fallback ringtone, could not play " + alarmNoise, t); 316 // The alarmNoise may be on the sd card which could be busy right now. 317 // Use the fallback ringtone. 318 try { 319 // Must reset the media player to clear the error state. 320 mMediaPlayer.reset(); 321 mMediaPlayer.setDataSource(context, getFallbackRingtoneUri(context)); 322 return startPlayback(inTelephoneCall); 323 } catch (Throwable t2) { 324 // At this point we just don't play anything. 325 LOGGER.e("Failed to play fallback ringtone", t2); 326 } 327 } 328 329 return false; 330 } 331 332 /** 333 * Prepare the MediaPlayer for playback if the alarm stream is not muted, then start the 334 * playback. 335 * 336 * @param inTelephoneCall {@code true} if there is currently an active telephone call 337 * @return {@code true} if a crescendo has started and future volume adjustments are 338 * required to advance the crescendo effect 339 */ 340 private boolean startPlayback(boolean inTelephoneCall) 341 throws IOException { 342 // Do not play alarms if stream volume is 0 (typically because ringer mode is silent). 343 if (mAudioManager.getStreamVolume(STREAM_ALARM) == 0) { 344 return false; 345 } 346 347 // Indicate the ringtone should be played via the alarm stream. 348 if (Utils.isLOrLater()) { 349 mMediaPlayer.setAudioAttributes(new AudioAttributes.Builder() 350 .setUsage(AudioAttributes.USAGE_ALARM) 351 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) 352 .build()); 353 } 354 355 // Check if we are in a call. If we are, use the in-call alarm resource at a low volume 356 // to not disrupt the call. 357 boolean scheduleVolumeAdjustment = false; 358 if (inTelephoneCall) { 359 LOGGER.v("Using the in-call alarm"); 360 mMediaPlayer.setVolume(IN_CALL_VOLUME, IN_CALL_VOLUME); 361 } else if (mCrescendoDuration > 0) { 362 mMediaPlayer.setVolume(0, 0); 363 364 // Compute the time at which the crescendo will stop. 365 mCrescendoStopTime = Utils.now() + mCrescendoDuration; 366 scheduleVolumeAdjustment = true; 367 } 368 369 mMediaPlayer.setAudioStreamType(STREAM_ALARM); 370 mMediaPlayer.setLooping(true); 371 mMediaPlayer.prepare(); 372 mAudioManager.requestAudioFocus(null, STREAM_ALARM, AUDIOFOCUS_GAIN_TRANSIENT); 373 mMediaPlayer.start(); 374 375 return scheduleVolumeAdjustment; 376 } 377 378 /** 379 * Stops the playback of the ringtone. Executes on the ringtone-thread. 380 */ 381 @Override 382 public void stop(Context context) { 383 checkAsyncRingtonePlayerThread(); 384 385 LOGGER.i("Stop ringtone via android.media.MediaPlayer."); 386 387 mCrescendoDuration = 0; 388 mCrescendoStopTime = 0; 389 390 // Stop audio playing 391 if (mMediaPlayer != null) { 392 mMediaPlayer.stop(); 393 mMediaPlayer.release(); 394 mMediaPlayer = null; 395 } 396 397 if (mAudioManager != null) { 398 mAudioManager.abandonAudioFocus(null); 399 } 400 } 401 402 /** 403 * Adjusts the volume of the ringtone being played to create a crescendo effect. 404 */ 405 @Override 406 public boolean adjustVolume(Context context) { 407 checkAsyncRingtonePlayerThread(); 408 409 // If media player is absent or not playing, ignore volume adjustment. 410 if (mMediaPlayer == null || !mMediaPlayer.isPlaying()) { 411 mCrescendoDuration = 0; 412 mCrescendoStopTime = 0; 413 return false; 414 } 415 416 // If the crescendo is complete set the volume to the maximum; we're done. 417 final long currentTime = Utils.now(); 418 if (currentTime > mCrescendoStopTime) { 419 mCrescendoDuration = 0; 420 mCrescendoStopTime = 0; 421 mMediaPlayer.setVolume(1, 1); 422 return false; 423 } 424 425 // The current volume of the crescendo is the percentage of the crescendo completed. 426 final float volume = computeVolume(currentTime, mCrescendoStopTime, mCrescendoDuration); 427 mMediaPlayer.setVolume(volume, volume); 428 LOGGER.i("MediaPlayer volume set to " + volume); 429 430 // Schedule the next volume bump in the crescendo. 431 return true; 432 } 433 } 434 435 /** 436 * Loops playback of a ringtone using {@link Ringtone}. 437 */ 438 private class RingtonePlaybackDelegate implements PlaybackDelegate { 439 440 /** The audio focus manager. Only used by the ringtone thread. */ 441 private AudioManager mAudioManager; 442 443 /** The current ringtone. Only used by the ringtone thread. */ 444 private Ringtone mRingtone; 445 446 /** The method to adjust playback volume; cannot be null. */ 447 private Method mSetVolumeMethod; 448 449 /** The method to adjust playback looping; cannot be null. */ 450 private Method mSetLoopingMethod; 451 452 /** The duration over which to increase the volume. */ 453 private long mCrescendoDuration = 0; 454 455 /** The time at which the crescendo shall cease; 0 if no crescendo is present. */ 456 private long mCrescendoStopTime = 0; 457 458 private RingtonePlaybackDelegate() { 459 try { 460 mSetVolumeMethod = Ringtone.class.getDeclaredMethod("setVolume", float.class); 461 } catch (NoSuchMethodException nsme) { 462 LOGGER.e("Unable to locate method: Ringtone.setVolume(float).", nsme); 463 } 464 465 try { 466 mSetLoopingMethod = Ringtone.class.getDeclaredMethod("setLooping", boolean.class); 467 } catch (NoSuchMethodException nsme) { 468 LOGGER.e("Unable to locate method: Ringtone.setLooping(boolean).", nsme); 469 } 470 } 471 472 /** 473 * Starts the actual playback of the ringtone. Executes on ringtone-thread. 474 */ 475 @Override 476 public boolean play(Context context, Uri ringtoneUri, long crescendoDuration) { 477 checkAsyncRingtonePlayerThread(); 478 mCrescendoDuration = crescendoDuration; 479 480 LOGGER.i("Play ringtone via android.media.Ringtone."); 481 482 if (mAudioManager == null) { 483 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 484 } 485 486 final boolean inTelephoneCall = isInTelephoneCall(context); 487 if (inTelephoneCall) { 488 ringtoneUri = getInCallRingtoneUri(context); 489 } 490 491 // Attempt to fetch the specified ringtone. 492 mRingtone = RingtoneManager.getRingtone(context, ringtoneUri); 493 494 if (mRingtone == null) { 495 // Fall back to the system default ringtone. 496 ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM); 497 mRingtone = RingtoneManager.getRingtone(context, ringtoneUri); 498 } 499 500 // Attempt to enable looping the ringtone. 501 try { 502 mSetLoopingMethod.invoke(mRingtone, true); 503 } catch (Exception e) { 504 LOGGER.e("Unable to turn looping on for android.media.Ringtone", e); 505 506 // Fall back to the default ringtone if looping could not be enabled. 507 // (Default alarm ringtone most likely has looping tags set within the .ogg file) 508 mRingtone = null; 509 } 510 511 // If no ringtone exists at this point there isn't much recourse. 512 if (mRingtone == null) { 513 LOGGER.i("Unable to locate alarm ringtone, using internal fallback ringtone."); 514 ringtoneUri = getFallbackRingtoneUri(context); 515 mRingtone = RingtoneManager.getRingtone(context, ringtoneUri); 516 } 517 518 try { 519 return startPlayback(inTelephoneCall); 520 } catch (Throwable t) { 521 LOGGER.e("Using the fallback ringtone, could not play " + ringtoneUri, t); 522 // Recover from any/all playback errors by attempting to play the fallback tone. 523 mRingtone = RingtoneManager.getRingtone(context, getFallbackRingtoneUri(context)); 524 try { 525 return startPlayback(inTelephoneCall); 526 } catch (Throwable t2) { 527 // At this point we just don't play anything. 528 LOGGER.e("Failed to play fallback ringtone", t2); 529 } 530 } 531 532 return false; 533 } 534 535 /** 536 * Prepare the Ringtone for playback, then start the playback. 537 * 538 * @param inTelephoneCall {@code true} if there is currently an active telephone call 539 * @return {@code true} if a crescendo has started and future volume adjustments are 540 * required to advance the crescendo effect 541 */ 542 private boolean startPlayback(boolean inTelephoneCall) { 543 // Indicate the ringtone should be played via the alarm stream. 544 if (Utils.isLOrLater()) { 545 mRingtone.setAudioAttributes(new AudioAttributes.Builder() 546 .setUsage(AudioAttributes.USAGE_ALARM) 547 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) 548 .build()); 549 } 550 551 // Attempt to adjust the ringtone volume if the user is in a telephone call. 552 boolean scheduleVolumeAdjustment = false; 553 if (inTelephoneCall) { 554 LOGGER.v("Using the in-call alarm"); 555 setRingtoneVolume(IN_CALL_VOLUME); 556 } else if (mCrescendoDuration > 0) { 557 setRingtoneVolume(0); 558 559 // Compute the time at which the crescendo will stop. 560 mCrescendoStopTime = Utils.now() + mCrescendoDuration; 561 scheduleVolumeAdjustment = true; 562 } 563 564 mAudioManager.requestAudioFocus(null, STREAM_ALARM, AUDIOFOCUS_GAIN_TRANSIENT); 565 566 mRingtone.play(); 567 568 return scheduleVolumeAdjustment; 569 } 570 571 /** 572 * Sets the volume of the ringtone. 573 * 574 * @param volume a raw scalar in range 0.0 to 1.0, where 0.0 mutes this player, and 1.0 575 * corresponds to no attenuation being applied. 576 */ 577 private void setRingtoneVolume(float volume) { 578 try { 579 mSetVolumeMethod.invoke(mRingtone, volume); 580 } catch (Exception e) { 581 LOGGER.e("Unable to set volume for android.media.Ringtone", e); 582 } 583 } 584 585 /** 586 * Stops the playback of the ringtone. Executes on the ringtone-thread. 587 */ 588 @Override 589 public void stop(Context context) { 590 checkAsyncRingtonePlayerThread(); 591 592 LOGGER.i("Stop ringtone via android.media.Ringtone."); 593 594 mCrescendoDuration = 0; 595 mCrescendoStopTime = 0; 596 597 if (mRingtone != null && mRingtone.isPlaying()) { 598 LOGGER.d("Ringtone.stop() invoked."); 599 mRingtone.stop(); 600 } 601 602 mRingtone = null; 603 604 if (mAudioManager != null) { 605 mAudioManager.abandonAudioFocus(null); 606 } 607 } 608 609 /** 610 * Adjusts the volume of the ringtone being played to create a crescendo effect. 611 */ 612 @Override 613 public boolean adjustVolume(Context context) { 614 checkAsyncRingtonePlayerThread(); 615 616 // If ringtone is absent or not playing, ignore volume adjustment. 617 if (mRingtone == null || !mRingtone.isPlaying()) { 618 mCrescendoDuration = 0; 619 mCrescendoStopTime = 0; 620 return false; 621 } 622 623 // If the crescendo is complete set the volume to the maximum; we're done. 624 final long currentTime = Utils.now(); 625 if (currentTime > mCrescendoStopTime) { 626 mCrescendoDuration = 0; 627 mCrescendoStopTime = 0; 628 setRingtoneVolume(1); 629 return false; 630 } 631 632 final float volume = computeVolume(currentTime, mCrescendoStopTime, mCrescendoDuration); 633 setRingtoneVolume(volume); 634 635 // Schedule the next volume bump in the crescendo. 636 return true; 637 } 638 } 639 }