blob: 8800dc865a550421ca8249e004d0635094741d79 [file] [log] [blame]
/*
* 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.Manifest;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.media.audiofx.HapticGenerator;
import android.net.Uri;
import android.os.RemoteException;
import android.os.Trace;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.provider.MediaStore;
import android.provider.MediaStore.MediaColumns;
import android.provider.Settings;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Ringtone provides a quick method for playing a ringtone, notification, or
* other similar types of sounds.
* <p>
* 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";
/**
* The ringtone should only play sound. Any vibration is managed externally.
* @hide
*/
public static final int MEDIA_SOUND = 1;
/**
* The ringtone should only play vibration. Any sound is managed externally.
* Requires the {@link android.Manifest.permission#VIBRATE} permission.
* @hide
*/
public static final int MEDIA_VIBRATION = 1 << 1;
/**
* The ringtone should play sound and vibration.
* @hide
*/
public static final int MEDIA_SOUND_AND_VIBRATION = MEDIA_SOUND | MEDIA_VIBRATION;
// This is not a public value, because apps shouldn't enable "all" media - that wouldn't be
// safe if new media types were added.
static final int MEDIA_ALL = MEDIA_SOUND | MEDIA_VIBRATION;
/**
* Declares the types of media that this Ringtone is allowed to play.
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = "MEDIA_", value = {
MEDIA_SOUND,
MEDIA_VIBRATION,
MEDIA_SOUND_AND_VIBRATION,
})
public @interface RingtoneMedia {}
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')";
// Flag-selected ringtone implementation to use.
private final ApiInterface mApiImpl;
/** {@hide} */
@UnsupportedAppUsage
public Ringtone(Context context, boolean allowRemote) {
mApiImpl = new RingtoneV1(context, allowRemote);
}
/**
* Constructor for legacy V1 initialization paths using non-public APIs on RingtoneV1.
*/
private Ringtone(RingtoneV1 ringtoneV1) {
mApiImpl = ringtoneV1;
}
private Ringtone(Builder builder, @Ringtone.RingtoneMedia int effectiveEnabledMedia,
@NonNull AudioAttributes effectiveAudioAttributes,
@Nullable VibrationEffect effectiveVibrationEffect,
boolean effectiveHapticGeneratorEnabled) {
mApiImpl = new RingtoneV2(builder.mContext, builder.mInjectables, builder.mAllowRemote,
effectiveEnabledMedia, builder.mUri, effectiveAudioAttributes,
builder.mUseExactAudioAttributes, builder.mVolumeShaperConfig,
builder.mPreferBuiltinDevice, builder.mInitialSoundVolume, builder.mLooping,
effectiveHapticGeneratorEnabled, effectiveVibrationEffect);
}
/**
* Temporary V1 constructor for legacy V1 paths with audio attributes.
* @hide
*/
public static Ringtone createV1WithCustomAudioAttributes(
Context context, AudioAttributes audioAttributes, Uri uri,
VolumeShaper.Configuration volumeShaperConfig, boolean allowRemote) {
RingtoneV1 ringtoneV1 = new RingtoneV1(context, allowRemote);
ringtoneV1.setAudioAttributesField(audioAttributes);
ringtoneV1.setUri(uri, volumeShaperConfig);
ringtoneV1.reinitializeActivePlayer();
return new Ringtone(ringtoneV1);
}
/**
* Temporary V1 constructor for legacy V1 paths with stream type.
* @hide
*/
public static Ringtone createV1WithCustomStreamType(
Context context, int streamType, Uri uri,
VolumeShaper.Configuration volumeShaperConfig) {
RingtoneV1 ringtoneV1 = new RingtoneV1(context, /* allowRemote= */ true);
if (streamType >= 0) {
ringtoneV1.setStreamType(streamType);
}
ringtoneV1.setUri(uri, volumeShaperConfig);
if (!ringtoneV1.reinitializeActivePlayer()) {
Log.e(TAG, "Failed to open ringtone " + uri);
return null;
}
return new Ringtone(ringtoneV1);
}
/** @hide */
@RingtoneMedia
public int getEnabledMedia() {
return mApiImpl.getEnabledMedia();
}
/**
* 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) {
mApiImpl.setStreamType(streamType);
}
/**
* 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 mApiImpl.getStreamType();
}
/**
* Sets the {@link AudioAttributes} for this ringtone.
* @param attributes the non-null attributes characterizing this ringtone.
*/
public void setAudioAttributes(AudioAttributes attributes)
throws IllegalArgumentException {
mApiImpl.setAudioAttributes(attributes);
}
/**
* Returns the vibration effect that this ringtone was created with, if vibration is enabled.
* Otherwise, returns null.
* @hide
*/
@Nullable
public VibrationEffect getVibrationEffect() {
return mApiImpl.getVibrationEffect();
}
/** @hide */
@VisibleForTesting
public boolean getPreferBuiltinDevice() {
return mApiImpl.getPreferBuiltinDevice();
}
/** @hide */
@VisibleForTesting
public VolumeShaper.Configuration getVolumeShaperConfig() {
return mApiImpl.getVolumeShaperConfig();
}
/**
* Returns whether this player is local only, or can defer to the remote player. The
* result may differ from the builder if there is no remote player available at all.
* @hide
*/
@VisibleForTesting
public boolean isLocalOnly() {
return mApiImpl.isLocalOnly();
}
/** @hide */
@VisibleForTesting
public boolean isUsingRemotePlayer() {
return mApiImpl.isUsingRemotePlayer();
}
/**
* Creates a local media player for the ringtone using currently set attributes.
* @return true if media player creation succeeded or is deferred,
* false if it did not succeed and can't be tried remotely.
* @hide
*/
public boolean reinitializeActivePlayer() {
return mApiImpl.reinitializeActivePlayer();
}
/**
* Same as AudioManager.hasHapticChannels except it assumes an already created ringtone.
* @hide
*/
public boolean hasHapticChannels() {
return mApiImpl.hasHapticChannels();
}
/**
* 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 mApiImpl.getAudioAttributes();
}
/**
* Sets the player to be looping or non-looping.
* @param looping whether to loop or not.
*/
public void setLooping(boolean looping) {
mApiImpl.setLooping(looping);
}
/**
* Returns whether the looping mode was enabled on this player.
* @return true if this player loops when playing.
*/
public boolean isLooping() {
return mApiImpl.isLooping();
}
/**
* 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) {
mApiImpl.setVolume(volume);
}
/**
* Returns the volume scalar set on this player.
* @return a value between 0.0f and 1.0f.
*/
public float getVolume() {
return mApiImpl.getVolume();
}
/**
* Enable or disable the {@link android.media.audiofx.HapticGenerator} effect. The effect can
* only be enabled on devices that support the effect.
*
* @return true if the HapticGenerator effect is successfully enabled. Otherwise, return false.
* @see android.media.audiofx.HapticGenerator#isAvailable()
*/
public boolean setHapticGeneratorEnabled(boolean enabled) {
return mApiImpl.setHapticGeneratorEnabled(enabled);
}
/**
* Return whether the {@link android.media.audiofx.HapticGenerator} effect is enabled or not.
* @return true if the HapticGenerator is enabled.
*/
public boolean isHapticGeneratorEnabled() {
return mApiImpl.isHapticGeneratorEnabled();
}
/**
* 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) {
return mApiImpl.getTitle(context);
}
/**
* @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;
}
/** {@hide} */
@UnsupportedAppUsage
public Uri getUri() {
return mApiImpl.getUri();
}
/**
* Plays the ringtone.
*/
public void play() {
mApiImpl.play();
}
/**
* Stops a playing ringtone.
*/
public void stop() {
mApiImpl.stop();
}
/**
* Whether this ringtone is currently playing.
*
* @return True if playing, false otherwise.
*/
public boolean isPlaying() {
return mApiImpl.isPlaying();
}
/**
* Build a {@link Ringtone} to easily play sounds for ringtones, alarms and notifications.
*
* TODO: when un-hide, deprecate Ringtone: setAudioAttributes, setLooping,
* setHapticGeneratorEnabled (no-effect if MEDIA_VIBRATION),
* static RingtoneManager.getRingtone.
* @hide
*/
public static final class Builder {
private final Context mContext;
private final int mEnabledMedia;
private Uri mUri;
private final AudioAttributes mAudioAttributes;
private boolean mUseExactAudioAttributes = false;
// Not a static default since it doesn't really need to be in memory forever.
private Injectables mInjectables = new Injectables();
private VolumeShaper.Configuration mVolumeShaperConfig;
private boolean mPreferBuiltinDevice = false;
private boolean mAllowRemote = true;
private boolean mHapticGeneratorEnabled = false;
private float mInitialSoundVolume = 1.0f;
private boolean mLooping = false;
private VibrationEffect mVibrationEffect;
/**
* Constructs a builder to play the given media types from the mediaUri. If the mediaUri
* is null (for example, an unset-setting), then fallback logic will dictate what plays.
*
* <p>When built, if the ringtone is already known to be a no-op, such as explicitly
* silent, then the {@link #build} may return null.
*
* @param context The context for playing the ringtone.
* @param enabledMedia Which media to play. Media not included is implicitly muted. Device
* settings such as volume and vibrate-only may also affect which
* media is played.
* @param audioAttributes The attributes to use for playback, which affects the volumes and
* settings that are applied.
*/
public Builder(@NonNull Context context, @RingtoneMedia int enabledMedia,
@NonNull AudioAttributes audioAttributes) {
mContext = context;
mEnabledMedia = enabledMedia;
mAudioAttributes = audioAttributes;
}
/**
* Inject test intercepters for static methods.
* @hide
*/
@NonNull
public Builder setInjectables(Injectables injectables) {
mInjectables = injectables;
return this;
}
/**
* The uri for the ringtone media to play. This is typically a user's preference for the
* sound. If null, then it is treated as though the user's preference is unset and
* fallback behavior, such as using the default ringtone setting, are used instead.
*
* When sound media is enabled, this is assumed to be a sound URI.
*/
@NonNull
public Builder setUri(@Nullable Uri uri) {
mUri = uri;
return this;
}
/**
* Sets the VibrationEffect to use if vibration is enabled on this ringtone. The caller
* should use {@link android.os.Vibrator#areVibrationFeaturesSupported} to ensure
* that the effect is usable on this device, otherwise system defaults will be used.
*
* <p>Vibration will only happen if the Builder was created with media type
* {@link Ringtone#MEDIA_VIBRATION} or {@link Ringtone#MEDIA_SOUND_AND_VIBRATION}, and
* the application has the {@link android.Manifest.permission#VIBRATE} permission.
*
* <p>If the Ringtone is looping when it is played, then the VibrationEffect will be
* modified to loop. Similarly, if the ringtone is not looping, a repeating
* VibrationEffect will be modified to be non-repeating when the ringtone is played. Calls
* to {@link Ringtone#setLooping} after the ringtone has started playing will stop a looping
* vibration, but has no effect otherwise: specifically it will not restart vibration.
*/
@NonNull
public Builder setVibrationEffect(@NonNull VibrationEffect effect) {
mVibrationEffect = effect;
return this;
}
/**
* Sets whether the resulting ringtone should loop until {@link Ringtone#stop()} is called,
* or just play once.
*/
@NonNull
public Builder setLooping(boolean looping) {
mLooping = looping;
return this;
}
/**
* Sets the VolumeShaper.Configuration to apply to the ringtone.
* @hide
*/
@NonNull
public Builder setVolumeShaperConfig(
@Nullable VolumeShaper.Configuration volumeShaperConfig) {
mVolumeShaperConfig = volumeShaperConfig;
return this;
}
/**
* Whether to enable or disable the haptic generator.
* @hide
*/
@NonNull
public Builder setEnableHapticGenerator(boolean enabled) {
// Note that this property is mutable (but deprecated) on the Ringtone class itself.
mHapticGeneratorEnabled = enabled;
return this;
}
/**
* Sets the initial sound volume for the ringtone.
*/
@NonNull
public Builder setInitialSoundVolume(float initialSoundVolume) {
mInitialSoundVolume = initialSoundVolume;
return this;
}
/**
* Sets the preferred device of the ringtone playback to the built-in device. This is
* only for use by the system server with known-good Uris.
* @hide
*/
@NonNull
public Builder setPreferBuiltinDevice() {
mPreferBuiltinDevice = true;
mAllowRemote = false; // Already in system.
return this;
}
/**
* Indicates that {@link AudioAttributes#areHapticChannelsMuted()} on the builder's
* AudioAttributes should not be overridden. This is used to enable legacy behavior of
* calling {@link Ringtone#setAudioAttributes} on an already-created ringtone, and can in
* turn cause vibration during a "sound-only" session or can suppress audio-coupled
* haptics that would usually take priority (therefore potentially falling back to
* the VibrationEffect or system defaults).
*
* <p>Without this setting, the haptic channels will be automatically muted or not by the
* Ringtone according to whether vibration is enabled or not.
*
* <p>This is for internal-use only. New applications should configure the vibration
* behavior explicitly with the (TODO: future RingtoneSetting.setVibrationSource).
* Handling haptic channels outside Ringtone leads to extra loads of the sound uri.
* @hide
*/
@NonNull
public Builder setUseExactAudioAttributes(boolean useExactAttrs) {
mUseExactAudioAttributes = useExactAttrs;
return this;
}
/**
* Prevent fallback to the remote service. This is primarily intended for use within the
* remote IRingtonePlayer service itself, to avoid loops.
* @hide
*/
@NonNull
public Builder setLocalOnly() {
mAllowRemote = false;
return this;
}
private boolean isVibrationEnabledAndAvailable() {
if ((mEnabledMedia & MEDIA_VIBRATION) == 0) {
return false;
}
Vibrator vibrator = mContext.getSystemService(Vibrator.class);
if (!vibrator.hasVibrator()) {
return false;
}
if (mContext.checkSelfPermission(Manifest.permission.VIBRATE)
!= PackageManager.PERMISSION_GRANTED) {
Log.w(TAG, "Ringtone requests vibration enabled, but no VIBRATE permission");
return false;
}
return true;
}
/**
* Returns the built Ringtone, or null if there was a problem loading the Uri and there
* are no fallback options available.
*/
@Nullable
public Ringtone build() {
@Ringtone.RingtoneMedia int effectiveEnabledMedia = mEnabledMedia;
VibrationEffect effectiveVibrationEffect = mVibrationEffect;
// Normalize media to that supported on this SDK level.
if (effectiveEnabledMedia != (effectiveEnabledMedia & MEDIA_ALL)) {
Log.e(TAG, "Unsupported media type: " + effectiveEnabledMedia);
effectiveEnabledMedia = effectiveEnabledMedia & MEDIA_ALL;
}
final boolean effectiveHapticGenerator;
final boolean hapticChannelsSupported;
AudioAttributes effectiveAudioAttributes = mAudioAttributes;
final boolean hapticChannelsMuted = mAudioAttributes.areHapticChannelsMuted();
if (!isVibrationEnabledAndAvailable()) {
// Vibration isn't active: turn off everything that might cause extra work.
effectiveEnabledMedia &= ~MEDIA_VIBRATION;
effectiveHapticGenerator = false;
effectiveVibrationEffect = null;
if (!mUseExactAudioAttributes && !hapticChannelsMuted) {
effectiveAudioAttributes = new AudioAttributes.Builder(effectiveAudioAttributes)
.setHapticChannelsMuted(true)
.build();
}
} else {
// Vibration is active.
effectiveHapticGenerator =
mHapticGeneratorEnabled && mInjectables.isHapticGeneratorAvailable();
hapticChannelsSupported = mInjectables.isHapticPlaybackSupported();
// Haptic channels are preferred if they are available, and not explicitly muted.
// We won't know if haptic channels are available until loading the media player,
// and since the media player needs to be reset to change audio attributes, then
// we proactively enable the channels - it won't matter if they aren't present.
if (!mUseExactAudioAttributes) {
boolean shouldBeMuted = effectiveHapticGenerator || !hapticChannelsSupported;
if (shouldBeMuted != hapticChannelsMuted) {
effectiveAudioAttributes =
new AudioAttributes.Builder(effectiveAudioAttributes)
.setHapticChannelsMuted(shouldBeMuted)
.build();
}
}
// If no contextual vibration, then try loading the default one for the URI.
if (mVibrationEffect == null && mUri != null) {
effectiveVibrationEffect = VibrationEffect.get(mUri, mContext);
}
}
try {
Ringtone ringtone = new Ringtone(this, effectiveEnabledMedia,
effectiveAudioAttributes, effectiveVibrationEffect,
effectiveHapticGenerator);
if (ringtone.reinitializeActivePlayer()) {
return ringtone;
} else {
Log.e(TAG, "Failed to open ringtone " + mUri);
return null;
}
} catch (Exception ex) {
// Catching Exception isn't great, but was done in the old
// RingtoneManager.getRingtone and hides errors like DocumentsProvider throwing
// IllegalArgumentException instead of FileNotFoundException, and also robolectric
// failures when ShadowMediaPlayer wasn't pre-informed of the ringtone.
Log.e(TAG, "Failed while opening ringtone " + mUri, ex);
return null;
}
}
}
/**
* Interface for intercepting static methods and constructors, for unit testing only.
* @hide
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public static class Injectables {
/** Intercept {@code new MediaPlayer()}. */
@NonNull
public MediaPlayer newMediaPlayer() {
return new MediaPlayer();
}
/** Intercept {@link HapticGenerator#isAvailable}. */
public boolean isHapticGeneratorAvailable() {
return HapticGenerator.isAvailable();
}
/**
* Intercept {@link HapticGenerator#create} using
* {@link MediaPlayer#getAudioSessionId()} from the given media player.
*/
@Nullable
public HapticGenerator createHapticGenerator(@NonNull MediaPlayer mediaPlayer) {
return HapticGenerator.create(mediaPlayer.getAudioSessionId());
}
/** Returns the result of {@link AudioManager#isHapticPlaybackSupported()}. */
public boolean isHapticPlaybackSupported() {
return AudioManager.isHapticPlaybackSupported();
}
/**
* Returns whether the MediaPlayer tracks have haptic channels. This is the same as
* AudioManager.hasHapticChannels, except it uses an already prepared MediaPlayer to avoid
* loading the metadata a second time.
*/
public boolean hasHapticChannels(MediaPlayer mp) {
try {
Trace.beginSection("Ringtone.hasHapticChannels");
for (MediaPlayer.TrackInfo trackInfo : mp.getTrackInfo()) {
if (trackInfo.hasHapticChannels()) {
return true;
}
}
} finally {
Trace.endSection();
}
return false;
}
}
/**
* Interface for alternative Ringtone implementations. See the public Ringtone methods that
* delegate to these for documentation.
* @hide
*/
interface ApiInterface {
void setStreamType(int streamType);
int getStreamType();
void setAudioAttributes(AudioAttributes attributes);
boolean getPreferBuiltinDevice();
VolumeShaper.Configuration getVolumeShaperConfig();
boolean isLocalOnly();
boolean isUsingRemotePlayer();
boolean reinitializeActivePlayer();
boolean hasHapticChannels();
AudioAttributes getAudioAttributes();
void setLooping(boolean looping);
boolean isLooping();
void setVolume(float volume);
float getVolume();
boolean setHapticGeneratorEnabled(boolean enabled);
boolean isHapticGeneratorEnabled();
String getTitle(Context context);
Uri getUri();
void play();
void stop();
boolean isPlaying();
// V2 future-public methods
@RingtoneMedia int getEnabledMedia();
VibrationEffect getVibrationEffect();
}
}