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