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