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