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