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 * 194 * @hide 195 */ createLocalMediaPlayer()196 public void createLocalMediaPlayer() { 197 Trace.beginSection("createLocalMediaPlayer"); 198 if (mUri == null) { 199 Log.e(TAG, "Could not create media player as no URI was provided."); 200 return; 201 } 202 destroyLocalPlayer(); 203 // try opening uri locally before delegating to remote player 204 mLocalPlayer = new MediaPlayer(); 205 try { 206 mLocalPlayer.setDataSource(mContext, mUri); 207 mLocalPlayer.setAudioAttributes(mAudioAttributes); 208 mLocalPlayer.setPreferredDevice( 209 mPreferBuiltinDevice ? getBuiltinDevice(mAudioManager) : null); 210 synchronized (mPlaybackSettingsLock) { 211 applyPlaybackProperties_sync(); 212 } 213 if (mVolumeShaperConfig != null) { 214 mVolumeShaper = mLocalPlayer.createVolumeShaper(mVolumeShaperConfig); 215 } 216 mLocalPlayer.prepare(); 217 218 } catch (SecurityException | IOException e) { 219 destroyLocalPlayer(); 220 if (!mAllowRemote) { 221 Log.w(TAG, "Remote playback not allowed: " + e); 222 } 223 } 224 225 if (LOGD) { 226 if (mLocalPlayer != null) { 227 Log.d(TAG, "Successfully created local player"); 228 } else { 229 Log.d(TAG, "Problem opening; delegating to remote player"); 230 } 231 } 232 Trace.endSection(); 233 } 234 235 /** 236 * Returns whether a local player has been created for this ringtone. 237 * @hide 238 */ 239 @VisibleForTesting hasLocalPlayer()240 public boolean hasLocalPlayer() { 241 return mLocalPlayer != null; 242 } 243 244 /** 245 * Returns the {@link AudioAttributes} used by this object. 246 * @return the {@link AudioAttributes} that were set with 247 * {@link #setAudioAttributes(AudioAttributes)} or the default attributes if none were set. 248 */ getAudioAttributes()249 public AudioAttributes getAudioAttributes() { 250 return mAudioAttributes; 251 } 252 253 /** 254 * Sets the player to be looping or non-looping. 255 * @param looping whether to loop or not. 256 */ setLooping(boolean looping)257 public void setLooping(boolean looping) { 258 synchronized (mPlaybackSettingsLock) { 259 mIsLooping = looping; 260 applyPlaybackProperties_sync(); 261 } 262 } 263 264 /** 265 * Returns whether the looping mode was enabled on this player. 266 * @return true if this player loops when playing. 267 */ isLooping()268 public boolean isLooping() { 269 synchronized (mPlaybackSettingsLock) { 270 return mIsLooping; 271 } 272 } 273 274 /** 275 * Sets the volume on this player. 276 * @param volume a raw scalar in range 0.0 to 1.0, where 0.0 mutes this player, and 1.0 277 * corresponds to no attenuation being applied. 278 */ setVolume(float volume)279 public void setVolume(float volume) { 280 synchronized (mPlaybackSettingsLock) { 281 if (volume < 0.0f) { volume = 0.0f; } 282 if (volume > 1.0f) { volume = 1.0f; } 283 mVolume = volume; 284 applyPlaybackProperties_sync(); 285 } 286 } 287 288 /** 289 * Returns the volume scalar set on this player. 290 * @return a value between 0.0f and 1.0f. 291 */ getVolume()292 public float getVolume() { 293 synchronized (mPlaybackSettingsLock) { 294 return mVolume; 295 } 296 } 297 298 /** 299 * Enable or disable the {@link android.media.audiofx.HapticGenerator} effect. The effect can 300 * only be enabled on devices that support the effect. 301 * 302 * @return true if the HapticGenerator effect is successfully enabled. Otherwise, return false. 303 * @see android.media.audiofx.HapticGenerator#isAvailable() 304 */ setHapticGeneratorEnabled(boolean enabled)305 public boolean setHapticGeneratorEnabled(boolean enabled) { 306 if (!HapticGenerator.isAvailable()) { 307 return false; 308 } 309 synchronized (mPlaybackSettingsLock) { 310 mHapticGeneratorEnabled = enabled; 311 applyPlaybackProperties_sync(); 312 } 313 return true; 314 } 315 316 /** 317 * Return whether the {@link android.media.audiofx.HapticGenerator} effect is enabled or not. 318 * @return true if the HapticGenerator is enabled. 319 */ isHapticGeneratorEnabled()320 public boolean isHapticGeneratorEnabled() { 321 synchronized (mPlaybackSettingsLock) { 322 return mHapticGeneratorEnabled; 323 } 324 } 325 326 /** 327 * Must be called synchronized on mPlaybackSettingsLock 328 */ applyPlaybackProperties_sync()329 private void applyPlaybackProperties_sync() { 330 if (mLocalPlayer != null) { 331 mLocalPlayer.setVolume(mVolume); 332 mLocalPlayer.setLooping(mIsLooping); 333 if (mHapticGenerator == null && mHapticGeneratorEnabled) { 334 mHapticGenerator = HapticGenerator.create(mLocalPlayer.getAudioSessionId()); 335 } 336 if (mHapticGenerator != null) { 337 mHapticGenerator.setEnabled(mHapticGeneratorEnabled); 338 } 339 } else if (mAllowRemote && (mRemotePlayer != null)) { 340 try { 341 mRemotePlayer.setPlaybackProperties( 342 mRemoteToken, mVolume, mIsLooping, mHapticGeneratorEnabled); 343 } catch (RemoteException e) { 344 Log.w(TAG, "Problem setting playback properties: ", e); 345 } 346 } else { 347 Log.w(TAG, 348 "Neither local nor remote player available when applying playback properties"); 349 } 350 } 351 352 /** 353 * Returns a human-presentable title for ringtone. Looks in media 354 * content provider. If not in either, uses the filename 355 * 356 * @param context A context used for querying. 357 */ getTitle(Context context)358 public String getTitle(Context context) { 359 if (mTitle != null) return mTitle; 360 return mTitle = getTitle(context, mUri, true /*followSettingsUri*/, mAllowRemote); 361 } 362 363 /** 364 * @hide 365 */ getTitle( Context context, Uri uri, boolean followSettingsUri, boolean allowRemote)366 public static String getTitle( 367 Context context, Uri uri, boolean followSettingsUri, boolean allowRemote) { 368 ContentResolver res = context.getContentResolver(); 369 370 String title = null; 371 372 if (uri != null) { 373 String authority = ContentProvider.getAuthorityWithoutUserId(uri.getAuthority()); 374 375 if (Settings.AUTHORITY.equals(authority)) { 376 if (followSettingsUri) { 377 Uri actualUri = RingtoneManager.getActualDefaultRingtoneUri(context, 378 RingtoneManager.getDefaultType(uri)); 379 String actualTitle = getTitle( 380 context, actualUri, false /*followSettingsUri*/, allowRemote); 381 title = context 382 .getString(com.android.internal.R.string.ringtone_default_with_actual, 383 actualTitle); 384 } 385 } else { 386 Cursor cursor = null; 387 try { 388 if (MediaStore.AUTHORITY.equals(authority)) { 389 final String mediaSelection = allowRemote ? null : MEDIA_SELECTION; 390 cursor = res.query(uri, MEDIA_COLUMNS, mediaSelection, null, null); 391 if (cursor != null && cursor.getCount() == 1) { 392 cursor.moveToFirst(); 393 return cursor.getString(1); 394 } 395 // missing cursor is handled below 396 } 397 } catch (SecurityException e) { 398 IRingtonePlayer mRemotePlayer = null; 399 if (allowRemote) { 400 AudioManager audioManager = 401 (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 402 mRemotePlayer = audioManager.getRingtonePlayer(); 403 } 404 if (mRemotePlayer != null) { 405 try { 406 title = mRemotePlayer.getTitle(uri); 407 } catch (RemoteException re) { 408 } 409 } 410 } finally { 411 if (cursor != null) { 412 cursor.close(); 413 } 414 cursor = null; 415 } 416 if (title == null) { 417 title = uri.getLastPathSegment(); 418 } 419 } 420 } else { 421 title = context.getString(com.android.internal.R.string.ringtone_silent); 422 } 423 424 if (title == null) { 425 title = context.getString(com.android.internal.R.string.ringtone_unknown); 426 if (title == null) { 427 title = ""; 428 } 429 } 430 431 return title; 432 } 433 434 /** 435 * Set {@link Uri} to be used for ringtone playback. 436 * {@link IRingtonePlayer}. 437 * 438 * @hide 439 */ 440 @UnsupportedAppUsage setUri(Uri uri)441 public void setUri(Uri uri) { 442 setUri(uri, null); 443 } 444 445 /** 446 * @hide 447 */ setVolumeShaperConfig(@ullable VolumeShaper.Configuration volumeShaperConfig)448 public void setVolumeShaperConfig(@Nullable VolumeShaper.Configuration volumeShaperConfig) { 449 mVolumeShaperConfig = volumeShaperConfig; 450 } 451 452 /** 453 * Set {@link Uri} to be used for ringtone playback. Attempts to open 454 * locally, otherwise will delegate playback to remote 455 * {@link IRingtonePlayer}. Add {@link VolumeShaper} if required. 456 * 457 * @hide 458 */ setUri(Uri uri, @Nullable VolumeShaper.Configuration volumeShaperConfig)459 public void setUri(Uri uri, @Nullable VolumeShaper.Configuration volumeShaperConfig) { 460 mVolumeShaperConfig = volumeShaperConfig; 461 462 mUri = uri; 463 if (mUri == null) { 464 destroyLocalPlayer(); 465 } 466 } 467 468 /** {@hide} */ 469 @UnsupportedAppUsage getUri()470 public Uri getUri() { 471 return mUri; 472 } 473 474 /** 475 * Plays the ringtone. 476 */ play()477 public void play() { 478 if (mLocalPlayer != null) { 479 // Play ringtones if stream volume is over 0 or if it is a haptic-only ringtone 480 // (typically because ringer mode is vibrate). 481 boolean isHapticOnly = AudioManager.hasHapticChannels(mContext, mUri) 482 && !mAudioAttributes.areHapticChannelsMuted() && mVolume == 0; 483 if (isHapticOnly || mAudioManager.getStreamVolume( 484 AudioAttributes.toLegacyStreamType(mAudioAttributes)) != 0) { 485 startLocalPlayer(); 486 } 487 } else if (mAllowRemote && (mRemotePlayer != null) && (mUri != null)) { 488 final Uri canonicalUri = mUri.getCanonicalUri(); 489 final boolean looping; 490 final float volume; 491 synchronized (mPlaybackSettingsLock) { 492 looping = mIsLooping; 493 volume = mVolume; 494 } 495 try { 496 mRemotePlayer.playWithVolumeShaping(mRemoteToken, canonicalUri, mAudioAttributes, 497 volume, looping, mVolumeShaperConfig); 498 } catch (RemoteException e) { 499 if (!playFallbackRingtone()) { 500 Log.w(TAG, "Problem playing ringtone: " + e); 501 } 502 } 503 } else { 504 if (!playFallbackRingtone()) { 505 Log.w(TAG, "Neither local nor remote playback available"); 506 } 507 } 508 } 509 510 /** 511 * Stops a playing ringtone. 512 */ stop()513 public void stop() { 514 if (mLocalPlayer != null) { 515 destroyLocalPlayer(); 516 } else if (mAllowRemote && (mRemotePlayer != null)) { 517 try { 518 mRemotePlayer.stop(mRemoteToken); 519 } catch (RemoteException e) { 520 Log.w(TAG, "Problem stopping ringtone: " + e); 521 } 522 } 523 } 524 destroyLocalPlayer()525 private void destroyLocalPlayer() { 526 if (mLocalPlayer != null) { 527 if (mHapticGenerator != null) { 528 mHapticGenerator.release(); 529 mHapticGenerator = null; 530 } 531 mLocalPlayer.setOnCompletionListener(null); 532 mLocalPlayer.reset(); 533 mLocalPlayer.release(); 534 mLocalPlayer = null; 535 mVolumeShaper = null; 536 synchronized (sActiveRingtones) { 537 sActiveRingtones.remove(this); 538 } 539 } 540 } 541 startLocalPlayer()542 private void startLocalPlayer() { 543 if (mLocalPlayer == null) { 544 return; 545 } 546 synchronized (sActiveRingtones) { 547 sActiveRingtones.add(this); 548 } 549 mLocalPlayer.setOnCompletionListener(mCompletionListener); 550 mLocalPlayer.start(); 551 if (mVolumeShaper != null) { 552 mVolumeShaper.apply(VolumeShaper.Operation.PLAY); 553 } 554 } 555 556 /** 557 * Whether this ringtone is currently playing. 558 * 559 * @return True if playing, false otherwise. 560 */ isPlaying()561 public boolean isPlaying() { 562 if (mLocalPlayer != null) { 563 return mLocalPlayer.isPlaying(); 564 } else if (mAllowRemote && (mRemotePlayer != null)) { 565 try { 566 return mRemotePlayer.isPlaying(mRemoteToken); 567 } catch (RemoteException e) { 568 Log.w(TAG, "Problem checking ringtone: " + e); 569 return false; 570 } 571 } else { 572 Log.w(TAG, "Neither local nor remote playback available"); 573 return false; 574 } 575 } 576 playFallbackRingtone()577 private boolean playFallbackRingtone() { 578 int streamType = AudioAttributes.toLegacyStreamType(mAudioAttributes); 579 if (mAudioManager.getStreamVolume(streamType) == 0) { 580 return false; 581 } 582 int ringtoneType = RingtoneManager.getDefaultType(mUri); 583 if (ringtoneType != -1 && 584 RingtoneManager.getActualDefaultRingtoneUri(mContext, ringtoneType) == null) { 585 Log.w(TAG, "not playing fallback for " + mUri); 586 return false; 587 } 588 // Default ringtone, try fallback ringtone. 589 try { 590 AssetFileDescriptor afd = mContext.getResources().openRawResourceFd( 591 com.android.internal.R.raw.fallbackring); 592 if (afd == null) { 593 Log.e(TAG, "Could not load fallback ringtone"); 594 return false; 595 } 596 mLocalPlayer = new MediaPlayer(); 597 if (afd.getDeclaredLength() < 0) { 598 mLocalPlayer.setDataSource(afd.getFileDescriptor()); 599 } else { 600 mLocalPlayer.setDataSource(afd.getFileDescriptor(), 601 afd.getStartOffset(), 602 afd.getDeclaredLength()); 603 } 604 mLocalPlayer.setAudioAttributes(mAudioAttributes); 605 synchronized (mPlaybackSettingsLock) { 606 applyPlaybackProperties_sync(); 607 } 608 if (mVolumeShaperConfig != null) { 609 mVolumeShaper = mLocalPlayer.createVolumeShaper(mVolumeShaperConfig); 610 } 611 mLocalPlayer.prepare(); 612 startLocalPlayer(); 613 afd.close(); 614 } catch (IOException ioe) { 615 destroyLocalPlayer(); 616 Log.e(TAG, "Failed to open fallback ringtone"); 617 return false; 618 } catch (NotFoundException nfe) { 619 Log.e(TAG, "Fallback ringtone does not exist"); 620 return false; 621 } 622 return true; 623 } 624 setTitle(String title)625 void setTitle(String title) { 626 mTitle = title; 627 } 628 629 @Override finalize()630 protected void finalize() { 631 if (mLocalPlayer != null) { 632 mLocalPlayer.release(); 633 } 634 } 635 636 class MyOnCompletionListener implements MediaPlayer.OnCompletionListener { 637 @Override onCompletion(MediaPlayer mp)638 public void onCompletion(MediaPlayer mp) { 639 synchronized (sActiveRingtones) { 640 sActiveRingtones.remove(Ringtone.this); 641 } 642 mp.setOnCompletionListener(null); // Help the Java GC: break the refcount cycle. 643 } 644 } 645 } 646