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