1 /* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.media; 18 19 import android.content.ContentProvider; 20 import android.content.ContentResolver; 21 import android.content.Context; 22 import android.content.res.AssetFileDescriptor; 23 import android.content.res.Resources.NotFoundException; 24 import android.database.Cursor; 25 import android.media.MediaPlayer.OnCompletionListener; 26 import android.net.Uri; 27 import android.os.Binder; 28 import android.os.RemoteException; 29 import android.provider.MediaStore; 30 import android.provider.Settings; 31 import android.provider.MediaStore.MediaColumns; 32 import android.util.Log; 33 34 import java.io.IOException; 35 import java.util.ArrayList; 36 37 /** 38 * Ringtone provides a quick method for playing a ringtone, notification, or 39 * other similar types of sounds. 40 * <p> 41 * For ways of retrieving {@link Ringtone} objects or to show a ringtone 42 * picker, see {@link RingtoneManager}. 43 * 44 * @see RingtoneManager 45 */ 46 public class Ringtone { 47 private static final String TAG = "Ringtone"; 48 private static final boolean LOGD = true; 49 50 private static final String[] MEDIA_COLUMNS = new String[] { 51 MediaStore.Audio.Media._ID, 52 MediaStore.Audio.Media.DATA, 53 MediaStore.Audio.Media.TITLE 54 }; 55 /** Selection that limits query results to just audio files */ 56 private static final String MEDIA_SELECTION = MediaColumns.MIME_TYPE + " LIKE 'audio/%' OR " 57 + MediaColumns.MIME_TYPE + " IN ('application/ogg', 'application/x-flac')"; 58 59 // keep references on active Ringtones until stopped or completion listener called. 60 private static final ArrayList<Ringtone> sActiveRingtones = new ArrayList<Ringtone>(); 61 62 private final Context mContext; 63 private final AudioManager mAudioManager; 64 65 /** 66 * Flag indicating if we're allowed to fall back to remote playback using 67 * {@link #mRemotePlayer}. Typically this is false when we're the remote 68 * player and there is nobody else to delegate to. 69 */ 70 private final boolean mAllowRemote; 71 private final IRingtonePlayer mRemotePlayer; 72 private final Binder mRemoteToken; 73 74 private MediaPlayer mLocalPlayer; 75 private final MyOnCompletionListener mCompletionListener = new MyOnCompletionListener(); 76 77 private Uri mUri; 78 private String mTitle; 79 80 private AudioAttributes mAudioAttributes = new AudioAttributes.Builder() 81 .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) 82 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) 83 .build(); 84 // playback properties, use synchronized with mPlaybackSettingsLock 85 private boolean mIsLooping = false; 86 private float mVolume = 1.0f; 87 private final Object mPlaybackSettingsLock = new Object(); 88 89 /** {@hide} */ Ringtone(Context context, boolean allowRemote)90 public Ringtone(Context context, boolean allowRemote) { 91 mContext = context; 92 mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); 93 mAllowRemote = allowRemote; 94 mRemotePlayer = allowRemote ? mAudioManager.getRingtonePlayer() : null; 95 mRemoteToken = allowRemote ? new Binder() : null; 96 } 97 98 /** 99 * Sets the stream type where this ringtone will be played. 100 * 101 * @param streamType The stream, see {@link AudioManager}. 102 * @deprecated use {@link #setAudioAttributes(AudioAttributes)} 103 */ 104 @Deprecated setStreamType(int streamType)105 public void setStreamType(int streamType) { 106 PlayerBase.deprecateStreamTypeForPlayback(streamType, "Ringtone", "setStreamType()"); 107 setAudioAttributes(new AudioAttributes.Builder() 108 .setInternalLegacyStreamType(streamType) 109 .build()); 110 } 111 112 /** 113 * Gets the stream type where this ringtone will be played. 114 * 115 * @return The stream type, see {@link AudioManager}. 116 * @deprecated use of stream types is deprecated, see 117 * {@link #setAudioAttributes(AudioAttributes)} 118 */ 119 @Deprecated getStreamType()120 public int getStreamType() { 121 return AudioAttributes.toLegacyStreamType(mAudioAttributes); 122 } 123 124 /** 125 * Sets the {@link AudioAttributes} for this ringtone. 126 * @param attributes the non-null attributes characterizing this ringtone. 127 */ setAudioAttributes(AudioAttributes attributes)128 public void setAudioAttributes(AudioAttributes attributes) 129 throws IllegalArgumentException { 130 if (attributes == null) { 131 throw new IllegalArgumentException("Invalid null AudioAttributes for Ringtone"); 132 } 133 mAudioAttributes = attributes; 134 // The audio attributes have to be set before the media player is prepared. 135 // Re-initialize it. 136 setUri(mUri); 137 } 138 139 /** 140 * Returns the {@link AudioAttributes} used by this object. 141 * @return the {@link AudioAttributes} that were set with 142 * {@link #setAudioAttributes(AudioAttributes)} or the default attributes if none were set. 143 */ getAudioAttributes()144 public AudioAttributes getAudioAttributes() { 145 return mAudioAttributes; 146 } 147 148 /** 149 * Sets the player to be looping or non-looping. 150 * @param looping whether to loop or not. 151 */ setLooping(boolean looping)152 public void setLooping(boolean looping) { 153 synchronized (mPlaybackSettingsLock) { 154 mIsLooping = looping; 155 applyPlaybackProperties_sync(); 156 } 157 } 158 159 /** 160 * Returns whether the looping mode was enabled on this player. 161 * @return true if this player loops when playing. 162 */ isLooping()163 public boolean isLooping() { 164 synchronized (mPlaybackSettingsLock) { 165 return mIsLooping; 166 } 167 } 168 169 /** 170 * Sets the volume on this player. 171 * @param volume a raw scalar in range 0.0 to 1.0, where 0.0 mutes this player, and 1.0 172 * corresponds to no attenuation being applied. 173 */ setVolume(float volume)174 public void setVolume(float volume) { 175 synchronized (mPlaybackSettingsLock) { 176 if (volume < 0.0f) { volume = 0.0f; } 177 if (volume > 1.0f) { volume = 1.0f; } 178 mVolume = volume; 179 applyPlaybackProperties_sync(); 180 } 181 } 182 183 /** 184 * Returns the volume scalar set on this player. 185 * @return a value between 0.0f and 1.0f. 186 */ getVolume()187 public float getVolume() { 188 synchronized (mPlaybackSettingsLock) { 189 return mVolume; 190 } 191 } 192 193 /** 194 * Must be called synchronized on mPlaybackSettingsLock 195 */ applyPlaybackProperties_sync()196 private void applyPlaybackProperties_sync() { 197 if (mLocalPlayer != null) { 198 mLocalPlayer.setVolume(mVolume); 199 mLocalPlayer.setLooping(mIsLooping); 200 } else if (mAllowRemote && (mRemotePlayer != null)) { 201 try { 202 mRemotePlayer.setPlaybackProperties(mRemoteToken, mVolume, mIsLooping); 203 } catch (RemoteException e) { 204 Log.w(TAG, "Problem setting playback properties: ", e); 205 } 206 } else { 207 Log.w(TAG, 208 "Neither local nor remote player available when applying playback properties"); 209 } 210 } 211 212 /** 213 * Returns a human-presentable title for ringtone. Looks in media 214 * content provider. If not in either, uses the filename 215 * 216 * @param context A context used for querying. 217 */ getTitle(Context context)218 public String getTitle(Context context) { 219 if (mTitle != null) return mTitle; 220 return mTitle = getTitle(context, mUri, true /*followSettingsUri*/, mAllowRemote); 221 } 222 223 /** 224 * @hide 225 */ getTitle( Context context, Uri uri, boolean followSettingsUri, boolean allowRemote)226 public static String getTitle( 227 Context context, Uri uri, boolean followSettingsUri, boolean allowRemote) { 228 ContentResolver res = context.getContentResolver(); 229 230 String title = null; 231 232 if (uri != null) { 233 String authority = ContentProvider.getAuthorityWithoutUserId(uri.getAuthority()); 234 235 if (Settings.AUTHORITY.equals(authority)) { 236 if (followSettingsUri) { 237 Uri actualUri = RingtoneManager.getActualDefaultRingtoneUri(context, 238 RingtoneManager.getDefaultType(uri)); 239 String actualTitle = getTitle( 240 context, actualUri, false /*followSettingsUri*/, allowRemote); 241 title = context 242 .getString(com.android.internal.R.string.ringtone_default_with_actual, 243 actualTitle); 244 } 245 } else { 246 Cursor cursor = null; 247 try { 248 if (MediaStore.AUTHORITY.equals(authority)) { 249 final String mediaSelection = allowRemote ? null : MEDIA_SELECTION; 250 cursor = res.query(uri, MEDIA_COLUMNS, mediaSelection, null, null); 251 if (cursor != null && cursor.getCount() == 1) { 252 cursor.moveToFirst(); 253 return cursor.getString(2); 254 } 255 // missing cursor is handled below 256 } 257 } catch (SecurityException e) { 258 IRingtonePlayer mRemotePlayer = null; 259 if (allowRemote) { 260 AudioManager audioManager = 261 (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 262 mRemotePlayer = audioManager.getRingtonePlayer(); 263 } 264 if (mRemotePlayer != null) { 265 try { 266 title = mRemotePlayer.getTitle(uri); 267 } catch (RemoteException re) { 268 } 269 } 270 } finally { 271 if (cursor != null) { 272 cursor.close(); 273 } 274 cursor = null; 275 } 276 if (title == null) { 277 title = uri.getLastPathSegment(); 278 } 279 } 280 } else { 281 title = context.getString(com.android.internal.R.string.ringtone_silent); 282 } 283 284 if (title == null) { 285 title = context.getString(com.android.internal.R.string.ringtone_unknown); 286 if (title == null) { 287 title = ""; 288 } 289 } 290 291 return title; 292 } 293 294 /** 295 * Set {@link Uri} to be used for ringtone playback. Attempts to open 296 * locally, otherwise will delegate playback to remote 297 * {@link IRingtonePlayer}. 298 * 299 * @hide 300 */ setUri(Uri uri)301 public void setUri(Uri uri) { 302 destroyLocalPlayer(); 303 304 mUri = uri; 305 if (mUri == null) { 306 return; 307 } 308 309 // TODO: detect READ_EXTERNAL and specific content provider case, instead of relying on throwing 310 311 // try opening uri locally before delegating to remote player 312 mLocalPlayer = new MediaPlayer(); 313 try { 314 mLocalPlayer.setDataSource(mContext, mUri); 315 mLocalPlayer.setAudioAttributes(mAudioAttributes); 316 synchronized (mPlaybackSettingsLock) { 317 applyPlaybackProperties_sync(); 318 } 319 mLocalPlayer.prepare(); 320 321 } catch (SecurityException | IOException e) { 322 destroyLocalPlayer(); 323 if (!mAllowRemote) { 324 Log.w(TAG, "Remote playback not allowed: " + e); 325 } 326 } 327 328 if (LOGD) { 329 if (mLocalPlayer != null) { 330 Log.d(TAG, "Successfully created local player"); 331 } else { 332 Log.d(TAG, "Problem opening; delegating to remote player"); 333 } 334 } 335 } 336 337 /** {@hide} */ getUri()338 public Uri getUri() { 339 return mUri; 340 } 341 342 /** 343 * Plays the ringtone. 344 */ play()345 public void play() { 346 if (mLocalPlayer != null) { 347 // do not play ringtones if stream volume is 0 348 // (typically because ringer mode is silent). 349 if (mAudioManager.getStreamVolume( 350 AudioAttributes.toLegacyStreamType(mAudioAttributes)) != 0) { 351 startLocalPlayer(); 352 } 353 } else if (mAllowRemote && (mRemotePlayer != null)) { 354 final Uri canonicalUri = mUri.getCanonicalUri(); 355 final boolean looping; 356 final float volume; 357 synchronized (mPlaybackSettingsLock) { 358 looping = mIsLooping; 359 volume = mVolume; 360 } 361 try { 362 mRemotePlayer.play(mRemoteToken, canonicalUri, mAudioAttributes, volume, looping); 363 } catch (RemoteException e) { 364 if (!playFallbackRingtone()) { 365 Log.w(TAG, "Problem playing ringtone: " + e); 366 } 367 } 368 } else { 369 if (!playFallbackRingtone()) { 370 Log.w(TAG, "Neither local nor remote playback available"); 371 } 372 } 373 } 374 375 /** 376 * Stops a playing ringtone. 377 */ stop()378 public void stop() { 379 if (mLocalPlayer != null) { 380 destroyLocalPlayer(); 381 } else if (mAllowRemote && (mRemotePlayer != null)) { 382 try { 383 mRemotePlayer.stop(mRemoteToken); 384 } catch (RemoteException e) { 385 Log.w(TAG, "Problem stopping ringtone: " + e); 386 } 387 } 388 } 389 destroyLocalPlayer()390 private void destroyLocalPlayer() { 391 if (mLocalPlayer != null) { 392 mLocalPlayer.setOnCompletionListener(null); 393 mLocalPlayer.reset(); 394 mLocalPlayer.release(); 395 mLocalPlayer = null; 396 synchronized (sActiveRingtones) { 397 sActiveRingtones.remove(this); 398 } 399 } 400 } 401 startLocalPlayer()402 private void startLocalPlayer() { 403 if (mLocalPlayer == null) { 404 return; 405 } 406 synchronized (sActiveRingtones) { 407 sActiveRingtones.add(this); 408 } 409 mLocalPlayer.setOnCompletionListener(mCompletionListener); 410 mLocalPlayer.start(); 411 } 412 413 /** 414 * Whether this ringtone is currently playing. 415 * 416 * @return True if playing, false otherwise. 417 */ isPlaying()418 public boolean isPlaying() { 419 if (mLocalPlayer != null) { 420 return mLocalPlayer.isPlaying(); 421 } else if (mAllowRemote && (mRemotePlayer != null)) { 422 try { 423 return mRemotePlayer.isPlaying(mRemoteToken); 424 } catch (RemoteException e) { 425 Log.w(TAG, "Problem checking ringtone: " + e); 426 return false; 427 } 428 } else { 429 Log.w(TAG, "Neither local nor remote playback available"); 430 return false; 431 } 432 } 433 playFallbackRingtone()434 private boolean playFallbackRingtone() { 435 if (mAudioManager.getStreamVolume(AudioAttributes.toLegacyStreamType(mAudioAttributes)) 436 != 0) { 437 int ringtoneType = RingtoneManager.getDefaultType(mUri); 438 if (ringtoneType == -1 || 439 RingtoneManager.getActualDefaultRingtoneUri(mContext, ringtoneType) != null) { 440 // Default ringtone, try fallback ringtone. 441 try { 442 AssetFileDescriptor afd = mContext.getResources().openRawResourceFd( 443 com.android.internal.R.raw.fallbackring); 444 if (afd != null) { 445 mLocalPlayer = new MediaPlayer(); 446 if (afd.getDeclaredLength() < 0) { 447 mLocalPlayer.setDataSource(afd.getFileDescriptor()); 448 } else { 449 mLocalPlayer.setDataSource(afd.getFileDescriptor(), 450 afd.getStartOffset(), 451 afd.getDeclaredLength()); 452 } 453 mLocalPlayer.setAudioAttributes(mAudioAttributes); 454 synchronized (mPlaybackSettingsLock) { 455 applyPlaybackProperties_sync(); 456 } 457 mLocalPlayer.prepare(); 458 startLocalPlayer(); 459 afd.close(); 460 return true; 461 } else { 462 Log.e(TAG, "Could not load fallback ringtone"); 463 } 464 } catch (IOException ioe) { 465 destroyLocalPlayer(); 466 Log.e(TAG, "Failed to open fallback ringtone"); 467 } catch (NotFoundException nfe) { 468 Log.e(TAG, "Fallback ringtone does not exist"); 469 } 470 } else { 471 Log.w(TAG, "not playing fallback for " + mUri); 472 } 473 } 474 return false; 475 } 476 setTitle(String title)477 void setTitle(String title) { 478 mTitle = title; 479 } 480 481 @Override finalize()482 protected void finalize() { 483 if (mLocalPlayer != null) { 484 mLocalPlayer.release(); 485 } 486 } 487 488 class MyOnCompletionListener implements MediaPlayer.OnCompletionListener { 489 @Override onCompletion(MediaPlayer mp)490 public void onCompletion(MediaPlayer mp) { 491 synchronized (sActiveRingtones) { 492 sActiveRingtones.remove(Ringtone.this); 493 } 494 mp.setOnCompletionListener(null); // Help the Java GC: break the refcount cycle. 495 } 496 } 497 } 498