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