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