AsyncRingtonePlayer.java revision 4d8e08e17d4a5206cba13cd66cdcfe4dac9dde7a
1package com.android.deskclock; 2 3import android.content.Context; 4import android.media.AudioAttributes; 5import android.media.AudioManager; 6import android.media.MediaPlayer; 7import android.media.Ringtone; 8import android.media.RingtoneManager; 9import android.net.Uri; 10import android.os.Build; 11import android.os.Bundle; 12import android.os.Handler; 13import android.os.HandlerThread; 14import android.os.Looper; 15import android.os.Message; 16import android.telephony.TelephonyManager; 17 18import java.io.IOException; 19import java.lang.reflect.Method; 20 21/** 22 * <p>Plays the alarm ringtone. Uses {@link Ringtone} in a separate thread so that this class can be 23 * used from the main thread. Consequently, problems controlling the ringtone do not cause ANRs in 24 * the main thread of the application.</p> 25 * 26 * <p>This class also serves a second purpose. It accomplishes alarm ringtone playback using two 27 * different mechanisms depending on the underlying platform.</p> 28 * 29 * <ul> 30 * <li>Prior to the M platform release, ringtone playback is accomplished using 31 * {@link MediaPlayer}. android.permission.READ_EXTERNAL_STORAGE is required to play custom 32 * ringtones located on the SD card using this mechanism. {@link MediaPlayer} allows clients to 33 * adjust the volume of the stream and specify that the stream should be looped.</li> 34 * 35 * <li>Starting with the M platform release, ringtone playback is accomplished using 36 * {@link Ringtone}. android.permission.READ_EXTERNAL_STORAGE is <strong>NOT</strong> required 37 * to play custom ringtones located on the SD card using this mechanism. {@link Ringtone} allows 38 * clients to adjust the volume of the stream and specify that the stream should be looped but 39 * those methods are marked @hide in M and thus invoked using reflection. Consequently, revoking 40 * the android.permission.READ_EXTERNAL_STORAGE permission has no effect on playback in M+.</li> 41 * </ul> 42 */ 43public class AsyncRingtonePlayer { 44 45 private static final String TAG = "AsyncRingtonePlayer"; 46 47 // Volume suggested by media team for in-call alarms. 48 private static final float IN_CALL_VOLUME = 0.125f; 49 50 // Message codes used with the ringtone thread. 51 private static final int EVENT_PLAY = 1; 52 private static final int EVENT_STOP = 2; 53 private static final String RINGTONE_URI_KEY = "RINGTONE_URI_KEY"; 54 55 /** Handler running on the ringtone thread. */ 56 private Handler mHandler; 57 58 /** {@link MediaPlayerPlaybackDelegate} on pre M; {@link RingtonePlaybackDelegate} on M+ */ 59 private PlaybackDelegate mPlaybackDelegate; 60 61 /** The context. */ 62 private final Context mContext; 63 64 public AsyncRingtonePlayer(Context context) { 65 mContext = context; 66 } 67 68 /** Plays the ringtone. */ 69 public void play(Uri ringtoneUri) { 70 LogUtils.d(TAG, "Posting play."); 71 postMessage(EVENT_PLAY, ringtoneUri); 72 } 73 74 /** Stops playing the ringtone. */ 75 public void stop() { 76 LogUtils.d(TAG, "Posting stop."); 77 postMessage(EVENT_STOP, null); 78 } 79 80 /** 81 * Posts a message to the ringtone-thread handler. 82 * 83 * @param messageCode The message to post. 84 */ 85 private void postMessage(int messageCode, Uri ringtoneUri) { 86 synchronized (this) { 87 if (mHandler == null) { 88 mHandler = getNewHandler(); 89 } 90 91 final Message message = mHandler.obtainMessage(messageCode); 92 if (ringtoneUri != null) { 93 final Bundle bundle = new Bundle(); 94 bundle.putParcelable(RINGTONE_URI_KEY, ringtoneUri); 95 message.setData(bundle); 96 } 97 message.sendToTarget(); 98 } 99 } 100 101 /** 102 * Creates a new ringtone Handler running in its own thread. 103 */ 104 private Handler getNewHandler() { 105 final HandlerThread thread = new HandlerThread("ringtone-player"); 106 thread.start(); 107 108 return new Handler(thread.getLooper()) { 109 @Override 110 public void handleMessage(Message msg) { 111 switch (msg.what) { 112 case EVENT_PLAY: 113 final Uri ringtoneUri = msg.getData().getParcelable(RINGTONE_URI_KEY); 114 getPlaybackDelegate().play(mContext, ringtoneUri); 115 break; 116 case EVENT_STOP: 117 getPlaybackDelegate().stop(mContext); 118 break; 119 } 120 } 121 }; 122 } 123 124 /** 125 * @return <code>true</code> iff the device is currently in a telephone call 126 */ 127 private static boolean isInTelephoneCall(Context context) { 128 final TelephonyManager tm = (TelephonyManager) 129 context.getSystemService(Context.TELEPHONY_SERVICE); 130 return tm.getCallState() != TelephonyManager.CALL_STATE_IDLE; 131 } 132 133 /** 134 * @return Uri of the ringtone to play when the user is in a telephone call 135 */ 136 private static Uri getInCallRingtoneUri(Context context) { 137 final String packageName = context.getPackageName(); 138 return Uri.parse("android.resource://" + packageName + "/" + R.raw.in_call_alarm); 139 } 140 141 /** 142 * @return Uri of the ringtone to play when the chosen ringtone fails to play 143 */ 144 private static Uri getFallbackRingtoneUri(Context context) { 145 final String packageName = context.getPackageName(); 146 return Uri.parse("android.resource://" + packageName + "/" + R.raw.fallbackring); 147 } 148 149 /** 150 * @return the platform-specific playback delegate to use to play the ringtone 151 */ 152 private PlaybackDelegate getPlaybackDelegate() { 153 if (mPlaybackDelegate == null) { 154 // TODO change this to Build.VERSION.SDK_INT > Build.VERSION.LOLLIPOP_MR1 155 if (Build.VERSION.CODENAME.equals("MNC")) { 156 // Use the newer Ringtone-based playback delegate because it does not require 157 // any permissions to read from the SD card. (M+) 158 mPlaybackDelegate = new RingtonePlaybackDelegate(); 159 } else { 160 // Fall back to the older MediaPlayer-based playback delegate because it is the only 161 // way to force the looping of the ringtone before M. (pre M) 162 mPlaybackDelegate = new MediaPlayerPlaybackDelegate(); 163 } 164 } 165 166 return mPlaybackDelegate; 167 } 168 169 /** 170 * This interface abstracts away the differences between playing ringtones via {@link Ringtone} 171 * vs {@link MediaPlayer}. 172 */ 173 private interface PlaybackDelegate { 174 void play(Context context, Uri ringtoneUri); 175 void stop(Context context); 176 } 177 178 /** 179 * Loops playback of a ringtone using {@link MediaPlayer}. 180 */ 181 private static class MediaPlayerPlaybackDelegate implements PlaybackDelegate { 182 183 /** The audio focus manager. Only used by the ringtone thread. */ 184 private AudioManager mAudioManager; 185 186 /** Non-{@code null} while playing a ringtone; {@code null} otherwise. */ 187 private MediaPlayer mMediaPlayer; 188 189 /** 190 * Starts the actual playback of the ringtone. Executes on ringtone-thread. 191 */ 192 @Override 193 public void play(final Context context, Uri ringtoneUri) { 194 if (Looper.getMainLooper() == Looper.myLooper()) { 195 LogUtils.e(TAG, "Must not be on the main thread!", new IllegalStateException()); 196 } 197 198 LogUtils.i(TAG, "Play ringtone via android.media.MediaPlayer."); 199 200 if (mAudioManager == null) { 201 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 202 } 203 204 Uri alarmNoise = ringtoneUri; 205 // Fall back to the default alarm if the database does not have an alarm stored. 206 if (alarmNoise == null) { 207 alarmNoise = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM); 208 LogUtils.v("Using default alarm: " + alarmNoise.toString()); 209 } 210 211 mMediaPlayer = new MediaPlayer(); 212 mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { 213 @Override 214 public boolean onError(MediaPlayer mp, int what, int extra) { 215 LogUtils.e("Error occurred while playing audio. Stopping AlarmKlaxon."); 216 stop(context); 217 return true; 218 } 219 }); 220 221 try { 222 // Check if we are in a call. If we are, use the in-call alarm resource at a 223 // low volume to not disrupt the call. 224 if (isInTelephoneCall(context)) { 225 LogUtils.v("Using the in-call alarm"); 226 mMediaPlayer.setVolume(IN_CALL_VOLUME, IN_CALL_VOLUME); 227 alarmNoise = getInCallRingtoneUri(context); 228 } 229 230 // If alarmNoise is a custom ringtone on the sd card the app must be granted 231 // android.permission.READ_EXTERNAL_STORAGE. Pre-M this is ensured at app 232 // installation time. M+, this permission can be revoked by the user any time. 233 mMediaPlayer.setDataSource(context, alarmNoise); 234 235 startAlarm(mMediaPlayer); 236 } catch (Throwable t) { 237 LogUtils.e("Use the fallback ringtone, original was " + alarmNoise, t); 238 // The alarmNoise may be on the sd card which could be busy right now. 239 // Use the fallback ringtone. 240 try { 241 // Must reset the media player to clear the error state. 242 mMediaPlayer.reset(); 243 mMediaPlayer.setDataSource(context, getFallbackRingtoneUri(context)); 244 startAlarm(mMediaPlayer); 245 } catch (Throwable t2) { 246 // At this point we just don't play anything. 247 LogUtils.e("Failed to play fallback ringtone", t2); 248 } 249 } 250 } 251 252 /** 253 * Do the common stuff when starting the alarm. 254 */ 255 private void startAlarm(MediaPlayer player) throws IOException { 256 // do not play alarms if stream volume is 0 (typically because ringer mode is silent). 257 if (mAudioManager.getStreamVolume(AudioManager.STREAM_ALARM) != 0) { 258 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 259 player.setAudioAttributes(new AudioAttributes.Builder() 260 .setUsage(AudioAttributes.USAGE_ALARM) 261 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) 262 .build()); 263 } 264 265 player.setAudioStreamType(AudioManager.STREAM_ALARM); 266 player.setLooping(true); 267 player.prepare(); 268 mAudioManager.requestAudioFocus(null, AudioManager.STREAM_ALARM, 269 AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); 270 player.start(); 271 } 272 } 273 274 /** 275 * Stops the playback of the ringtone. Executes on the ringtone-thread. 276 */ 277 @Override 278 public void stop(Context context) { 279 if (Looper.getMainLooper() == Looper.myLooper()) { 280 LogUtils.e(TAG, "Must not be on the main thread!", new IllegalStateException()); 281 } 282 283 LogUtils.i(TAG, "Stop ringtone via android.media.MediaPlayer."); 284 285 // Stop audio playing 286 if (mMediaPlayer != null) { 287 mMediaPlayer.stop(); 288 mAudioManager.abandonAudioFocus(null); 289 mMediaPlayer.release(); 290 mMediaPlayer = null; 291 } 292 } 293 } 294 295 /** 296 * Loops playback of a ringtone using {@link Ringtone}. 297 */ 298 private static class RingtonePlaybackDelegate implements PlaybackDelegate { 299 300 /** The audio focus manager. Only used by the ringtone thread. */ 301 private AudioManager mAudioManager; 302 303 /** The current ringtone. Only used by the ringtone thread. */ 304 private Ringtone mRingtone; 305 306 /** The method to adjust playback volume; cannot be null. */ 307 private Method mSetVolumeMethod; 308 309 /** The method to adjust playback looping; cannot be null. */ 310 private Method mSetLoopingMethod; 311 312 private RingtonePlaybackDelegate() { 313 try { 314 mSetVolumeMethod = Ringtone.class.getDeclaredMethod("setVolume", float.class); 315 } catch (NoSuchMethodException nsme) { 316 LogUtils.e(TAG, "Unable to locate method: Ringtone.setVolume(float).", nsme); 317 } 318 319 try { 320 mSetLoopingMethod = Ringtone.class.getDeclaredMethod("setLooping", boolean.class); 321 } catch (NoSuchMethodException nsme) { 322 LogUtils.e(TAG, "Unable to locate method: Ringtone.setLooping(boolean).", nsme); 323 } 324 } 325 326 /** 327 * Starts the actual playback of the ringtone. Executes on ringtone-thread. 328 */ 329 @Override 330 public void play(Context context, Uri ringtoneUri) { 331 if (Looper.getMainLooper() == Looper.myLooper()) { 332 LogUtils.e(TAG, "Must not be on the main thread!", new IllegalStateException()); 333 } 334 335 LogUtils.i(TAG, "Play ringtone via android.media.Ringtone."); 336 337 if (mAudioManager == null) { 338 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 339 } 340 341 final boolean inTelephoneCall = isInTelephoneCall(context); 342 if (inTelephoneCall) { 343 ringtoneUri = getInCallRingtoneUri(context); 344 } 345 346 // attempt to fetch the specified ringtone 347 mRingtone = RingtoneManager.getRingtone(context, ringtoneUri); 348 349 // Attempt to enable looping the ringtone. 350 try { 351 mSetLoopingMethod.invoke(mRingtone, true); 352 } catch (Exception e) { 353 LogUtils.e(TAG, "Unable to turn looping on for android.media.Ringtone", e); 354 355 // Fall back to the default ringtone if looping could not be enabled. 356 // (Default alarm ringtone most likely has looping tags set within the .ogg file) 357 mRingtone = null; 358 } 359 360 if (mRingtone == null) { 361 // fall back to the default ringtone 362 final Uri defaultUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM); 363 mRingtone = RingtoneManager.getRingtone(context, defaultUri); 364 } 365 366 // if we don't have a ringtone at this point there isn't much recourse 367 if (mRingtone == null) { 368 LogUtils.i(TAG, "Unable to locate alarm ringtone."); 369 return; 370 } 371 372 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 373 mRingtone.setAudioAttributes(new AudioAttributes.Builder() 374 .setUsage(AudioAttributes.USAGE_ALARM) 375 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) 376 .build()); 377 } 378 379 // Attempt to adjust the ringtone volume if the user is in a telephone call. 380 if (inTelephoneCall) { 381 LogUtils.v("Using the in-call alarm"); 382 try { 383 mSetVolumeMethod.invoke(mRingtone, IN_CALL_VOLUME); 384 } catch (Exception e) { 385 LogUtils.e(TAG, "Unable to set in-call volume for android.media.Ringtone", e); 386 } 387 } 388 389 mAudioManager.requestAudioFocus(null, AudioManager.STREAM_ALARM, 390 AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); 391 mRingtone.play(); 392 } 393 394 /** 395 * Stops the playback of the ringtone. Executes on the ringtone-thread. 396 */ 397 @Override 398 public void stop(Context context) { 399 if (Looper.getMainLooper() == Looper.myLooper()) { 400 LogUtils.e(TAG, "Must not be on the main thread!", new IllegalStateException()); 401 } 402 403 LogUtils.i(TAG, "Stop ringtone via android.media.Ringtone."); 404 405 if (mRingtone != null && mRingtone.isPlaying()) { 406 LogUtils.d(TAG, "Ringtone.stop() invoked."); 407 mRingtone.stop(); 408 } 409 410 if (mAudioManager != null) { 411 mAudioManager.abandonAudioFocus(null); 412 } 413 } 414 } 415} 416 417