Home | History | Annotate | Download | only in deskclock
      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      */
     75     public AsyncRingtonePlayer(Context context, String crescendoPrefKey) {
     76         mContext = context;
     77         mCrescendoPrefKey = crescendoPrefKey;
     78     }
     79 
     80     /** Plays the ringtone. */
     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. */
     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. */
     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      */
    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      */
    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