/*
* Copyright (C) 2006 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.media;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.content.res.Resources.NotFoundException;
import android.database.Cursor;
import android.net.Uri;
import android.os.Binder;
import android.os.RemoteException;
import android.provider.MediaStore;
import android.provider.MediaStore.MediaColumns;
import android.provider.Settings;
import android.util.Log;
import java.io.IOException;
import java.util.ArrayList;
/**
* Ringtone provides a quick method for playing a ringtone, notification, or
* other similar types of sounds.
*
* For ways of retrieving {@link Ringtone} objects or to show a ringtone
* picker, see {@link RingtoneManager}.
*
* @see RingtoneManager
*/
public class Ringtone {
private static final String TAG = "Ringtone";
private static final boolean LOGD = true;
private static final String[] MEDIA_COLUMNS = new String[] {
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.TITLE
};
/** Selection that limits query results to just audio files */
private static final String MEDIA_SELECTION = MediaColumns.MIME_TYPE + " LIKE 'audio/%' OR "
+ MediaColumns.MIME_TYPE + " IN ('application/ogg', 'application/x-flac')";
// keep references on active Ringtones until stopped or completion listener called.
private static final ArrayList sActiveRingtones = new ArrayList();
private final Context mContext;
private final AudioManager mAudioManager;
private VolumeShaper.Configuration mVolumeShaperConfig;
private VolumeShaper mVolumeShaper;
/**
* Flag indicating if we're allowed to fall back to remote playback using
* {@link #mRemotePlayer}. Typically this is false when we're the remote
* player and there is nobody else to delegate to.
*/
private final boolean mAllowRemote;
private final IRingtonePlayer mRemotePlayer;
private final Binder mRemoteToken;
@UnsupportedAppUsage
private MediaPlayer mLocalPlayer;
private final MyOnCompletionListener mCompletionListener = new MyOnCompletionListener();
@UnsupportedAppUsage
private Uri mUri;
private String mTitle;
private AudioAttributes mAudioAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build();
// playback properties, use synchronized with mPlaybackSettingsLock
private boolean mIsLooping = false;
private float mVolume = 1.0f;
private final Object mPlaybackSettingsLock = new Object();
/** {@hide} */
@UnsupportedAppUsage
public Ringtone(Context context, boolean allowRemote) {
mContext = context;
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
mAllowRemote = allowRemote;
mRemotePlayer = allowRemote ? mAudioManager.getRingtonePlayer() : null;
mRemoteToken = allowRemote ? new Binder() : null;
}
/**
* Sets the stream type where this ringtone will be played.
*
* @param streamType The stream, see {@link AudioManager}.
* @deprecated use {@link #setAudioAttributes(AudioAttributes)}
*/
@Deprecated
public void setStreamType(int streamType) {
PlayerBase.deprecateStreamTypeForPlayback(streamType, "Ringtone", "setStreamType()");
setAudioAttributes(new AudioAttributes.Builder()
.setInternalLegacyStreamType(streamType)
.build());
}
/**
* Gets the stream type where this ringtone will be played.
*
* @return The stream type, see {@link AudioManager}.
* @deprecated use of stream types is deprecated, see
* {@link #setAudioAttributes(AudioAttributes)}
*/
@Deprecated
public int getStreamType() {
return AudioAttributes.toLegacyStreamType(mAudioAttributes);
}
/**
* Sets the {@link AudioAttributes} for this ringtone.
* @param attributes the non-null attributes characterizing this ringtone.
*/
public void setAudioAttributes(AudioAttributes attributes)
throws IllegalArgumentException {
if (attributes == null) {
throw new IllegalArgumentException("Invalid null AudioAttributes for Ringtone");
}
mAudioAttributes = attributes;
// The audio attributes have to be set before the media player is prepared.
// Re-initialize it.
setUri(mUri, mVolumeShaperConfig);
}
/**
* Returns the {@link AudioAttributes} used by this object.
* @return the {@link AudioAttributes} that were set with
* {@link #setAudioAttributes(AudioAttributes)} or the default attributes if none were set.
*/
public AudioAttributes getAudioAttributes() {
return mAudioAttributes;
}
/**
* Sets the player to be looping or non-looping.
* @param looping whether to loop or not.
*/
public void setLooping(boolean looping) {
synchronized (mPlaybackSettingsLock) {
mIsLooping = looping;
applyPlaybackProperties_sync();
}
}
/**
* Returns whether the looping mode was enabled on this player.
* @return true if this player loops when playing.
*/
public boolean isLooping() {
synchronized (mPlaybackSettingsLock) {
return mIsLooping;
}
}
/**
* Sets the volume on this player.
* @param volume a raw scalar in range 0.0 to 1.0, where 0.0 mutes this player, and 1.0
* corresponds to no attenuation being applied.
*/
public void setVolume(float volume) {
synchronized (mPlaybackSettingsLock) {
if (volume < 0.0f) { volume = 0.0f; }
if (volume > 1.0f) { volume = 1.0f; }
mVolume = volume;
applyPlaybackProperties_sync();
}
}
/**
* Returns the volume scalar set on this player.
* @return a value between 0.0f and 1.0f.
*/
public float getVolume() {
synchronized (mPlaybackSettingsLock) {
return mVolume;
}
}
/**
* Must be called synchronized on mPlaybackSettingsLock
*/
private void applyPlaybackProperties_sync() {
if (mLocalPlayer != null) {
mLocalPlayer.setVolume(mVolume);
mLocalPlayer.setLooping(mIsLooping);
} else if (mAllowRemote && (mRemotePlayer != null)) {
try {
mRemotePlayer.setPlaybackProperties(mRemoteToken, mVolume, mIsLooping);
} catch (RemoteException e) {
Log.w(TAG, "Problem setting playback properties: ", e);
}
} else {
Log.w(TAG,
"Neither local nor remote player available when applying playback properties");
}
}
/**
* Returns a human-presentable title for ringtone. Looks in media
* content provider. If not in either, uses the filename
*
* @param context A context used for querying.
*/
public String getTitle(Context context) {
if (mTitle != null) return mTitle;
return mTitle = getTitle(context, mUri, true /*followSettingsUri*/, mAllowRemote);
}
/**
* @hide
*/
public static String getTitle(
Context context, Uri uri, boolean followSettingsUri, boolean allowRemote) {
ContentResolver res = context.getContentResolver();
String title = null;
if (uri != null) {
String authority = ContentProvider.getAuthorityWithoutUserId(uri.getAuthority());
if (Settings.AUTHORITY.equals(authority)) {
if (followSettingsUri) {
Uri actualUri = RingtoneManager.getActualDefaultRingtoneUri(context,
RingtoneManager.getDefaultType(uri));
String actualTitle = getTitle(
context, actualUri, false /*followSettingsUri*/, allowRemote);
title = context
.getString(com.android.internal.R.string.ringtone_default_with_actual,
actualTitle);
}
} else {
Cursor cursor = null;
try {
if (MediaStore.AUTHORITY.equals(authority)) {
final String mediaSelection = allowRemote ? null : MEDIA_SELECTION;
cursor = res.query(uri, MEDIA_COLUMNS, mediaSelection, null, null);
if (cursor != null && cursor.getCount() == 1) {
cursor.moveToFirst();
return cursor.getString(1);
}
// missing cursor is handled below
}
} catch (SecurityException e) {
IRingtonePlayer mRemotePlayer = null;
if (allowRemote) {
AudioManager audioManager =
(AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
mRemotePlayer = audioManager.getRingtonePlayer();
}
if (mRemotePlayer != null) {
try {
title = mRemotePlayer.getTitle(uri);
} catch (RemoteException re) {
}
}
} finally {
if (cursor != null) {
cursor.close();
}
cursor = null;
}
if (title == null) {
title = uri.getLastPathSegment();
}
}
} else {
title = context.getString(com.android.internal.R.string.ringtone_silent);
}
if (title == null) {
title = context.getString(com.android.internal.R.string.ringtone_unknown);
if (title == null) {
title = "";
}
}
return title;
}
/**
* Set {@link Uri} to be used for ringtone playback. Attempts to open
* locally, otherwise will delegate playback to remote
* {@link IRingtonePlayer}.
*
* @hide
*/
@UnsupportedAppUsage
public void setUri(Uri uri) {
setUri(uri, null);
}
/**
* Set {@link Uri} to be used for ringtone playback. Attempts to open
* locally, otherwise will delegate playback to remote
* {@link IRingtonePlayer}. Add {@link VolumeShaper} if required.
*
* @hide
*/
public void setUri(Uri uri, @Nullable VolumeShaper.Configuration volumeShaperConfig) {
mVolumeShaperConfig = volumeShaperConfig;
destroyLocalPlayer();
mUri = uri;
if (mUri == null) {
return;
}
// TODO: detect READ_EXTERNAL and specific content provider case, instead of relying on throwing
// try opening uri locally before delegating to remote player
mLocalPlayer = new MediaPlayer();
try {
mLocalPlayer.setDataSource(mContext, mUri);
mLocalPlayer.setAudioAttributes(mAudioAttributes);
synchronized (mPlaybackSettingsLock) {
applyPlaybackProperties_sync();
}
if (mVolumeShaperConfig != null) {
mVolumeShaper = mLocalPlayer.createVolumeShaper(mVolumeShaperConfig);
}
mLocalPlayer.prepare();
} catch (SecurityException | IOException e) {
destroyLocalPlayer();
if (!mAllowRemote) {
Log.w(TAG, "Remote playback not allowed: " + e);
}
}
if (LOGD) {
if (mLocalPlayer != null) {
Log.d(TAG, "Successfully created local player");
} else {
Log.d(TAG, "Problem opening; delegating to remote player");
}
}
}
/** {@hide} */
@UnsupportedAppUsage
public Uri getUri() {
return mUri;
}
/**
* Plays the ringtone.
*/
public void play() {
if (mLocalPlayer != null) {
// do not play ringtones if stream volume is 0
// (typically because ringer mode is silent).
if (mAudioManager.getStreamVolume(
AudioAttributes.toLegacyStreamType(mAudioAttributes)) != 0) {
startLocalPlayer();
}
} else if (mAllowRemote && (mRemotePlayer != null)) {
final Uri canonicalUri = mUri.getCanonicalUri();
final boolean looping;
final float volume;
synchronized (mPlaybackSettingsLock) {
looping = mIsLooping;
volume = mVolume;
}
try {
mRemotePlayer.playWithVolumeShaping(mRemoteToken, canonicalUri, mAudioAttributes,
volume, looping, mVolumeShaperConfig);
} catch (RemoteException e) {
if (!playFallbackRingtone()) {
Log.w(TAG, "Problem playing ringtone: " + e);
}
}
} else {
if (!playFallbackRingtone()) {
Log.w(TAG, "Neither local nor remote playback available");
}
}
}
/**
* Stops a playing ringtone.
*/
public void stop() {
if (mLocalPlayer != null) {
destroyLocalPlayer();
} else if (mAllowRemote && (mRemotePlayer != null)) {
try {
mRemotePlayer.stop(mRemoteToken);
} catch (RemoteException e) {
Log.w(TAG, "Problem stopping ringtone: " + e);
}
}
}
private void destroyLocalPlayer() {
if (mLocalPlayer != null) {
mLocalPlayer.setOnCompletionListener(null);
mLocalPlayer.reset();
mLocalPlayer.release();
mLocalPlayer = null;
mVolumeShaper = null;
synchronized (sActiveRingtones) {
sActiveRingtones.remove(this);
}
}
}
private void startLocalPlayer() {
if (mLocalPlayer == null) {
return;
}
synchronized (sActiveRingtones) {
sActiveRingtones.add(this);
}
mLocalPlayer.setOnCompletionListener(mCompletionListener);
mLocalPlayer.start();
if (mVolumeShaper != null) {
mVolumeShaper.apply(VolumeShaper.Operation.PLAY);
}
}
/**
* Whether this ringtone is currently playing.
*
* @return True if playing, false otherwise.
*/
public boolean isPlaying() {
if (mLocalPlayer != null) {
return mLocalPlayer.isPlaying();
} else if (mAllowRemote && (mRemotePlayer != null)) {
try {
return mRemotePlayer.isPlaying(mRemoteToken);
} catch (RemoteException e) {
Log.w(TAG, "Problem checking ringtone: " + e);
return false;
}
} else {
Log.w(TAG, "Neither local nor remote playback available");
return false;
}
}
private boolean playFallbackRingtone() {
if (mAudioManager.getStreamVolume(AudioAttributes.toLegacyStreamType(mAudioAttributes))
!= 0) {
int ringtoneType = RingtoneManager.getDefaultType(mUri);
if (ringtoneType == -1 ||
RingtoneManager.getActualDefaultRingtoneUri(mContext, ringtoneType) != null) {
// Default ringtone, try fallback ringtone.
try {
AssetFileDescriptor afd = mContext.getResources().openRawResourceFd(
com.android.internal.R.raw.fallbackring);
if (afd != null) {
mLocalPlayer = new MediaPlayer();
if (afd.getDeclaredLength() < 0) {
mLocalPlayer.setDataSource(afd.getFileDescriptor());
} else {
mLocalPlayer.setDataSource(afd.getFileDescriptor(),
afd.getStartOffset(),
afd.getDeclaredLength());
}
mLocalPlayer.setAudioAttributes(mAudioAttributes);
synchronized (mPlaybackSettingsLock) {
applyPlaybackProperties_sync();
}
if (mVolumeShaperConfig != null) {
mVolumeShaper = mLocalPlayer.createVolumeShaper(mVolumeShaperConfig);
}
mLocalPlayer.prepare();
startLocalPlayer();
afd.close();
return true;
} else {
Log.e(TAG, "Could not load fallback ringtone");
}
} catch (IOException ioe) {
destroyLocalPlayer();
Log.e(TAG, "Failed to open fallback ringtone");
} catch (NotFoundException nfe) {
Log.e(TAG, "Fallback ringtone does not exist");
}
} else {
Log.w(TAG, "not playing fallback for " + mUri);
}
}
return false;
}
void setTitle(String title) {
mTitle = title;
}
@Override
protected void finalize() {
if (mLocalPlayer != null) {
mLocalPlayer.release();
}
}
class MyOnCompletionListener implements MediaPlayer.OnCompletionListener {
@Override
public void onCompletion(MediaPlayer mp) {
synchronized (sActiveRingtones) {
sActiveRingtones.remove(Ringtone.this);
}
mp.setOnCompletionListener(null); // Help the Java GC: break the refcount cycle.
}
}
}