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