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