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