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 }