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